From 36ebc827488aa8a42548d78b573a812b6925af11 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 9 Jul 2016 21:07:56 +0200 Subject: [PATCH 0001/1387] Add platform.SaveFile --- src/borg/platform/__init__.py | 2 +- src/borg/platform/base.py | 44 +++++++++++++++++++++++++++++++++-- src/borg/platform/linux.pyx | 4 ++-- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/borg/platform/__init__.py b/src/borg/platform/__init__.py index 29a97fc4..e1772c5e 100644 --- a/src/borg/platform/__init__.py +++ b/src/borg/platform/__init__.py @@ -8,7 +8,7 @@ Public APIs are documented in platform.base. from .base import acl_get, acl_set from .base import set_flags, get_flags -from .base import SyncFile, sync_dir, fdatasync +from .base import SaveFile, SyncFile, sync_dir, fdatasync from .base import swidth, API_VERSION if sys.platform.startswith('linux'): # pragma: linux only diff --git a/src/borg/platform/base.py b/src/borg/platform/base.py index ef8853e3..9580f06b 100644 --- a/src/borg/platform/base.py +++ b/src/borg/platform/base.py @@ -80,8 +80,11 @@ class SyncFile: TODO: A Windows implementation should use CreateFile with FILE_FLAG_WRITE_THROUGH. """ - def __init__(self, path): - self.fd = open(path, 'xb') + def __init__(self, path, binary=True): + mode = 'x' + if binary: + mode += 'b' + self.fd = open(path, mode) self.fileno = self.fd.fileno() def __enter__(self): @@ -112,6 +115,43 @@ class SyncFile: platform.sync_dir(os.path.dirname(self.fd.name)) +class SaveFile: + """ + Update file contents atomically. + + Must be used as a context manager (defining the scope of the transaction). + + On a journaling file system the file contents are always updated + atomically and won't become corrupted, even on pure failures or + crashes (for caveats see SyncFile). + """ + + SUFFIX = '.tmp' + + def __init__(self, path, binary=True): + self.binary = binary + self.path = path + self.tmppath = self.path + self.SUFFIX + + def __enter__(self): + from .. import platform + try: + os.unlink(self.tmppath) + except OSError: + pass + self.fd = platform.SyncFile(self.tmppath, self.binary) + return self.fd + + def __exit__(self, exc_type, exc_val, exc_tb): + from .. import platform + self.fd.close() + if exc_type is not None: + os.unlink(self.tmppath) + return + os.rename(self.tmppath, self.path) + platform.sync_dir(os.path.dirname(self.path)) + + def swidth(s): """terminal output width of string diff --git a/src/borg/platform/linux.pyx b/src/borg/platform/linux.pyx index 4bbdcc35..7dea321a 100644 --- a/src/borg/platform/linux.pyx +++ b/src/borg/platform/linux.pyx @@ -228,8 +228,8 @@ class SyncFile(BaseSyncFile): disk in the immediate future. """ - def __init__(self, path): - super().__init__(path) + def __init__(self, path, binary=True): + super().__init__(path, binary) self.offset = 0 self.write_window = (16 * 1024 ** 2) & ~PAGE_MASK self.last_sync = 0 From f4be2b352313bd008707d48bacea6e70d3272294 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 9 Jul 2016 21:10:46 +0200 Subject: [PATCH 0002/1387] Use platform.SaveFile for repository, cache and key files Fixes #1060 --- src/borg/cache.py | 9 +++++---- src/borg/key.py | 3 ++- src/borg/repository.py | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 4dc4c218..6dcd88d2 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -19,6 +19,7 @@ from .helpers import yes from .item import Item from .key import PlaintextKey from .locking import UpgradableLock +from .platform import SaveFile from .remote import cache_if_remote ChunkListEntry = namedtuple('ChunkListEntry', 'id size csize') @@ -141,11 +142,11 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" config.set('cache', 'version', '1') config.set('cache', 'repository', self.repository.id_str) config.set('cache', 'manifest', '') - with open(os.path.join(self.path, 'config'), 'w') as fd: + with SaveFile(os.path.join(self.path, 'config'), binary=False) as fd: config.write(fd) ChunkIndex().write(os.path.join(self.path, 'chunks').encode('utf-8')) os.makedirs(os.path.join(self.path, 'chunks.archive.d')) - with open(os.path.join(self.path, 'files'), 'wb') as fd: + with SaveFile(os.path.join(self.path, 'files')) as fd: pass # empty file def _do_open(self): @@ -212,7 +213,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if not self.txn_active: return if self.files is not None: - with open(os.path.join(self.path, 'files'), 'wb') as fd: + with SaveFile(os.path.join(self.path, 'files')) as fd: for path_hash, item in self.files.items(): # Discard cached files with the newest mtime to avoid # issues with filesystem snapshots and mtime precision @@ -223,7 +224,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" self.config.set('cache', 'timestamp', self.manifest.timestamp) self.config.set('cache', 'key_type', str(self.key.TYPE)) self.config.set('cache', 'previous_location', self.repository._location.canonical_path()) - with open(os.path.join(self.path, 'config'), 'w') as fd: + with SaveFile(os.path.join(self.path, 'config'), binary=False) as fd: self.config.write(fd) self.chunks.write(os.path.join(self.path, 'chunks').encode('utf-8')) os.rename(os.path.join(self.path, 'txn.active'), diff --git a/src/borg/key.py b/src/borg/key.py index 6965ae73..2fd4ad3f 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -22,6 +22,7 @@ from .helpers import get_keys_dir from .helpers import bin_to_hex from .helpers import CompressionDecider2, CompressionSpec from .item import Key, EncryptedKey +from .platform import SaveFile PREFIX = b'\0' * 8 @@ -470,7 +471,7 @@ class KeyfileKey(KeyfileKeyBase): def save(self, target, passphrase): key_data = self._save(passphrase) - with open(target, 'w') as fd: + with SaveFile(target, binary=False) as fd: fd.write('%s %s\n' % (self.FILE_ID, bin_to_hex(self.repository_id))) fd.write(key_data) fd.write('\n') diff --git a/src/borg/repository.py b/src/borg/repository.py index 6af3f87d..9d9dc3fb 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -23,7 +23,7 @@ from .helpers import ProgressIndicatorPercent from .helpers import bin_to_hex from .locking import UpgradableLock, LockError, LockErrorT from .lrucache import LRUCache -from .platform import SyncFile, sync_dir +from .platform import SaveFile, SyncFile, sync_dir MAX_OBJECT_SIZE = 20 * 1024 * 1024 MAGIC = b'BORG_SEG' @@ -160,7 +160,7 @@ class Repository: def save_config(self, path, config): config_path = os.path.join(path, 'config') - with open(config_path, 'w') as fd: + with SaveFile(config_path, binary=False) as fd: config.write(fd) def save_key(self, keydata): From 7053a721409fcfe7cceec5934a7cc915e884b7d0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 13 Jul 2016 20:04:20 +0200 Subject: [PATCH 0003/1387] Fix borg break-lock ignoring BORG_REPO env var --- borg/archiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/borg/archiver.py b/borg/archiver.py index 5fc05ee2..cdf12ef7 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1316,7 +1316,7 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='break repository and cache locks') subparser.set_defaults(func=self.do_break_lock) - subparser.add_argument('location', metavar='REPOSITORY', + subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False), help='repository for which to break the locks') From b9952efd68163a909c05cdd3f2a5408e27dd5424 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 15 Jul 2016 00:43:49 +0200 Subject: [PATCH 0004/1387] faq: 'A' unchanged file; remove ambiguous entry age sentence. --- docs/faq.rst | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 87794493..eaeef714 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -336,11 +336,8 @@ I am seeing 'A' (added) status for a unchanged file!? The files cache is used to determine whether |project_name| already "knows" / has backed up a file and if so, to skip the file from -chunking. It does intentionally *not* contain files that: - -- have >= 10 as "entry age" (|project_name| has not seen this file for a while) -- have a modification time (mtime) same as the newest mtime in the created - archive +chunking. It does intentionally *not* contain files that have a modification +time (mtime) same as the newest mtime in the created archive. So, if you see an 'A' status for unchanged file(s), they are likely the files with the most recent mtime in that archive. From 5f713ec1c17edcc6daa79175198cd02197b1b6d3 Mon Sep 17 00:00:00 2001 From: Pankaj Garg Date: Fri, 15 Jul 2016 00:33:49 +0530 Subject: [PATCH 0005/1387] Fixed the issue of xenial64 box being unable to run because vagrant user is absent --- Vagrantfile | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index fc5fdacf..f89a1639 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -12,12 +12,19 @@ end def packages_debianoid return <<-EOF + if id "vagrant" >/dev/null 2>&1; then + username='vagrant' + home_dir=/home/vagrant + else + username='ubuntu' + home_dir=/home/ubuntu + fi apt-get update # install all the (security and other) updates apt-get dist-upgrade -y # for building borgbackup and dependencies: apt-get install -y libssl-dev libacl1-dev liblz4-dev libfuse-dev fuse pkg-config - usermod -a -G fuse vagrant + usermod -a -G fuse $username apt-get install -y fakeroot build-essential git apt-get install -y python3-dev python3-setuptools # for building python: @@ -27,7 +34,7 @@ def packages_debianoid # newer versions are not compatible with py 3.2 any more. easy_install3 'pip<8.0' pip3 install 'virtualenv<14.0' - touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile + touch $home_dir/.bash_profile ; chown $username $home_dir/.bash_profile EOF end @@ -283,7 +290,13 @@ end def fix_perms return <<-EOF # . ~/.profile - chown -R vagrant /vagrant/borg + + if id "vagrant" >/dev/null 2>&1; then + chown -R vagrant /vagrant/borg + else + chown -R ubuntu /vagrant/borg + fi + EOF end @@ -338,6 +351,17 @@ Vagrant.configure(2) do |config| b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("centos6_64") end + config.vm.define "xenial64" do |b| + b.vm.box = "ubuntu/xenial64" + b.vm.provider :virtualbox do |v| + v.memory = 768 + end + b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid + b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("xenial64") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("xenial64") + b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("xenial64") + end + config.vm.define "trusty64" do |b| b.vm.box = "ubuntu/trusty64" b.vm.provider :virtualbox do |v| From 52a72cf73a0d5f9354f100a23c4acd9c30069cc6 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 17 Jul 2016 19:13:09 +0200 Subject: [PATCH 0006/1387] Fix some item subscripts that came from 1.0-maint --- src/borg/archive.py | 2 +- src/borg/archiver.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index b4572772..e4565515 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -429,7 +429,7 @@ Number of files: {0.stats.nfiles}'''.format( sys.stdout.buffer.flush() if has_damaged_chunks: logger.warning('File %s has damaged (all-zero) chunks. Try running borg check --repair.' % - remove_surrogates(item[b'path'])) + remove_surrogates(item.path)) return original_path = original_path or item.path diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 701ddf35..03907193 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -446,7 +446,7 @@ class Archiver: try: archive.extract_item(dir_item, stdout=stdout) except BackupOSError as e: - self.print_warning('%s: %s', remove_surrogates(dir_item[b'path']), e) + self.print_warning('%s: %s', remove_surrogates(dir_item.path), e) if output_list: logging.getLogger('borg.output.list').info(remove_surrogates(orig_path)) try: @@ -468,7 +468,7 @@ class Archiver: try: archive.extract_item(dir_item) except BackupOSError as e: - self.print_warning('%s: %s', remove_surrogates(dir_item[b'path']), e) + self.print_warning('%s: %s', remove_surrogates(dir_item.path), e) for pattern in include_patterns: if pattern.match_count == 0: self.print_warning("Include pattern '%s' never matched.", pattern) From 69f8d3c3f76316e6a5e1e86fb7b95e05889cb206 Mon Sep 17 00:00:00 2001 From: Chris Lamb Date: Mon, 18 Jul 2016 12:14:48 +0200 Subject: [PATCH 0007/1387] Use an OrderedDict for helptext, making the build reproducible Closes #1346 --- borg/archiver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/borg/archiver.py b/borg/archiver.py index cdf12ef7..02da0f1b 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -14,6 +14,7 @@ import stat import sys import textwrap import traceback +import collections from . import __version__ from .helpers import Error, location_validator, archivename_validator, format_line, format_time, format_file_size, \ @@ -736,7 +737,7 @@ class Archiver: Cache.break_lock(repository) return self.exit_code - helptext = {} + helptext = collections.OrderedDict() helptext['patterns'] = textwrap.dedent(''' Exclusion patterns support four separate styles, fnmatch, shell, regular expressions and path prefixes. By default, fnmatch is used. If followed From b33f8b4ff18f7ed793d68aab38837ce17c6b35a5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 21 Jul 2016 21:03:22 +0200 Subject: [PATCH 0008/1387] update faq with new 30 mins checkpoint interval --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index 3eee339a..e2b992d2 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -241,7 +241,7 @@ If a backup stops mid-way, does the already-backed-up data stay there? Yes, |project_name| supports resuming backups. During a backup a special checkpoint archive named ``.checkpoint`` -is saved every checkpoint interval (the default value for this is 5 +is saved every checkpoint interval (the default value for this is 30 minutes) containing all the data backed-up until that point. Checkpoints only happen between files (so they don't help for interruptions From 477b374a286b8ebfbb0ce6f2e279c06df2e007b7 Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Fri, 22 Jul 2016 18:55:14 -0400 Subject: [PATCH 0009/1387] gitignore .coverage.* --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 935517fd..92c86d38 100644 --- a/.gitignore +++ b/.gitignore @@ -23,5 +23,6 @@ borg.build/ borg.dist/ borg.exe .coverage +.coverage.* .vagrant .eggs From 2eef0fb894bb48980d153db3d8c1c47732b37871 Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Fri, 22 Jul 2016 14:02:21 -0400 Subject: [PATCH 0010/1387] Ignore generated platform C files --- .gitignore | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 935517fd..01051899 100644 --- a/.gitignore +++ b/.gitignore @@ -8,10 +8,10 @@ hashindex.c chunker.c compress.c crypto.c -platform_darwin.c -platform_freebsd.c -platform_linux.c -platform_posix.c +src/borg/platform/darwin.c +src/borg/platform/freebsd.c +src/borg/platform/linux.c +src/borg/platform/posix.c *.egg-info *.pyc *.pyo From 88bffab935c12fbdea651c80ad6e4be2dc18ea0e Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Sat, 23 Jul 2016 09:38:43 -0400 Subject: [PATCH 0011/1387] Allow running tests outside of Vagrant Fixes #1361 --- src/borg/testsuite/platform.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/borg/testsuite/platform.py b/src/borg/testsuite/platform.py index 991c98b8..183e10a0 100644 --- a/src/borg/testsuite/platform.py +++ b/src/borg/testsuite/platform.py @@ -2,6 +2,7 @@ import os import shutil import sys import tempfile +import pwd import unittest from ..platform import acl_get, acl_set, swidth @@ -35,6 +36,14 @@ def fakeroot_detected(): return 'FAKEROOTKEY' in os.environ +def user_exists(username): + try: + pwd.getpwnam(username) + return True + except KeyError: + return False + + @unittest.skipUnless(sys.platform.startswith('linux'), 'linux only test') @unittest.skipIf(fakeroot_detected(), 'not compatible with fakeroot') class PlatformLinuxTestCase(BaseTestCase): @@ -72,6 +81,7 @@ class PlatformLinuxTestCase(BaseTestCase): self.assert_equal(self.get_acl(self.tmpdir)['acl_access'], ACCESS_ACL) self.assert_equal(self.get_acl(self.tmpdir)['acl_default'], DEFAULT_ACL) + @unittest.skipIf(not user_exists('übel'), 'requires übel user') def test_non_ascii_acl(self): # Testing non-ascii ACL processing to see whether our code is robust. # I have no idea whether non-ascii ACLs are allowed by the standard, From 868586dd3705752af6360d159fb79789fe553288 Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Sat, 23 Jul 2016 13:05:35 -0400 Subject: [PATCH 0012/1387] Fixed noatime detection --- src/borg/testsuite/archiver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 41e00c20..1c29fd40 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -393,7 +393,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): def has_noatime(some_file): atime_before = os.stat(some_file).st_atime_ns try: - os.close(os.open(some_file, flags_noatime)) + with open(os.open(some_file, flags_noatime)) as file: + file.read() except PermissionError: return False else: From ff108ef24b6fb55e5a44f06f1e157d9091da9a44 Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Sat, 23 Jul 2016 15:03:22 -0400 Subject: [PATCH 0013/1387] Catch a ValueError when checking if a user exists --- src/borg/testsuite/platform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/testsuite/platform.py b/src/borg/testsuite/platform.py index 183e10a0..0001eeec 100644 --- a/src/borg/testsuite/platform.py +++ b/src/borg/testsuite/platform.py @@ -40,7 +40,7 @@ def user_exists(username): try: pwd.getpwnam(username) return True - except KeyError: + except (KeyError, ValueError): return False From 9fe4473ff25df1c4c5519b65851afddbe2727ae9 Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Tue, 26 Jul 2016 13:41:38 -0400 Subject: [PATCH 0014/1387] Ignore stdout/stderr broken pipe errors --- borg/archiver.py | 6 +++--- borg/helpers.py | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 02da0f1b..74c1d1a4 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -22,7 +22,7 @@ from .helpers import Error, location_validator, archivename_validator, format_li get_cache_dir, prune_within, prune_split, \ Manifest, NoManifestError, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \ dir_is_tagged, bigint_to_int, ChunkerParams, CompressionSpec, PrefixSpec, is_slow_msgpack, yes, sysinfo, \ - EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, log_multi, PatternMatcher + EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, log_multi, PatternMatcher, ErrorIgnoringTextIOWrapper from .logger import create_logger, setup_logging logger = create_logger() from .compress import Compressor, COMPR_BUFFER @@ -1592,8 +1592,8 @@ def setup_signal_handlers(): # pragma: no cover def main(): # pragma: no cover # Make sure stdout and stderr have errors='replace') to avoid unicode # issues when print()-ing unicode file names - sys.stdout = io.TextIOWrapper(sys.stdout.buffer, sys.stdout.encoding, 'replace', line_buffering=True) - sys.stderr = io.TextIOWrapper(sys.stderr.buffer, sys.stderr.encoding, 'replace', line_buffering=True) + sys.stdout = ErrorIgnoringTextIOWrapper(sys.stdout.buffer, sys.stdout.encoding, 'replace', line_buffering=True) + sys.stderr = ErrorIgnoringTextIOWrapper(sys.stderr.buffer, sys.stderr.encoding, 'replace', line_buffering=True) setup_signal_handlers() archiver = Archiver() msg = None diff --git a/borg/helpers.py b/borg/helpers.py index 56e336e0..1a7bf8af 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -12,6 +12,8 @@ import sys import platform import time import unicodedata +import io +import errno import logging from .logger import create_logger @@ -1089,3 +1091,27 @@ def log_multi(*msgs, level=logging.INFO): lines.extend(msg.splitlines()) for line in lines: logger.log(level, line) + + +class ErrorIgnoringTextIOWrapper(io.TextIOWrapper): + def read(self, n): + if not self.closed: + try: + return super().read(n) + except BrokenPipeError: + try: + super().close() + except OSError: + pass + return '' + + def write(self, s): + if not self.closed: + try: + return super().write(s) + except BrokenPipeError: + try: + super().close() + except OSError: + pass + return len(s) From dec671d8ff63878c0977b229338f4d959ed0e0bd Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 26 Jul 2016 22:39:45 +0200 Subject: [PATCH 0015/1387] SaveFile: os.replace instead of rename --- src/borg/platform/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/platform/base.py b/src/borg/platform/base.py index 9580f06b..05739062 100644 --- a/src/borg/platform/base.py +++ b/src/borg/platform/base.py @@ -148,7 +148,7 @@ class SaveFile: if exc_type is not None: os.unlink(self.tmppath) return - os.rename(self.tmppath, self.path) + os.replace(self.tmppath, self.path) platform.sync_dir(os.path.dirname(self.path)) From 863ab66908a41ad121021f317d229898a509060c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 26 Jul 2016 22:40:23 +0200 Subject: [PATCH 0016/1387] SaveFile: unlink(tmppath): only ignore FileNotFoundError --- src/borg/platform/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/platform/base.py b/src/borg/platform/base.py index 05739062..2e894289 100644 --- a/src/borg/platform/base.py +++ b/src/borg/platform/base.py @@ -137,7 +137,7 @@ class SaveFile: from .. import platform try: os.unlink(self.tmppath) - except OSError: + except FileNotFoundError: pass self.fd = platform.SyncFile(self.tmppath, self.binary) return self.fd From 2e3fc9ddfc55e3c1350e890e9d1ba3c672a59bdc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 26 Jul 2016 22:49:25 +0200 Subject: [PATCH 0017/1387] SyncFile/SaveFile: default binary=False, just like open() --- src/borg/cache.py | 8 ++++---- src/borg/key.py | 2 +- src/borg/platform/base.py | 10 ++++------ src/borg/platform/linux.pyx | 2 +- src/borg/repository.py | 4 ++-- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 6dcd88d2..473f156b 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -142,11 +142,11 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" config.set('cache', 'version', '1') config.set('cache', 'repository', self.repository.id_str) config.set('cache', 'manifest', '') - with SaveFile(os.path.join(self.path, 'config'), binary=False) as fd: + with SaveFile(os.path.join(self.path, 'config')) as fd: config.write(fd) ChunkIndex().write(os.path.join(self.path, 'chunks').encode('utf-8')) os.makedirs(os.path.join(self.path, 'chunks.archive.d')) - with SaveFile(os.path.join(self.path, 'files')) as fd: + with SaveFile(os.path.join(self.path, 'files'), binary=True) as fd: pass # empty file def _do_open(self): @@ -213,7 +213,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if not self.txn_active: return if self.files is not None: - with SaveFile(os.path.join(self.path, 'files')) as fd: + with SaveFile(os.path.join(self.path, 'files'), binary=True) as fd: for path_hash, item in self.files.items(): # Discard cached files with the newest mtime to avoid # issues with filesystem snapshots and mtime precision @@ -224,7 +224,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" self.config.set('cache', 'timestamp', self.manifest.timestamp) self.config.set('cache', 'key_type', str(self.key.TYPE)) self.config.set('cache', 'previous_location', self.repository._location.canonical_path()) - with SaveFile(os.path.join(self.path, 'config'), binary=False) as fd: + with SaveFile(os.path.join(self.path, 'config')) as fd: self.config.write(fd) self.chunks.write(os.path.join(self.path, 'chunks').encode('utf-8')) os.rename(os.path.join(self.path, 'txn.active'), diff --git a/src/borg/key.py b/src/borg/key.py index 2fd4ad3f..b122b638 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -471,7 +471,7 @@ class KeyfileKey(KeyfileKeyBase): def save(self, target, passphrase): key_data = self._save(passphrase) - with SaveFile(target, binary=False) as fd: + with SaveFile(target) as fd: fd.write('%s %s\n' % (self.FILE_ID, bin_to_hex(self.repository_id))) fd.write(key_data) fd.write('\n') diff --git a/src/borg/platform/base.py b/src/borg/platform/base.py index 2e894289..da8d3bc0 100644 --- a/src/borg/platform/base.py +++ b/src/borg/platform/base.py @@ -80,10 +80,8 @@ class SyncFile: TODO: A Windows implementation should use CreateFile with FILE_FLAG_WRITE_THROUGH. """ - def __init__(self, path, binary=True): - mode = 'x' - if binary: - mode += 'b' + def __init__(self, path, binary=False): + mode = 'xb' if binary else 'x' self.fd = open(path, mode) self.fileno = self.fd.fileno() @@ -122,13 +120,13 @@ class SaveFile: Must be used as a context manager (defining the scope of the transaction). On a journaling file system the file contents are always updated - atomically and won't become corrupted, even on pure failures or + atomically and won't become corrupted, even on power failures or crashes (for caveats see SyncFile). """ SUFFIX = '.tmp' - def __init__(self, path, binary=True): + def __init__(self, path, binary=False): self.binary = binary self.path = path self.tmppath = self.path + self.SUFFIX diff --git a/src/borg/platform/linux.pyx b/src/borg/platform/linux.pyx index 7dea321a..d35b28ac 100644 --- a/src/borg/platform/linux.pyx +++ b/src/borg/platform/linux.pyx @@ -228,7 +228,7 @@ class SyncFile(BaseSyncFile): disk in the immediate future. """ - def __init__(self, path, binary=True): + def __init__(self, path, binary=False): super().__init__(path, binary) self.offset = 0 self.write_window = (16 * 1024 ** 2) & ~PAGE_MASK diff --git a/src/borg/repository.py b/src/borg/repository.py index 9d9dc3fb..ab65dfea 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -160,7 +160,7 @@ class Repository: def save_config(self, path, config): config_path = os.path.join(path, 'config') - with SaveFile(config_path, binary=False) as fd: + with SaveFile(config_path) as fd: config.write(fd) def save_key(self, keydata): @@ -731,7 +731,7 @@ class LoggedIO: if not os.path.exists(dirname): os.mkdir(dirname) sync_dir(os.path.join(self.path, 'data')) - self._write_fd = SyncFile(self.segment_filename(self.segment)) + self._write_fd = SyncFile(self.segment_filename(self.segment), binary=True) self._write_fd.write(MAGIC) self.offset = MAGIC_LEN return self._write_fd From 88a4989c57c6e7a815e4c072032d127b23bb4465 Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Fri, 22 Jul 2016 13:58:53 -0400 Subject: [PATCH 0018/1387] Add --append-only to borg init --- borg/archiver.py | 9 +++++++-- borg/remote.py | 26 ++++++++++++++++++++------ borg/repository.py | 2 +- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 74c1d1a4..d91bdd3e 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -64,13 +64,16 @@ def with_repository(fake=False, create=False, lock=True, exclusive=False, manife @functools.wraps(method) def wrapper(self, args, **kwargs): location = args.location # note: 'location' must be always present in args + append_only = getattr(args, 'append_only', False) if argument(args, fake): return method(self, args, repository=None, **kwargs) elif location.proto == 'ssh': - repository = RemoteRepository(location, create=create, lock_wait=self.lock_wait, lock=lock, args=args) + repository = RemoteRepository(location, create=create, lock_wait=self.lock_wait, lock=lock, + append_only=append_only, args=args) else: repository = Repository(location.path, create=create, exclusive=argument(args, exclusive), - lock_wait=self.lock_wait, lock=lock) + lock_wait=self.lock_wait, lock=lock, + append_only=append_only) with repository: if manifest or cache: kwargs['manifest'], kwargs['key'] = Manifest.load(repository) @@ -947,6 +950,8 @@ class Archiver: subparser.add_argument('-e', '--encryption', dest='encryption', choices=('none', 'keyfile', 'repokey'), default='repokey', help='select encryption key mode (default: "%(default)s")') + subparser.add_argument('-a', '--append-only', dest='append_only', action='store_true', + help='create an append-only mode repository') check_epilog = textwrap.dedent(""" The check command verifies the consistency of a repository and the corresponding archives. diff --git a/borg/remote.py b/borg/remote.py index a8e64c12..a3a0afe7 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -113,7 +113,7 @@ class RepositoryServer: # pragma: no cover def negotiate(self, versions): return RPC_PROTOCOL_VERSION - def open(self, path, create=False, lock_wait=None, lock=True): + def open(self, path, create=False, lock_wait=None, lock=True, append_only=False): path = os.fsdecode(path) if path.startswith('/~'): path = path[1:] @@ -124,7 +124,7 @@ class RepositoryServer: # pragma: no cover break else: raise PathNotAllowed(path) - self.repository = Repository(path, create, lock_wait=lock_wait, lock=lock, append_only=self.append_only) + self.repository = Repository(path, create, lock_wait=lock_wait, lock=lock, append_only=self.append_only or append_only) self.repository.__enter__() # clean exit handled by serve() method return self.repository.id @@ -133,10 +133,14 @@ class RemoteRepository: extra_test_args = [] class RPCError(Exception): - def __init__(self, name): + def __init__(self, name, remote_type): self.name = name + self.remote_type = remote_type - def __init__(self, location, create=False, lock_wait=None, lock=True, args=None): + class NoAppendOnlyOnServer(Error): + """Server does not support --append-only.""" + + def __init__(self, location, create=False, lock_wait=None, lock=True, append_only=False, args=None): self.location = self._location = location self.preload_ids = [] self.msgid = 0 @@ -172,7 +176,17 @@ class RemoteRepository: if version != RPC_PROTOCOL_VERSION: raise Exception('Server insisted on using unsupported protocol version %d' % version) try: - self.id = self.call('open', self.location.path, create, lock_wait, lock) + # Because of protocol versions, only send append_only if necessary + if append_only: + try: + self.id = self.call('open', self.location.path, create, lock_wait, lock, append_only) + except self.RPCError as err: + if err.remote_type == 'TypeError': + raise self.NoAppendOnlyOnServer() from err + else: + raise + else: + self.id = self.call('open', self.location.path, create, lock_wait, lock) except Exception: self.close() raise @@ -264,7 +278,7 @@ class RemoteRepository: elif error == b'InvalidRPCMethod': raise InvalidRPCMethod(*res) else: - raise self.RPCError(res.decode('utf-8')) + raise self.RPCError(res.decode('utf-8'), error.decode('utf-8')) calls = list(calls) waiting_for = [] diff --git a/borg/repository.py b/borg/repository.py index 87666a57..262467dc 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -108,7 +108,7 @@ class Repository: config.set('repository', 'version', '1') config.set('repository', 'segments_per_dir', str(self.DEFAULT_SEGMENTS_PER_DIR)) config.set('repository', 'max_segment_size', str(self.DEFAULT_MAX_SEGMENT_SIZE)) - config.set('repository', 'append_only', '0') + config.set('repository', 'append_only', str(int(self.append_only))) config.set('repository', 'id', hexlify(os.urandom(32)).decode('ascii')) self.save_config(path, config) From 0a4a95eb1767a1dad84b7e29d4e951efa4e63d2c Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Wed, 27 Jul 2016 09:12:52 -0400 Subject: [PATCH 0019/1387] Remove trailing whitespace --- README.rst | 8 ++--- docs/changes.rst | 6 ++-- docs/installation.rst | 2 +- docs/internals.rst | 2 +- docs/misc/internals-picture.txt | 26 +++++++------- docs/misc/prune-example.txt | 64 ++++++++++++++++----------------- tox.ini | 2 +- 7 files changed, 55 insertions(+), 55 deletions(-) diff --git a/README.rst b/README.rst index 035a38d9..f6132773 100644 --- a/README.rst +++ b/README.rst @@ -33,14 +33,14 @@ Main features Compared to other deduplication approaches, this method does NOT depend on: - * file/directory names staying the same: So you can move your stuff around + * file/directory names staying the same: So you can move your stuff around without killing the deduplication, even between machines sharing a repo. - * complete files or time stamps staying the same: If a big file changes a - little, only a few new chunks need to be stored - this is great for VMs or + * complete files or time stamps staying the same: If a big file changes a + little, only a few new chunks need to be stored - this is great for VMs or raw disks. - * The absolute position of a data chunk inside a file: Stuff may get shifted + * The absolute position of a data chunk inside a file: Stuff may get shifted and will still be found by the deduplication algorithm. **Speed** diff --git a/docs/changes.rst b/docs/changes.rst index 0696da51..7d9b47ab 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -383,7 +383,7 @@ Compatibility notes: - disambiguate -p option, #563: - -p now is same as --progress - - -P now is same as --prefix + - -P now is same as --prefix - remove deprecated "borg verify", use "borg extract --dry-run" instead - cleanup environment variable semantics, #355 @@ -448,7 +448,7 @@ New features: - format options for location: user, pid, fqdn, hostname, now, utcnow, user - borg list --list-format - borg prune -v --list enables the keep/prune list output, #658 - + Bug fixes: - fix _open_rb noatime handling, #657 @@ -466,7 +466,7 @@ Other changes: - Vagrant: drop Ubuntu Precise (12.04) - does not have Python >= 3.4 - Vagrant: use pyinstaller v3.1.1 to build binaries - docs: - + - borg upgrade: add to docs that only LOCAL repos are supported - borg upgrade also handles borg 0.xx -> 1.0 - use pip extras or requirements file to install llfuse diff --git a/docs/installation.rst b/docs/installation.rst index b39bb0af..fec03b55 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -181,7 +181,7 @@ and commands to make fuse work for using the mount command. echo 'vfs.usermount=1' >> /etc/sysctl.conf kldload fuse sysctl vfs.usermount=1 - + Cygwin ++++++ diff --git a/docs/internals.rst b/docs/internals.rst index 61d84589..b088f68e 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -371,7 +371,7 @@ repository_id enc_key the key used to encrypt data with AES (256 bits) - + enc_hmac_key the key used to HMAC the encrypted data (256 bits) diff --git a/docs/misc/internals-picture.txt b/docs/misc/internals-picture.txt index ae76f0c1..01351a7b 100644 --- a/docs/misc/internals-picture.txt +++ b/docs/misc/internals-picture.txt @@ -11,22 +11,22 @@ BorgBackup from 10.000m | | | +------+-------+ | | | | | - /chunk\/chunk\/chunk\... /maybe different chunks lists\ + /chunk\/chunk\/chunk\... /maybe different chunks lists\ +-----------------------------------------------------------------+ |item list | +-----------------------------------------------------------------+ - | - +-------------------------------------+--------------+ - | | | - | | | -+-------------+ +-------------+ | -|item0 | |item1 | | -| - owner | | - owner | | -| - size | | - size | ... -| - ... | | - ... | -| - chunks | | - chunks | -+----+--------+ +-----+-------+ - | | + | + +-------------------------------------+--------------+ + | | | + | | | ++-------------+ +-------------+ | +|item0 | |item1 | | +| - owner | | - owner | | +| - size | | - size | ... +| - ... | | - ... | +| - chunks | | - chunks | ++----+--------+ +-----+-------+ + | | | +-----+----------------------------+-----------------+ | | | | +-o-----o------------+ | diff --git a/docs/misc/prune-example.txt b/docs/misc/prune-example.txt index 6c8f8e55..ac608b6a 100644 --- a/docs/misc/prune-example.txt +++ b/docs/misc/prune-example.txt @@ -14,41 +14,41 @@ Calendar view ------------- 2015 - January February March -Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su - 1 2 3 4 1 1 - 5 6 7 8 9 10 11 2 3 4 5 6 7 8 2 3 4 5 6 7 8 -12 13 14 15 16 17 18 9 10 11 12 13 14 15 9 10 11 12 13 14 15 -19 20 21 22 23 24 25 16 17 18 19 20 21 22 16 17 18 19 20 21 22 -26 27 28 29 30 31 23 24 25 26 27 28 23 24 25 26 27 28 29 - 30 31 + January February March +Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su + 1 2 3 4 1 1 + 5 6 7 8 9 10 11 2 3 4 5 6 7 8 2 3 4 5 6 7 8 +12 13 14 15 16 17 18 9 10 11 12 13 14 15 9 10 11 12 13 14 15 +19 20 21 22 23 24 25 16 17 18 19 20 21 22 16 17 18 19 20 21 22 +26 27 28 29 30 31 23 24 25 26 27 28 23 24 25 26 27 28 29 + 30 31 - April May June -Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su - 1 2 3 4 5 1 2 3 1 2 3 4 5 6 7 - 6 7 8 9 10 11 12 4 5 6 7 8 9 10 8 9 10 11 12 13 14 -13 14 15 16 17 18 19 11 12 13 14 15 16 17 15 16 17 18 19 20 21 -20 21 22 23 24 25 26 18 19 20 21 22 23 24 22 23 24 25 26 27 28 -27 28 29 30 25 26 27 28 29 30 31 29 30m - + April May June +Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su + 1 2 3 4 5 1 2 3 1 2 3 4 5 6 7 + 6 7 8 9 10 11 12 4 5 6 7 8 9 10 8 9 10 11 12 13 14 +13 14 15 16 17 18 19 11 12 13 14 15 16 17 15 16 17 18 19 20 21 +20 21 22 23 24 25 26 18 19 20 21 22 23 24 22 23 24 25 26 27 28 +27 28 29 30 25 26 27 28 29 30 31 29 30m - July August September -Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su - 1 2 3 4 5 1 2 1 2 3 4 5 6 - 6 7 8 9 10 11 12 3 4 5 6 7 8 9 7 8 9 10 11 12 13 -13 14 15 16 17 18 19 10 11 12 13 14 15 16 14 15 16 17 18 19 20 -20 21 22 23 24 25 26 17 18 19 20 21 22 23 21 22 23 24 25 26 27 -27 28 29 30 31m 24 25 26 27 28 29 30 28 29 30m - 31m - October November December -Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su - 1 2 3 4 1 1 2 3 4 5 6 - 5 6 7 8 9 10 11 2 3 4 5 6 7 8 7 8 9 10 11 12 13 -12 13 14 15 16 17 18 9 10 11 12 13 14 15 14 15 16 17d18d19d20 -19 20 21 22 23 24 25 16 17 18 19 20 21 22 21d22d23d24d25d26d27d -26 27 28 29 30 31m 23 24 25 26 27 28 29 28d29d30d31d - 30m + July August September +Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su + 1 2 3 4 5 1 2 1 2 3 4 5 6 + 6 7 8 9 10 11 12 3 4 5 6 7 8 9 7 8 9 10 11 12 13 +13 14 15 16 17 18 19 10 11 12 13 14 15 16 14 15 16 17 18 19 20 +20 21 22 23 24 25 26 17 18 19 20 21 22 23 21 22 23 24 25 26 27 +27 28 29 30 31m 24 25 26 27 28 29 30 28 29 30m + 31m + + October November December +Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su + 1 2 3 4 1 1 2 3 4 5 6 + 5 6 7 8 9 10 11 2 3 4 5 6 7 8 7 8 9 10 11 12 13 +12 13 14 15 16 17 18 9 10 11 12 13 14 15 14 15 16 17d18d19d20 +19 20 21 22 23 24 25 16 17 18 19 20 21 22 21d22d23d24d25d26d27d +26 27 28 29 30 31m 23 24 25 26 27 28 29 28d29d30d31d + 30m List view --------- diff --git a/tox.ini b/tox.ini index c7b49bca..bc6195d9 100644 --- a/tox.ini +++ b/tox.ini @@ -18,4 +18,4 @@ passenv = * [testenv:flake8] changedir = deps = flake8 -commands = flake8 +commands = flake8 From c5fffbefff28858fe5b44ca034f02c5ac627be93 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 27 Jul 2016 23:54:15 +0200 Subject: [PATCH 0020/1387] Platform feature matrix --- docs/development.rst | 10 +++++++ docs/faq.rst | 19 ------------ docs/installation.rst | 70 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 19 deletions(-) diff --git a/docs/development.rst b/docs/development.rst index 2dd47e70..05da98b4 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -19,6 +19,16 @@ instead of 79. We do *not* use form-feed (``^L``) characters to separate sections either. Compliance is tested automatically when you run the tests. +Continuous Integration +---------------------- + +All pull requests go through Travis-CI_, which runs the tests on Linux +and Mac OS X as well as the flake8 style checker. Additional Unix-like platforms +are tested on Golem_. + +.. _Golem: https://golem.enkore.de/view/Borg/ +.. _Travis-CI: https://travis-ci.org/borgbackup/borg + Output and Logging ------------------ When writing logger calls, always use correct log level (debug only for diff --git a/docs/faq.rst b/docs/faq.rst index eaeef714..0ea3f079 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -47,25 +47,6 @@ If you want to have 2 independent backups, it is better to do it like this: - client machine ---borg create---> repo1 - client machine ---borg create---> repo2 -Which file types, attributes, etc. are preserved? -------------------------------------------------- - - * Directories - * Regular files - * Hardlinks (considering all files in the same archive) - * Symlinks (stored as symlink, the symlink is not followed) - * Character and block device files - * FIFOs ("named pipes") - * Name - * Contents - * Timestamps in nanosecond precision: mtime, atime, ctime - * IDs of owning user and owning group - * Names of owning user and owning group (if the IDs can be resolved) - * Unix Mode/Permissions (u/g/o permissions, suid, sgid, sticky) - * Extended Attributes (xattrs) on Linux, OS X and FreeBSD - * Access Control Lists (ACL_) on Linux, OS X and FreeBSD - * BSD flags on OS X and FreeBSD - Which file types, attributes, etc. are *not* preserved? ------------------------------------------------------- diff --git a/docs/installation.rst b/docs/installation.rst index b39bb0af..ab57b7c8 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -89,6 +89,76 @@ the old version using the same steps as shown above. .. _pyinstaller: http://www.pyinstaller.org .. _releases: https://github.com/borgbackup/borg/releases +.. _platforms: + +Features & platforms +-------------------- + +Besides regular file and directory structures, |project_name| can preserve + + * Hardlinks (considering all files in the same archive) + * Symlinks (stored as symlink, the symlink is not followed) + * Special files: + + * Character and block device files (restored via mknod) + * FIFOs ("named pipes") + * Special file *contents* can be backed up in ``--read-special`` mode. + By default the metadata to create them with mknod(2), mkfifo(2) etc. is stored. + * Timestamps in nanosecond precision: mtime, atime, ctime + * Permissions: + + * IDs of owning user and owning group + * Names of owning user and owning group (if the IDs can be resolved) + * Unix Mode/Permissions (u/g/o permissions, suid, sgid, sticky) + +On some platforms additional features are supported: + +.. Yes/No's are grouped by reason/mechanism/reference. + ++------------------+----------+-----------+------------+ +| Platform | ACLs | xattr | Flags | +| | [#acls]_ | [#xattr]_ | [#flags]_ | ++==================+==========+===========+============+ +| Linux x86 | Yes | Yes | No | ++------------------+ | | | +| Linux PowerPC | | | | ++------------------+ | | | +| Linux ARM | | | | ++------------------+----------+-----------+------------+ +| Mac OS X | Yes | Yes | Yes (all) | ++------------------+----------+-----------+ | +| FreeBSD | Yes | Yes | | ++------------------+----------+-----------+ | +| OpenBSD | n/a | n/a | | ++------------------+----------+-----------+ | +| NetBSD | n/a | No [2]_ | | ++------------------+----------+-----------+------------+ +| Solaris 11 | No [3]_ | n/a | ++------------------+ | | +| OpenIndiana | | | ++------------------+----------+-----------+------------+ +| Windows (cygwin) | No [4]_ | No | No | ++------------------+----------+-----------+------------+ + +Some Distributions (e.g. Debian) run additional tests after each release, these +are not reflected here. + +Other Unix-like operating systems may work as well, but have not been tested at all. + +Note that most of the platform-dependent features also depend on the file system. +For example, ntfs-3g on Linux isn't able to convey NTFS ACLs. + + +.. [2] Feature request :issue:`1332` +.. [3] Feature request :issue:`1337` +.. [4] Cygwin tries to map NTFS ACLs to permissions with varying degress of success. + +.. [#acls] The native access control list mechanism of the OS. This normally limits access to + non-native ACLs. For example, NTFS ACLs aren't completely accessible on Linux with ntfs-3g. +.. [#xattr] extended attributes; key-value pairs attached to a file, mainly used by the OS. + This includes resource forks on Mac OS X. +.. [#flags] aka *BSD flags*. + .. _source-install: From Source From b652f4039cbcaee80744dfe6946a0456d2adf8ac Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Thu, 28 Jul 2016 10:35:41 -0400 Subject: [PATCH 0021/1387] Remove trailing whitespace --- docs/changes.rst | 2 +- docs/deployment.rst | 2 +- src/borg/platform/posix.pyx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 8530a55b..ec270694 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -57,7 +57,7 @@ New features: - borg check: will not produce the "Checking segments" output unless new --progress option is passed, #824. -- options that imply output (--show-rc, --show-version, --list, --stats, +- options that imply output (--show-rc, --show-version, --list, --stats, --progress) don't need -v/--info to have that output displayed, #865 - borg recreate: re-create existing archives, #787 #686 #630 #70, also see #757, #770. diff --git a/docs/deployment.rst b/docs/deployment.rst index a29d09cd..1ad72016 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -157,7 +157,7 @@ Salt running on a Debian system. :: Install borg backup from pip: - pkg.installed: + pkg.installed: - pkgs: - python3 - python3-dev diff --git a/src/borg/platform/posix.pyx b/src/borg/platform/posix.pyx index 8d74f19e..c9726ea1 100644 --- a/src/borg/platform/posix.pyx +++ b/src/borg/platform/posix.pyx @@ -1,6 +1,6 @@ cdef extern from "wchar.h": cdef int wcswidth(const Py_UNICODE *str, size_t n) - + def swidth(s): str_len = len(s) terminal_width = wcswidth(s, str_len) From 1b6b0cfae608e7b2e2711a3d66ff7e18fabaf0f9 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 28 Jul 2016 18:40:20 +0200 Subject: [PATCH 0022/1387] Fix borg-check --verify-data tripping over ObjectNotFounds --- src/borg/archive.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index e4565515..f19ba979 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -954,20 +954,25 @@ class ArchiveChecker: def verify_data(self): logger.info('Starting cryptographic data integrity verification...') - pi = ProgressIndicatorPercent(total=len(self.chunks), msg="Verifying data %6.2f%%", step=0.01, same_line=True) - count = errors = 0 + count = len(self.chunks) + errors = 0 + pi = ProgressIndicatorPercent(total=count, msg="Verifying data %6.2f%%", step=0.01, same_line=True) for chunk_id, (refcount, *_) in self.chunks.iteritems(): pi.show() - if not refcount: - continue - encrypted_data = self.repository.get(chunk_id) try: - _, data = self.key.decrypt(chunk_id, encrypted_data) + encrypted_data = self.repository.get(chunk_id) + except Repository.ObjectNotFound: + self.error_found = True + errors += 1 + logger.error('chunk %s not found', bin_to_hex(chunk_id)) + continue + try: + _chunk_id = None if chunk_id == Manifest.MANIFEST_ID else chunk_id + _, data = self.key.decrypt(_chunk_id, encrypted_data) except IntegrityError as integrity_error: self.error_found = True errors += 1 logger.error('chunk %s, integrity error: %s', bin_to_hex(chunk_id), integrity_error) - count += 1 pi.finish() log = logger.error if errors else logger.info log('Finished cryptographic data integrity verification, verified %d chunks with %d integrity errors.', count, errors) From d0ec7e76bb36ffd4fce005c144c2f2c4c4080e3b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 28 Jul 2016 18:41:08 +0200 Subject: [PATCH 0023/1387] Fix borg-check --verify-data failing with rebuilt objects There are some instances where --repair would do something. In these instances --verify-data would fail if --repair was not given also, since the changes without --repair are only in the index, but not in the repository. --- src/borg/archive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index f19ba979..0efb1a6a 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -909,6 +909,8 @@ class ArchiveChecker: self.repository = repository self.init_chunks() self.key = self.identify_key(repository) + if verify_data: + self.verify_data() if Manifest.MANIFEST_ID not in self.chunks: logger.error("Repository manifest not found!") self.error_found = True @@ -916,8 +918,6 @@ class ArchiveChecker: else: self.manifest, _ = Manifest.load(repository, key=self.key) self.rebuild_refcounts(archive=archive, last=last, prefix=prefix) - if verify_data: - self.verify_data() self.orphan_chunks_check() self.finish(save_space=save_space) if self.error_found: From 0ae48dafbb5ea043317fe5c19c86f15f83e38d35 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 28 Jul 2016 18:41:19 +0200 Subject: [PATCH 0024/1387] ObjectNotFound: give ID as hex-string --- src/borg/repository.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/borg/repository.py b/src/borg/repository.py index 00a9c4e1..c83edcec 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -96,6 +96,11 @@ class Repository: class ObjectNotFound(ErrorWithTraceback): """Object with key {} not found in repository {}.""" + def __init__(self, id, repo): + if isinstance(id, bytes): + id = bin_to_hex(id) + super().__init__(id, repo) + def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True, append_only=False): self.path = os.path.abspath(path) self._location = Location('file://%s' % self.path) From 9226fc6f6f09822f5dee210737aa82629701cc11 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Jun 2016 16:59:38 +0200 Subject: [PATCH 0025/1387] split stat_attrs into cheap and expensive part we already have stat results in st, so computing stat_simple_attrs is rather cheap (except the username/groupname lookup maybe) and gets the most important stuff right in the Item, so it is brought early into a good state. after chunking, stat_ext_attrs is called to add the more expensive-to-get attributes, like bsdflags, xattrs and ACLs. --- src/borg/archive.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 0efb1a6a..69f84c7d 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -651,17 +651,24 @@ Number of files: {0.stats.nfiles}'''.format( logger.warning('forced deletion succeeded, but the deleted archive was corrupted.') logger.warning('borg check --repair is required to free all space.') - def stat_attrs(self, st, path): + def stat_simple_attrs(self, st): attrs = dict( mode=st.st_mode, - uid=st.st_uid, user=uid2user(st.st_uid), - gid=st.st_gid, group=gid2group(st.st_gid), + uid=st.st_uid, + gid=st.st_gid, atime=st.st_atime_ns, ctime=st.st_ctime_ns, mtime=st.st_mtime_ns, ) if self.numeric_owner: attrs['user'] = attrs['group'] = None + else: + attrs['user'] = uid2user(st.st_uid) + attrs['group'] = gid2group(st.st_gid) + return attrs + + def stat_ext_attrs(self, st, path): + attrs = {} with backup_io(): xattrs = xattr.get_all(path, follow_symlinks=False) bsdflags = get_flags(path, st) @@ -672,6 +679,11 @@ Number of files: {0.stats.nfiles}'''.format( attrs['bsdflags'] = bsdflags return attrs + def stat_attrs(self, st, path): + attrs = self.stat_simple_attrs(st) + attrs.update(self.stat_ext_attrs(st, path)) + return attrs + def process_dir(self, path, st): item = Item(path=make_path_safe(path)) item.update(self.stat_attrs(st, path)) @@ -760,6 +772,7 @@ Number of files: {0.stats.nfiles}'''.format( path=safe_path, hardlink_master=st.st_nlink > 1, # item is a hard link and has the chunks ) + item.update(self.stat_simple_attrs(st)) # Only chunkify the file if needed if chunks is None: compress = self.compression_decider1.decide(path) From bda50b59586f75c83dd95faf67291d69eb508874 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Jun 2016 17:14:13 +0200 Subject: [PATCH 0026/1387] stdin chunking: get Item into usable state early --- src/borg/archive.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 69f84c7d..09e7c1b8 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -714,20 +714,19 @@ Number of files: {0.stats.nfiles}'''.format( def process_stdin(self, path, cache): uid, gid = 0, 0 - fd = sys.stdin.buffer # binary - chunks = [] - for data in backup_io_iter(self.chunker.chunkify(fd)): - chunks.append(cache.add_chunk(self.key.id_hash(data), Chunk(data), self.stats)) - self.stats.nfiles += 1 t = int(time.time()) * 1000000000 item = Item( path=path, - chunks=chunks, mode=0o100660, # regular file, ug=rw uid=uid, user=uid2user(uid), gid=gid, group=gid2group(gid), mtime=t, atime=t, ctime=t, ) + fd = sys.stdin.buffer # binary + item.chunks = [] + for data in backup_io_iter(self.chunker.chunkify(fd)): + item.chunks.append(cache.add_chunk(self.key.id_hash(data), Chunk(data), self.stats)) + self.stats.nfiles += 1 self.add_item(item) return 'i' # stdin From 75d91c4bd1bec60fd43ffd1818805edc83b64b79 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Jun 2016 17:20:29 +0200 Subject: [PATCH 0027/1387] file chunking: refactor code a little so it directly works with item.chunks list instead of a temporary list. --- src/borg/archive.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 09e7c1b8..cf75053a 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -773,15 +773,17 @@ Number of files: {0.stats.nfiles}'''.format( ) item.update(self.stat_simple_attrs(st)) # Only chunkify the file if needed - if chunks is None: + if chunks is not None: + item.chunks = chunks + else: compress = self.compression_decider1.decide(path) logger.debug('%s -> compression %s', path, compress['name']) with backup_io(): fh = Archive._open_rb(path) with os.fdopen(fh, 'rb') as fd: - chunks = [] + item.chunks = [] for data in backup_io_iter(self.chunker.chunkify(fd, fh)): - chunks.append(cache.add_chunk(self.key.id_hash(data), + item.chunks.append(cache.add_chunk(self.key.id_hash(data), Chunk(data, compress=compress), self.stats)) if self.show_progress: @@ -789,9 +791,8 @@ Number of files: {0.stats.nfiles}'''.format( if not is_special_file: # we must not memorize special files, because the contents of e.g. a # block or char device will change without its mtime/size/inode changing. - cache.memorize_file(path_hash, st, [c.id for c in chunks]) + cache.memorize_file(path_hash, st, [c.id for c in item.chunks]) status = status or 'M' # regular file, modified (if not 'A' already) - item.chunks = chunks item.update(self.stat_attrs(st, path)) if is_special_file: # we processed a special file like a regular file. reflect that in mode, From dd5f957e6f4ba2feb94a6eb7e4d8e73b2e509989 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Jun 2016 18:07:01 +0200 Subject: [PATCH 0028/1387] in-file checkpoints, fixes #1198, fixes #1093 also: unify code for stdin and on-disk file processing --- src/borg/archive.py | 39 ++++++++++++++++++++++++--------------- src/borg/constants.py | 3 ++- src/borg/item.py | 2 ++ 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index cf75053a..87190a05 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -331,13 +331,10 @@ Number of files: {0.stats.nfiles}'''.format( for item in self.pipeline.unpack_many(self.metadata[b'items'], filter=filter, preload=preload): yield item - def add_item(self, item): - if self.show_progress: + def add_item(self, item, show_progress=True): + if show_progress and self.show_progress: self.stats.show_progress(item=item, dt=0.2) self.items_buffer.add(item) - if self.checkpoint_interval and time.time() - self.last_checkpoint > self.checkpoint_interval: - self.write_checkpoint() - self.last_checkpoint = time.time() def write_checkpoint(self): self.save(self.checkpoint_name) @@ -712,6 +709,26 @@ Number of files: {0.stats.nfiles}'''.format( self.add_item(item) return 's' # symlink + def chunk_file(self, item, cache, stats, fd, fh=-1, **chunk_kw): + checkpoint_number = 1 + item.chunks = [] + for data in backup_io_iter(self.chunker.chunkify(fd, fh)): + item.chunks.append(cache.add_chunk(self.key.id_hash(data), Chunk(data, **chunk_kw), stats)) + if self.show_progress: + self.stats.show_progress(item=item, dt=0.2) + if self.checkpoint_interval and time.time() - self.last_checkpoint > self.checkpoint_interval: + checkpoint_item = Item(internal_dict=item.as_dict()) + checkpoint_item.path += '.checkpoint_%d' % checkpoint_number + checkpoint_item.checkpoint = checkpoint_number + checkpoint_number += 1 + self.add_item(checkpoint_item, show_progress=False) + self.write_checkpoint() + self.last_checkpoint = time.time() + # we have saved the checkpoint file, but we will reference the same + # chunks also from the checkpoint or the "real" file we save next + for chunk in checkpoint_item.chunks: + cache.chunk_incref(chunk.id, stats) + def process_stdin(self, path, cache): uid, gid = 0, 0 t = int(time.time()) * 1000000000 @@ -723,9 +740,7 @@ Number of files: {0.stats.nfiles}'''.format( mtime=t, atime=t, ctime=t, ) fd = sys.stdin.buffer # binary - item.chunks = [] - for data in backup_io_iter(self.chunker.chunkify(fd)): - item.chunks.append(cache.add_chunk(self.key.id_hash(data), Chunk(data), self.stats)) + self.chunk_file(item, cache, self.stats, fd) self.stats.nfiles += 1 self.add_item(item) return 'i' # stdin @@ -781,13 +796,7 @@ Number of files: {0.stats.nfiles}'''.format( with backup_io(): fh = Archive._open_rb(path) with os.fdopen(fh, 'rb') as fd: - item.chunks = [] - for data in backup_io_iter(self.chunker.chunkify(fd, fh)): - item.chunks.append(cache.add_chunk(self.key.id_hash(data), - Chunk(data, compress=compress), - self.stats)) - if self.show_progress: - self.stats.show_progress(item=item, dt=0.2) + self.chunk_file(item, cache, self.stats, fd, fh, compress=compress) if not is_special_file: # we must not memorize special files, because the contents of e.g. a # block or char device will change without its mtime/size/inode changing. diff --git a/src/borg/constants.py b/src/borg/constants.py index 31f717f4..6b5c67c6 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -1,7 +1,8 @@ # this set must be kept complete, otherwise the RobustUnpacker might malfunction: ITEM_KEYS = frozenset(['path', 'source', 'rdev', 'chunks', 'chunks_healthy', 'hardlink_master', 'mode', 'user', 'group', 'uid', 'gid', 'mtime', 'atime', 'ctime', - 'xattrs', 'bsdflags', 'acl_nfs4', 'acl_access', 'acl_default', 'acl_extended', ]) + 'xattrs', 'bsdflags', 'acl_nfs4', 'acl_access', 'acl_default', 'acl_extended', + 'checkpoint']) # this is the set of keys that are always present in items: REQUIRED_ITEM_KEYS = frozenset(['path', 'mtime', ]) diff --git a/src/borg/item.py b/src/borg/item.py index 88b87eb5..52e72448 100644 --- a/src/borg/item.py +++ b/src/borg/item.py @@ -155,6 +155,8 @@ class Item(PropDict): deleted = PropDict._make_property('deleted', bool) nlink = PropDict._make_property('nlink', int) + checkpoint = PropDict._make_property('checkpoint', int) + class EncryptedKey(PropDict): """ From 49233be25d6b38438981b75658a4844a9b935dc4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 27 Jun 2016 00:25:05 +0200 Subject: [PATCH 0029/1387] filter out checkpoint files do not: - list them - extract them - diff them - include them for recreate --- src/borg/archive.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 87190a05..7dbe6c97 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -327,8 +327,12 @@ Number of files: {0.stats.nfiles}'''.format( def __repr__(self): return 'Archive(%r)' % self.name + def item_filter(self, item, filter=None): + return 'checkpoint' not in item and (filter(item) if filter else True) + def iter_items(self, filter=None, preload=False): - for item in self.pipeline.unpack_many(self.metadata[b'items'], filter=filter, preload=preload): + for item in self.pipeline.unpack_many(self.metadata[b'items'], preload=preload, + filter=lambda item: self.item_filter(item, filter)): yield item def add_item(self, item, show_progress=True): From 0ea6745250e7560f585a5e07aeb7f82138983c8b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 21 Jul 2016 21:11:47 +0200 Subject: [PATCH 0030/1387] update faq: we have in-file checkpoints now note: until there is some means to list and extract the (partial) checkpoint items, we do not need to talk about them. --- docs/faq.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index f3cccc2a..f36aaa92 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -225,10 +225,7 @@ During a backup a special checkpoint archive named ``.checkpoint`` is saved every checkpoint interval (the default value for this is 30 minutes) containing all the data backed-up until that point. -Checkpoints only happen between files (so they don't help for interruptions -happening while a very large file is being processed). - -This checkpoint archive is a valid archive (all files in it are valid and complete), +This checkpoint archive is a valid archive, but it is only a partial backup (not all files that you wanted to backup are contained in it). Having it in the repo until a successful, full backup is completed is useful because it references all the transmitted chunks up From e5bd6cef205ac477061f59618cc8e03c0705126b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 21 Jul 2016 22:24:48 +0200 Subject: [PATCH 0031/1387] add --consider-checkpoint-files option, update FAQ --- docs/faq.rst | 28 +++++++++++----------------- src/borg/archive.py | 9 +++++++-- src/borg/archiver.py | 18 +++++++++++++----- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index f36aaa92..2c571ea1 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -246,27 +246,21 @@ Once your backup has finished successfully, you can delete all ``.checkpoint`` archives. If you run ``borg prune``, it will also care for deleting unneeded checkpoints. +Note: the checkpointing mechanism creates hidden, partial files in an archive, +so that checkpoints even work while a big file is being processed. +They are named ``.checkpoint_`` and all operations usually ignore +these files, but you can make them considered by giving the option +``--consider-checkpoint-files``. You usually only need that option if you are +really desperate (e.g. if you have no completed backup of that file and you'ld +rather get a partial file extracted than nothing). You do **not** want to give +that option under any normal circumstances. + How can I backup huge file(s) over a unstable connection? --------------------------------------------------------- -You can use this "split trick" as a workaround for the in-between-files-only -checkpoints (see above), huge files and a instable connection to the repository: +This is not a problem any more, see previous FAQ item. -Split the huge file(s) into parts of manageable size (e.g. 100MB) and create -a temporary archive of them. Borg will create checkpoints now more frequently -than if you try to backup the files in their original form (e.g. 100GB). - -After that, you can remove the parts again and backup the huge file(s) in -their original form. This will now work a lot faster as a lot of content chunks -are already in the repository. - -After you have successfully backed up the huge original file(s), you can remove -the temporary archive you made from the parts. - -We realize that this is just a better-than-nothing workaround, see :issue:`1198` -for a potential solution. - -Please note that this workaround only helps you for backup, not for restore. +But please note that this only helps you for backup, not for restore. If it crashes with a UnicodeError, what can I do? ------------------------------------------------- diff --git a/src/borg/archive.py b/src/borg/archive.py index 7dbe6c97..c1d496dd 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -231,7 +231,8 @@ class Archive: def __init__(self, repository, key, manifest, name, cache=None, create=False, checkpoint_interval=300, numeric_owner=False, progress=False, - chunker_params=CHUNKER_PARAMS, start=None, end=None, compression=None, compression_files=None): + chunker_params=CHUNKER_PARAMS, start=None, end=None, compression=None, compression_files=None, + consider_checkpoint_files=False): self.cwd = os.getcwd() self.key = key self.repository = repository @@ -250,6 +251,7 @@ class Archive: if end is None: end = datetime.utcnow() self.end = end + self.consider_checkpoint_files = consider_checkpoint_files self.pipeline = DownloadPipeline(self.repository, self.key) if create: self.items_buffer = CacheChunkBuffer(self.cache, self.key, self.stats) @@ -328,7 +330,10 @@ Number of files: {0.stats.nfiles}'''.format( return 'Archive(%r)' % self.name def item_filter(self, item, filter=None): - return 'checkpoint' not in item and (filter(item) if filter else True) + if not self.consider_checkpoint_files and 'checkpoint' in item: + # this is a checkpoint (partial) file, we usually don't want to consider it. + return False + return filter(item) if filter else True def iter_items(self, filter=None, preload=False): for item in self.pipeline.unpack_many(self.metadata[b'items'], preload=preload, diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 083eccf1..f3c78de8 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -100,7 +100,8 @@ def with_archive(method): @functools.wraps(method) def wrapper(self, args, repository, key, manifest, **kwargs): archive = Archive(repository, key, manifest, args.location.archive, - numeric_owner=getattr(args, 'numeric_owner', False), cache=kwargs.get('cache')) + numeric_owner=getattr(args, 'numeric_owner', False), cache=kwargs.get('cache'), + consider_checkpoint_files=args.consider_checkpoint_files) return method(self, args, repository=repository, manifest=manifest, key=key, archive=archive, **kwargs) return wrapper @@ -668,7 +669,8 @@ class Archiver: print_output(line) archive1 = archive - archive2 = Archive(repository, key, manifest, args.archive2) + archive2 = Archive(repository, key, manifest, args.archive2, + consider_checkpoint_files=args.consider_checkpoint_files) can_compare_chunk_ids = archive1.metadata.get(b'chunker_params', False) == archive2.metadata.get( b'chunker_params', True) or args.same_chunker_params @@ -753,7 +755,8 @@ class Archiver: with cache_if_remote(repository) as cached_repo: if args.location.archive: - archive = Archive(repository, key, manifest, args.location.archive) + archive = Archive(repository, key, manifest, args.location.archive, + consider_checkpoint_files=args.consider_checkpoint_files) else: archive = None operations = FuseOperations(key, repository, manifest, archive, cached_repo) @@ -779,7 +782,8 @@ class Archiver: if args.location.archive: matcher, _ = self.build_matcher(args.excludes, args.paths) with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: - archive = Archive(repository, key, manifest, args.location.archive, cache=cache) + archive = Archive(repository, key, manifest, args.location.archive, cache=cache, + consider_checkpoint_files=args.consider_checkpoint_files) if args.format: format = args.format @@ -981,7 +985,8 @@ class Archiver: @with_repository() def do_debug_dump_archive_items(self, args, repository, manifest, key): """dump (decrypted, decompressed) archive items metadata (not: data)""" - archive = Archive(repository, key, manifest, args.location.archive) + archive = Archive(repository, key, manifest, args.location.archive, + consider_checkpoint_files=args.consider_checkpoint_files) for i, item_id in enumerate(archive.metadata[b'items']): _, data = key.decrypt(item_id, repository.get(item_id)) filename = '%06d_%s.items' % (i, bin_to_hex(item_id)) @@ -1232,6 +1237,9 @@ class Archiver: help='set umask to M (local and remote, default: %(default)04o)') common_group.add_argument('--remote-path', dest='remote_path', metavar='PATH', help='set remote path to executable (default: "borg")') + common_group.add_argument('--consider-checkpoint-files', dest='consider_checkpoint_files', + action='store_true', default=False, + help='treat checkpoint files like normal files (e.g. to list/extract them)') parser = argparse.ArgumentParser(prog=prog, description='Borg - Deduplicated Backups') parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__, From a7fca52f073417f410e0f73d0eccc8ba318feb94 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 21 Jul 2016 23:56:58 +0200 Subject: [PATCH 0032/1387] create part items rather than checkpoint items checkpoint items: chunks abc, abcdef, abcdefghi, .... part items: chunks abc, def, ghi solves the "restore a big file over unstable connection issue" (not very comfortably, but one can just extract one part after the other and concatenate them to get the full file) --- docs/faq.rst | 6 +++++- src/borg/archive.py | 37 ++++++++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 2c571ea1..d092b758 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -260,7 +260,11 @@ How can I backup huge file(s) over a unstable connection? This is not a problem any more, see previous FAQ item. -But please note that this only helps you for backup, not for restore. +How can I restore huge file(s) over a unstable connection? +---------------------------------------------------------- + +If you can not manage to extract the whole big file in one go, you can extract +all the checkpoint files (see above) and manually concatenate them together. If it crashes with a UnicodeError, what can I do? ------------------------------------------------- diff --git a/src/borg/archive.py b/src/borg/archive.py index c1d496dd..5805745c 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -719,24 +719,39 @@ Number of files: {0.stats.nfiles}'''.format( return 's' # symlink def chunk_file(self, item, cache, stats, fd, fh=-1, **chunk_kw): - checkpoint_number = 1 + def write_part(item, from_chunk, number): + item = Item(internal_dict=item.as_dict()) + length = len(item.chunks) + # the item should only have the *additional* chunks we processed after the last partial item: + item.chunks = item.chunks[from_chunk:] + item.path += '.checkpoint_%d' % number + item.checkpoint = number + number += 1 + self.add_item(item, show_progress=False) + self.write_checkpoint() + # we have saved the checkpoint file, but we will reference the same + # chunks also from the final, complete file: + for chunk in item.chunks: + cache.chunk_incref(chunk.id, stats) + return length, number + item.chunks = [] + from_chunk = 0 + part_number = 1 for data in backup_io_iter(self.chunker.chunkify(fd, fh)): item.chunks.append(cache.add_chunk(self.key.id_hash(data), Chunk(data, **chunk_kw), stats)) if self.show_progress: self.stats.show_progress(item=item, dt=0.2) if self.checkpoint_interval and time.time() - self.last_checkpoint > self.checkpoint_interval: - checkpoint_item = Item(internal_dict=item.as_dict()) - checkpoint_item.path += '.checkpoint_%d' % checkpoint_number - checkpoint_item.checkpoint = checkpoint_number - checkpoint_number += 1 - self.add_item(checkpoint_item, show_progress=False) - self.write_checkpoint() + from_chunk, part_number = write_part(item, from_chunk, part_number) + self.last_checkpoint = time.time() + else: + if part_number > 1 and item.chunks[from_chunk:]: + # if we already have created a part item inside this file, we want to put the final + # chunks (if any) into a part item also (so all parts can be concatenated to get + # the complete file): + from_chunk, part_number = write_part(item, from_chunk, part_number) self.last_checkpoint = time.time() - # we have saved the checkpoint file, but we will reference the same - # chunks also from the checkpoint or the "real" file we save next - for chunk in checkpoint_item.chunks: - cache.chunk_incref(chunk.id, stats) def process_stdin(self, path, cache): uid, gid = 0, 0 From 04ad1d1b0b4bc542ed36536c2e000d484c756002 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 22 Jul 2016 00:19:56 +0200 Subject: [PATCH 0033/1387] use "part file", "part", etc. consistently use .borg_part_N as filename to avoid collisions --- docs/faq.rst | 6 +++--- src/borg/archive.py | 15 +++++++-------- src/borg/archiver.py | 14 +++++++------- src/borg/constants.py | 2 +- src/borg/item.py | 2 +- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index d092b758..791da298 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -248,9 +248,9 @@ also care for deleting unneeded checkpoints. Note: the checkpointing mechanism creates hidden, partial files in an archive, so that checkpoints even work while a big file is being processed. -They are named ``.checkpoint_`` and all operations usually ignore +They are named ``.borg_part_`` and all operations usually ignore these files, but you can make them considered by giving the option -``--consider-checkpoint-files``. You usually only need that option if you are +``--consider-part-files``. You usually only need that option if you are really desperate (e.g. if you have no completed backup of that file and you'ld rather get a partial file extracted than nothing). You do **not** want to give that option under any normal circumstances. @@ -264,7 +264,7 @@ How can I restore huge file(s) over a unstable connection? ---------------------------------------------------------- If you can not manage to extract the whole big file in one go, you can extract -all the checkpoint files (see above) and manually concatenate them together. +all the part files (see above) and manually concatenate them together. If it crashes with a UnicodeError, what can I do? ------------------------------------------------- diff --git a/src/borg/archive.py b/src/borg/archive.py index 5805745c..49e68a40 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -232,7 +232,7 @@ class Archive: def __init__(self, repository, key, manifest, name, cache=None, create=False, checkpoint_interval=300, numeric_owner=False, progress=False, chunker_params=CHUNKER_PARAMS, start=None, end=None, compression=None, compression_files=None, - consider_checkpoint_files=False): + consider_part_files=False): self.cwd = os.getcwd() self.key = key self.repository = repository @@ -251,7 +251,7 @@ class Archive: if end is None: end = datetime.utcnow() self.end = end - self.consider_checkpoint_files = consider_checkpoint_files + self.consider_part_files = consider_part_files self.pipeline = DownloadPipeline(self.repository, self.key) if create: self.items_buffer = CacheChunkBuffer(self.cache, self.key, self.stats) @@ -330,8 +330,8 @@ Number of files: {0.stats.nfiles}'''.format( return 'Archive(%r)' % self.name def item_filter(self, item, filter=None): - if not self.consider_checkpoint_files and 'checkpoint' in item: - # this is a checkpoint (partial) file, we usually don't want to consider it. + if not self.consider_part_files and 'part' in item: + # this is a part(ial) file, we usually don't want to consider it. return False return filter(item) if filter else True @@ -724,13 +724,12 @@ Number of files: {0.stats.nfiles}'''.format( length = len(item.chunks) # the item should only have the *additional* chunks we processed after the last partial item: item.chunks = item.chunks[from_chunk:] - item.path += '.checkpoint_%d' % number - item.checkpoint = number + item.path += '.borg_part_%d' % number + item.part = number number += 1 self.add_item(item, show_progress=False) self.write_checkpoint() - # we have saved the checkpoint file, but we will reference the same - # chunks also from the final, complete file: + # we have saved the part file, but we will reference the same chunks also from the final, complete file: for chunk in item.chunks: cache.chunk_incref(chunk.id, stats) return length, number diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f3c78de8..aab9ff58 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -101,7 +101,7 @@ def with_archive(method): def wrapper(self, args, repository, key, manifest, **kwargs): archive = Archive(repository, key, manifest, args.location.archive, numeric_owner=getattr(args, 'numeric_owner', False), cache=kwargs.get('cache'), - consider_checkpoint_files=args.consider_checkpoint_files) + consider_part_files=args.consider_part_files) return method(self, args, repository=repository, manifest=manifest, key=key, archive=archive, **kwargs) return wrapper @@ -670,7 +670,7 @@ class Archiver: archive1 = archive archive2 = Archive(repository, key, manifest, args.archive2, - consider_checkpoint_files=args.consider_checkpoint_files) + consider_part_files=args.consider_part_files) can_compare_chunk_ids = archive1.metadata.get(b'chunker_params', False) == archive2.metadata.get( b'chunker_params', True) or args.same_chunker_params @@ -756,7 +756,7 @@ class Archiver: with cache_if_remote(repository) as cached_repo: if args.location.archive: archive = Archive(repository, key, manifest, args.location.archive, - consider_checkpoint_files=args.consider_checkpoint_files) + consider_part_files=args.consider_part_files) else: archive = None operations = FuseOperations(key, repository, manifest, archive, cached_repo) @@ -783,7 +783,7 @@ class Archiver: matcher, _ = self.build_matcher(args.excludes, args.paths) with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: archive = Archive(repository, key, manifest, args.location.archive, cache=cache, - consider_checkpoint_files=args.consider_checkpoint_files) + consider_part_files=args.consider_part_files) if args.format: format = args.format @@ -986,7 +986,7 @@ class Archiver: def do_debug_dump_archive_items(self, args, repository, manifest, key): """dump (decrypted, decompressed) archive items metadata (not: data)""" archive = Archive(repository, key, manifest, args.location.archive, - consider_checkpoint_files=args.consider_checkpoint_files) + consider_part_files=args.consider_part_files) for i, item_id in enumerate(archive.metadata[b'items']): _, data = key.decrypt(item_id, repository.get(item_id)) filename = '%06d_%s.items' % (i, bin_to_hex(item_id)) @@ -1237,9 +1237,9 @@ class Archiver: help='set umask to M (local and remote, default: %(default)04o)') common_group.add_argument('--remote-path', dest='remote_path', metavar='PATH', help='set remote path to executable (default: "borg")') - common_group.add_argument('--consider-checkpoint-files', dest='consider_checkpoint_files', + common_group.add_argument('--consider-part-files', dest='consider_part_files', action='store_true', default=False, - help='treat checkpoint files like normal files (e.g. to list/extract them)') + help='treat part files like normal files (e.g. to list/extract them)') parser = argparse.ArgumentParser(prog=prog, description='Borg - Deduplicated Backups') parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__, diff --git a/src/borg/constants.py b/src/borg/constants.py index 6b5c67c6..857641f6 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -2,7 +2,7 @@ ITEM_KEYS = frozenset(['path', 'source', 'rdev', 'chunks', 'chunks_healthy', 'hardlink_master', 'mode', 'user', 'group', 'uid', 'gid', 'mtime', 'atime', 'ctime', 'xattrs', 'bsdflags', 'acl_nfs4', 'acl_access', 'acl_default', 'acl_extended', - 'checkpoint']) + 'part']) # this is the set of keys that are always present in items: REQUIRED_ITEM_KEYS = frozenset(['path', 'mtime', ]) diff --git a/src/borg/item.py b/src/borg/item.py index 52e72448..90289dbe 100644 --- a/src/borg/item.py +++ b/src/borg/item.py @@ -155,7 +155,7 @@ class Item(PropDict): deleted = PropDict._make_property('deleted', bool) nlink = PropDict._make_property('nlink', int) - checkpoint = PropDict._make_property('checkpoint', int) + part = PropDict._make_property('part', int) class EncryptedKey(PropDict): From 999f0ae187481110609c4d693d58c8cd4b04debb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 28 Jul 2016 17:55:40 +0200 Subject: [PATCH 0034/1387] fix chunk refcounts considering part files --- src/borg/archive.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 49e68a40..13f596c3 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -729,9 +729,6 @@ Number of files: {0.stats.nfiles}'''.format( number += 1 self.add_item(item, show_progress=False) self.write_checkpoint() - # we have saved the part file, but we will reference the same chunks also from the final, complete file: - for chunk in item.chunks: - cache.chunk_incref(chunk.id, stats) return length, number item.chunks = [] @@ -745,12 +742,18 @@ Number of files: {0.stats.nfiles}'''.format( from_chunk, part_number = write_part(item, from_chunk, part_number) self.last_checkpoint = time.time() else: - if part_number > 1 and item.chunks[from_chunk:]: - # if we already have created a part item inside this file, we want to put the final - # chunks (if any) into a part item also (so all parts can be concatenated to get - # the complete file): - from_chunk, part_number = write_part(item, from_chunk, part_number) - self.last_checkpoint = time.time() + if part_number > 1: + if item.chunks[from_chunk:]: + # if we already have created a part item inside this file, we want to put the final + # chunks (if any) into a part item also (so all parts can be concatenated to get + # the complete file): + from_chunk, part_number = write_part(item, from_chunk, part_number) + self.last_checkpoint = time.time() + + # if we created part files, we have referenced all chunks from the part files, + # but we also will reference the same chunks also from the final, complete file: + for chunk in item.chunks: + cache.chunk_incref(chunk.id, stats) def process_stdin(self, path, cache): uid, gid = 0, 0 From 1eaf1b73650545253acd52313f66c794e4cc3898 Mon Sep 17 00:00:00 2001 From: Jonathan Zacsh Date: Tue, 26 Jul 2016 15:49:43 -0400 Subject: [PATCH 0035/1387] follow automation quickstart w/env. var pitfalls Mostly commentary on proper `export` usage, pitfall w/sudo, and debugging tips. --- docs/quickstart.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e3186f35..20cd32d1 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -129,6 +129,33 @@ certain number of old archives:: borg prune -v $REPOSITORY --prefix '{hostname}-' \ --keep-daily=7 --keep-weekly=4 --keep-monthly=6 +Pitfalls with shell variables and environment variables +------------------------------------------------------- + +This applies to all environment variables you want borg to see, not just +``BORG_PASSPHRASE``. The short explanation is: always ``export`` your variable, +and use single quotes if you're unsure of the details of your shell's expansion +behavior. E.g.:: + + export BORG_PASSPHRASE='complicated & long' + +This is because ``export`` exposes variables to subprocesses, which borg may be +one of. More on ``export`` can be found in the "ENVIRONMENT" section of the +bash(1) man page. + +Beware of how ``sudo`` interacts with environment variables. For example, you +may be surprised that the following ``export`` has no effect on your command:: + + export BORG_PASSPHRASE='complicated & long' + sudo ./yourborgwrapper.sh # still prompts for password + +For more information, see sudo(8) man page. Hint: see ``env_keep`` in +sudoers(5), or try ``sudo BORG_PASSPHRASE='yourphrase' borg`` syntax. + +.. Tip:: + To debug what your borg process is actually seeing, find its PID + (``ps aux|grep borg``) and then look into ``/proc//environ``. + .. backup_compression: Backup compression From d126265fe4e0b8016004e898a58faad3a9127339 Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Thu, 28 Jul 2016 15:57:03 -0400 Subject: [PATCH 0036/1387] borg mount: cache partially read data chunks Cherry-pick of bfb00df from master to 1.0-maint --- borg/archiver.py | 5 +++++ borg/fuse.py | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index d91bdd3e..76aa33ea 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1275,6 +1275,11 @@ class Archiver: option is given the command will run in the background until the filesystem is ``umounted``. + The BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is meant for advanced users + to tweak the performance. It sets the number of cached data chunks; additional + memory usage can be up to ~8 MiB times this number. The default is the number + of CPU cores. + For mount options, see the fuse(8) manual page. Additional mount options supported by borg: diff --git a/borg/fuse.py b/borg/fuse.py index 7b75b8b8..09c05d6f 100644 --- a/borg/fuse.py +++ b/borg/fuse.py @@ -13,6 +13,7 @@ import msgpack from .archive import Archive from .helpers import daemonize, bigint_to_int from .logger import create_logger +from .lrucache import LRUCache logger = create_logger() @@ -62,6 +63,9 @@ class FuseOperations(llfuse.Operations): self.pending_archives = {} self.accounted_chunks = {} self.cache = ItemCache() + data_cache_capacity = int(os.environ.get('BORG_MOUNT_DATA_CACHE_ENTRIES', os.cpu_count() or 1)) + logger.debug('mount data cache capacity: %d chunks', data_cache_capacity) + self.data_cache = LRUCache(capacity=data_cache_capacity, dispose=lambda _: None) if archive: self.process_archive(archive) else: @@ -282,8 +286,17 @@ class FuseOperations(llfuse.Operations): offset -= s continue n = min(size, s - offset) - chunk = self.key.decrypt(id, self.repository.get(id)) - parts.append(chunk[offset:offset + n]) + if id in self.data_cache: + data = self.data_cache[id] + if offset + n == len(data): + # evict fully read chunk from cache + del self.data_cache[id] + else: + data = self.key.decrypt(id, self.repository.get(id)) + if offset + n < len(data): + # chunk was only partially read, cache it + self.data_cache[id] = data + parts.append(data[offset:offset + n]) offset = 0 size -= n if not size: From edea587f3586974dfd50fa56899d7afef6c50375 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 28 Jul 2016 22:10:29 +0200 Subject: [PATCH 0037/1387] catch unpacker exceptions, resync, fixes #1351 --- borg/archive.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/borg/archive.py b/borg/archive.py index 9964f5f2..ac7d7756 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -754,6 +754,9 @@ def valid_msgpacked_dict(d, keys_serialized): class RobustUnpacker: """A restartable/robust version of the streaming msgpack unpacker """ + class UnpackerCrashed(Exception): + """raise if unpacker crashed""" + def __init__(self, validator, item_keys): super().__init__() self.item_keys = [msgpack.packb(name) for name in item_keys] @@ -798,7 +801,10 @@ class RobustUnpacker: pass data = data[1:] else: - return next(self._unpacker) + try: + return next(self._unpacker) + except (TypeError, ValueError) as err: + raise self.UnpackerCrashed(str(err)) class ArchiveChecker: @@ -1017,6 +1023,9 @@ class ArchiveChecker: yield item else: report('Did not get expected metadata dict when unpacking item metadata', chunk_id, i) + except RobustUnpacker.UnpackerCrashed as err: + report('Unpacker crashed while unpacking item metadata, trying to resync...', chunk_id, i) + unpacker.resync() except Exception: report('Exception while unpacking item metadata', chunk_id, i) raise From 852c583076efa8fbc29d576b2cd3c932b7f9c726 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 28 Jul 2016 22:12:34 +0200 Subject: [PATCH 0038/1387] handle unpacker exception with tighter scope --- borg/archive.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index ac7d7756..85454477 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -792,13 +792,14 @@ class RobustUnpacker: self._unpacker.feed(data) try: item = next(self._unpacker) + except (TypeError, ValueError, StopIteration): + # Ignore exceptions that might be raised when feeding + # msgpack with invalid data + pass + else: if self.validator(item): self._resync = False return item - # Ignore exceptions that might be raised when feeding - # msgpack with invalid data - except (TypeError, ValueError, StopIteration): - pass data = data[1:] else: try: From 97383e9e6051dd174b588a18e67aff65acc7b5b6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 28 Jul 2016 22:23:38 +0200 Subject: [PATCH 0039/1387] transform unpacker exception only at 1 place --- borg/archive.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index 85454477..653e2dd1 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -779,6 +779,14 @@ class RobustUnpacker: return self def __next__(self): + def unpack_next(): + try: + return next(self._unpacker) + except (TypeError, ValueError) as err: + # transform exceptions that might be raised when feeding + # msgpack with invalid data to a more specific exception + raise self.UnpackerCrashed(str(err)) + if self._resync: data = b''.join(self._buffered_data) while self._resync: @@ -791,10 +799,9 @@ class RobustUnpacker: self._unpacker = msgpack.Unpacker(object_hook=StableDict) self._unpacker.feed(data) try: - item = next(self._unpacker) - except (TypeError, ValueError, StopIteration): - # Ignore exceptions that might be raised when feeding - # msgpack with invalid data + item = unpack_next() + except (self.UnpackerCrashed, StopIteration): + # as long as we are resyncing, we also ignore StopIteration pass else: if self.validator(item): @@ -802,10 +809,7 @@ class RobustUnpacker: return item data = data[1:] else: - try: - return next(self._unpacker) - except (TypeError, ValueError) as err: - raise self.UnpackerCrashed(str(err)) + return unpack_next() class ArchiveChecker: From 32320c2f9f38308bed83332ea8e395ec75d6ab57 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 29 Jul 2016 00:14:06 +0200 Subject: [PATCH 0040/1387] FUSE: always create a root dir, fixes #1125 also: refactor / deduplicate directory creation --- borg/fuse.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/borg/fuse.py b/borg/fuse.py index 09c05d6f..f92f7690 100644 --- a/borg/fuse.py +++ b/borg/fuse.py @@ -66,17 +66,13 @@ class FuseOperations(llfuse.Operations): data_cache_capacity = int(os.environ.get('BORG_MOUNT_DATA_CACHE_ENTRIES', os.cpu_count() or 1)) logger.debug('mount data cache capacity: %d chunks', data_cache_capacity) self.data_cache = LRUCache(capacity=data_cache_capacity, dispose=lambda _: None) + self._create_dir(parent=1) # first call, create root dir (inode == 1) if archive: self.process_archive(archive) else: - # Create root inode - self.parent[1] = self.allocate_inode() - self.items[1] = self.default_dir for archive_name in manifest.archives: # Create archive placeholder inode - archive_inode = self.allocate_inode() - self.items[archive_inode] = self.default_dir - self.parent[archive_inode] = 1 + archive_inode = self._create_dir(parent=1) self.contents[1][os.fsencode(archive_name)] = archive_inode self.pending_archives[archive_inode] = Archive(repository, key, manifest, archive_name) @@ -106,6 +102,14 @@ class FuseOperations(llfuse.Operations): finally: llfuse.close(umount) + def _create_dir(self, parent): + """Create directory + """ + ino = self.allocate_inode() + self.items[ino] = self.default_dir + self.parent[ino] = parent + return ino + def process_archive(self, archive, prefix=[]): """Build fuse inode hierarchy from archive metadata """ @@ -129,11 +133,6 @@ class FuseOperations(llfuse.Operations): num_segments = len(segments) parent = 1 for i, segment in enumerate(segments, 1): - # Insert a default root inode if needed - if self._inode_count == 0 and segment: - archive_inode = self.allocate_inode() - self.items[archive_inode] = self.default_dir - self.parent[archive_inode] = parent # Leaf segment? if i == num_segments: if b'source' in item and stat.S_ISREG(item[b'mode']): @@ -149,9 +148,7 @@ class FuseOperations(llfuse.Operations): elif segment in self.contents[parent]: parent = self.contents[parent][segment] else: - inode = self.allocate_inode() - self.items[inode] = self.default_dir - self.parent[inode] = parent + inode = self._create_dir(parent) if segment: self.contents[parent][segment] = inode parent = inode From 4e3bfabebf11e3b9be250bf0376099b8d0f09452 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 30 Jul 2016 00:01:05 +0200 Subject: [PATCH 0041/1387] docs, platform/features: linux has BSD flag emulation --- docs/installation.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index c77bb513..9e2f0c19 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -148,7 +148,7 @@ On some platforms additional features are supported: | Platform | ACLs | xattr | Flags | | | [#acls]_ | [#xattr]_ | [#flags]_ | +==================+==========+===========+============+ -| Linux x86 | Yes | Yes | No | +| Linux x86 | Yes | Yes | Yes [1]_ | +------------------+ | | | | Linux PowerPC | | | | +------------------+ | | | @@ -177,7 +177,8 @@ Other Unix-like operating systems may work as well, but have not been tested at Note that most of the platform-dependent features also depend on the file system. For example, ntfs-3g on Linux isn't able to convey NTFS ACLs. - +.. [1] Only "nodump", "immutable", "compressed" and "append" are supported. + Feature request :issue:`618` for more flags. .. [2] Feature request :issue:`1332` .. [3] Feature request :issue:`1337` .. [4] Cygwin tries to map NTFS ACLs to permissions with varying degress of success. @@ -186,7 +187,8 @@ For example, ntfs-3g on Linux isn't able to convey NTFS ACLs. non-native ACLs. For example, NTFS ACLs aren't completely accessible on Linux with ntfs-3g. .. [#xattr] extended attributes; key-value pairs attached to a file, mainly used by the OS. This includes resource forks on Mac OS X. -.. [#flags] aka *BSD flags*. +.. [#flags] aka *BSD flags*. The Linux set of flags [1]_ is portable across platforms. + The BSDs define additional flags. .. _source-install: From ba2326555945177a70997cd9b4d5a7516952a8c3 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 30 Jul 2016 00:01:19 +0200 Subject: [PATCH 0042/1387] docs, development: link to appveyor --- docs/development.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/development.rst b/docs/development.rst index 9739c349..8a25bcf6 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -23,9 +23,10 @@ Continuous Integration ---------------------- All pull requests go through Travis-CI_, which runs the tests on Linux -and Mac OS X as well as the flake8 style checker. Additional Unix-like platforms -are tested on Golem_. +and Mac OS X as well as the flake8 style checker. Windows builds run on AppVeyor_, +while additional Unix-like platforms are tested on Golem_. +.. _AppVeyor: https://ci.appveyor.com/project/borgbackup/borg/ .. _Golem: https://golem.enkore.de/view/Borg/ .. _Travis-CI: https://travis-ci.org/borgbackup/borg From 389503db60a7af234eb0387cadcd8ef97c66b220 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 14 Jul 2016 01:54:47 +0200 Subject: [PATCH 0043/1387] Repository.compact_segments: always save_space --- src/borg/repository.py | 32 +++++++++++++++++++------------- src/borg/testsuite/repository.py | 2 +- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index c83edcec..e863bfee 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -248,9 +248,10 @@ class Repository: def commit(self, save_space=False): """Commit transaction """ + # save_space is not used anymore, but stays for RPC/API compatibility. self.io.write_commit() if not self.append_only: - self.compact_segments(save_space=save_space) + self.compact_segments() self.write_index() self.rollback() @@ -348,7 +349,7 @@ class Repository: os.unlink(os.path.join(self.path, name)) self.index = None - def compact_segments(self, save_space=False): + def compact_segments(self): """Compact sparse segments by copying data into new segments """ if not self.compact: @@ -357,12 +358,11 @@ class Repository: segments = self.segments unused = [] # list of segments, that are not used anymore - def complete_xfer(): - # complete the transfer (usually exactly when some target segment - # is full, or at the very end when everything is processed) + def complete_xfer(intermediate=True): + # complete the current transfer (when some target segment is full) nonlocal unused # commit the new, compact, used segments - self.io.write_commit() + self.io.write_commit(intermediate=intermediate) # get rid of the old, sparse, unused segments. free space. for segment in unused: assert self.segments.pop(segment) == 0 @@ -383,7 +383,7 @@ class Repository: for tag, key, offset, data in self.io.iter_objects(segment, include_data=True): if tag == TAG_PUT and self.index.get(key, (-1, -1)) == (segment, offset): try: - new_segment, offset = self.io.write_put(key, data, raise_full=save_space) + new_segment, offset = self.io.write_put(key, data, raise_full=True) except LoggedIO.SegmentFull: complete_xfer() new_segment, offset = self.io.write_put(key, data) @@ -394,13 +394,13 @@ class Repository: elif tag == TAG_DELETE: if index_transaction_id is None or segment > index_transaction_id: try: - self.io.write_delete(key, raise_full=save_space) + self.io.write_delete(key, raise_full=True) except LoggedIO.SegmentFull: complete_xfer() self.io.write_delete(key) assert segments[segment] == 0 unused.append(segment) - complete_xfer() + complete_xfer(intermediate=False) def replay_segments(self, index_transaction_id, segments_transaction_id): self.prepare_txn(index_transaction_id, do_cleanup=False) @@ -536,7 +536,7 @@ class Repository: if current_index.get(key, (-1, -1)) != value: report_error('Index mismatch for key {}. {} != {}'.format(key, value, current_index.get(key, (-1, -1)))) if repair: - self.compact_segments(save_space=save_space) + self.compact_segments() self.write_index() self.rollback() if error_found: @@ -898,9 +898,15 @@ class LoggedIO: self.offset += self.put_header_fmt.size return self.segment, self.put_header_fmt.size - def write_commit(self): - self.close_segment() - fd = self.get_write_fd() + def write_commit(self, intermediate=False): + if intermediate: + # Intermediate commits go directly into the current segment - this makes checking their validity more + # expensive, but is faster and reduces clobber. + fd = self.get_write_fd() + fd.sync() + else: + self.close_segment() + fd = self.get_write_fd() header = self.header_no_crc_fmt.pack(self.header_fmt.size, TAG_COMMIT) crc = self.crc_fmt.pack(crc32(header) & 0xffffffff) fd.write(b''.join((crc, header))) diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index a9daa2b6..5f5ec170 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -449,7 +449,7 @@ class RepositoryCheckTestCase(RepositoryTestCaseBase): # Simulate a crash before compact with patch.object(Repository, 'compact_segments') as compact: self.repository.commit() - compact.assert_called_once_with(save_space=False) + compact.assert_called_once_with() self.reopen() with self.repository: self.check(repair=True) From e9a73b808f8d5057f716c7c650331b27f065774e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 14 Jul 2016 02:08:15 +0200 Subject: [PATCH 0044/1387] Check for sufficient free space before committing --- docs/quickstart.rst | 20 ++++++++++-- src/borg/_hashindex.c | 8 ++++- src/borg/hashindex.pyx | 11 +++++-- src/borg/helpers.py | 22 ++++++++++++- src/borg/repository.py | 56 +++++++++++++++++++++++++++++--- src/borg/testsuite/hashindex.py | 15 +++++++++ src/borg/testsuite/helpers.py | 22 ++++++++++++- src/borg/testsuite/repository.py | 33 ++++++++++++++----- 8 files changed, 165 insertions(+), 22 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 76726d25..ccedf56a 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -17,13 +17,20 @@ a good amount of free space on the filesystem that has your backup repository (and also on ~/.cache). A few GB should suffice for most hard-drive sized repositories. See also :ref:`cache-memory-usage`. +Borg doesn't use space reserved for root on repository disks (even when run as root), +on file systems which do not support this mechanism (e.g. XFS) we recommend to +reserve some space in Borg itself just to be safe by adjusting the +``additional_free_space`` setting in the ``[repository]`` section of a repositories +``config`` file. A good starting point is ``2G``. + If |project_name| runs out of disk space, it tries to free as much space as it can while aborting the current operation safely, which allows to free more space -by deleting/pruning archives. This mechanism is not bullet-proof though. +by deleting/pruning archives. This mechanism is not bullet-proof in some +circumstances [1]_. + If you *really* run out of disk space, it can be hard or impossible to free space, because |project_name| needs free space to operate - even to delete backup -archives. There is a ``--save-space`` option for some commands, but even with -that |project_name| will need free space to operate. +archives. You can use some monitoring process or just include the free space information in your backup log files (you check them regularly anyway, right?). @@ -36,6 +43,13 @@ Also helpful: - consider using quotas - use `prune` regularly +.. [1] This failsafe can fail in these circumstances: + + - The underlying file system doesn't support statvfs(2), or returns incorrect + data, or the repository doesn't reside on a single file system + - Other tasks fill the disk simultaneously + - Hard quotas (which may not be reflected in statvfs(2)) + A step by step example ---------------------- diff --git a/src/borg/_hashindex.c b/src/borg/_hashindex.c index bfa3ef09..0a92ca60 100644 --- a/src/borg/_hashindex.c +++ b/src/borg/_hashindex.c @@ -441,7 +441,13 @@ hashindex_next_key(HashIndex *index, const void *key) } static int -hashindex_get_size(HashIndex *index) +hashindex_len(HashIndex *index) { return index->num_entries; } + +static int +hashindex_size(HashIndex *index) +{ + return sizeof(HashHeader) + index->num_buckets * index->bucket_size; +} diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 389cf256..74c52c9c 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -8,7 +8,7 @@ from libc.stdint cimport uint32_t, UINT32_MAX, uint64_t from libc.errno cimport errno from cpython.exc cimport PyErr_SetFromErrnoWithFilename -API_VERSION = 2 +API_VERSION = 3 cdef extern from "_hashindex.c": @@ -18,7 +18,8 @@ cdef extern from "_hashindex.c": HashIndex *hashindex_read(char *path) HashIndex *hashindex_init(int capacity, int key_size, int value_size) void hashindex_free(HashIndex *index) - int hashindex_get_size(HashIndex *index) + int hashindex_len(HashIndex *index) + int hashindex_size(HashIndex *index) int hashindex_write(HashIndex *index, char *path) void *hashindex_get(HashIndex *index, void *key) void *hashindex_next_key(HashIndex *index, void *key) @@ -119,7 +120,11 @@ cdef class IndexBase: raise def __len__(self): - return hashindex_get_size(self.index) + return hashindex_len(self.index) + + def size(self): + """Return size (bytes) of hash table.""" + return hashindex_size(self.index) cdef class NSIndex(IndexBase): diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 4da8fe66..db345f41 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -85,7 +85,7 @@ class PlaceholderError(Error): def check_extension_modules(): from . import platform - if hashindex.API_VERSION != 2: + if hashindex.API_VERSION != 3: raise ExtensionModuleError if chunker.API_VERSION != 2: raise ExtensionModuleError @@ -618,6 +618,26 @@ def format_file_size(v, precision=2, sign=False): return sizeof_fmt_decimal(v, suffix='B', sep=' ', precision=precision, sign=sign) +def parse_file_size(s): + """Return int from file size (1234, 55G, 1.7T).""" + if not s: + return int(s) # will raise + suffix = s[-1] + power = 1000 + try: + factor = { + 'K': power, + 'M': power**2, + 'G': power**3, + 'T': power**4, + 'P': power**5, + }[suffix] + s = s[:-1] + except KeyError: + factor = 1 + return int(float(s) * factor) + + def sizeof_fmt(num, suffix='B', units=None, power=None, sep='', precision=2, sign=False): prefix = '+' if sign and num > 0 else '' diff --git a/src/borg/repository.py b/src/borg/repository.py index e863bfee..468a2efa 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) from .constants import * # NOQA from .hashindex import NSIndex -from .helpers import Error, ErrorWithTraceback, IntegrityError +from .helpers import Error, ErrorWithTraceback, IntegrityError, format_file_size, parse_file_size from .helpers import Location from .helpers import ProgressIndicatorPercent from .helpers import bin_to_hex @@ -101,6 +101,9 @@ class Repository: id = bin_to_hex(id) super().__init__(id, repo) + class InsufficientFreeSpaceError(Error): + """Insufficient free space to complete transaction (required: {}, available: {}).""" + def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True, append_only=False): self.path = os.path.abspath(path) self._location = Location('file://%s' % self.path) @@ -136,8 +139,10 @@ class Repository: # EIO or FS corruption ensues, which is why we specifically check for ENOSPC. if self._active_txn and no_space_left_on_device: logger.warning('No space left on device, cleaning up partial transaction to free space.') - self.io.cleanup(self.io.get_segments_transaction_id()) - self.rollback() + cleanup = True + else: + cleanup = False + self.rollback(cleanup) self.close() @property @@ -160,6 +165,7 @@ class Repository: config.set('repository', 'segments_per_dir', str(DEFAULT_SEGMENTS_PER_DIR)) config.set('repository', 'max_segment_size', str(DEFAULT_MAX_SEGMENT_SIZE)) config.set('repository', 'append_only', str(int(self.append_only))) + config.set('repository', 'additional_free_space', '0') config.set('repository', 'id', bin_to_hex(os.urandom(32))) self.save_config(path, config) @@ -231,6 +237,7 @@ class Repository: raise self.InvalidRepository(path) self.max_segment_size = self.config.getint('repository', 'max_segment_size') self.segments_per_dir = self.config.getint('repository', 'segments_per_dir') + self.additional_free_space = parse_file_size(self.config.get('repository', 'additional_free_space', fallback=0)) # append_only can be set in the constructor # it shouldn't be overridden (True -> False) here self.append_only = self.append_only or self.config.getboolean('repository', 'append_only', fallback=False) @@ -249,6 +256,7 @@ class Repository: """Commit transaction """ # save_space is not used anymore, but stays for RPC/API compatibility. + self.check_free_space() self.io.write_commit() if not self.append_only: self.compact_segments() @@ -349,6 +357,44 @@ class Repository: os.unlink(os.path.join(self.path, name)) self.index = None + def check_free_space(self): + """Pre-commit check for sufficient free space to actually perform the commit.""" + # As a baseline we take four times the current (on-disk) index size. + # At this point the index may only be updated by compaction, which won't resize it. + # We still apply a factor of four so that a later, separate invocation can free space + # (journaling all deletes for all chunks is one index size) or still make minor additions + # (which may grow the index up to twice it's current size). + # Note that in a subsequent operation the committed index is still on-disk, therefore we + # arrive at index_size * (1 + 2 + 1). + # In that order: journaled deletes (1), hashtable growth (2), persisted index (1). + required_free_space = self.index.size() * 4 + + # Conservatively estimate hints file size: + # 10 bytes for each segment-refcount pair, 10 bytes for each segment-space pair + # Assume maximum of 5 bytes per integer. Segment numbers will usually be packed more densely (1-3 bytes), + # as will refcounts and free space integers. For 5 MiB segments this estimate is good to ~20 PB repo size. + # Add 4K to generously account for constant format overhead. + hints_size = len(self.segments) * 10 + len(self.compact) * 10 + 4096 + required_free_space += hints_size + + required_free_space += self.additional_free_space + if not self.append_only: + # Keep one full worst-case segment free in non-append-only mode + required_free_space += self.max_segment_size + MAX_OBJECT_SIZE + try: + st_vfs = os.statvfs(self.path) + except OSError as os_error: + logger.warning('Failed to check free space before committing: ' + str(os_error)) + return + # f_bavail: even as root - don't touch the Federal Block Reserve! + free_space = st_vfs.f_bavail * st_vfs.f_bsize + logger.debug('check_free_space: required bytes {}, free bytes {}'.format(required_free_space, free_space)) + if free_space < required_free_space: + self.rollback(cleanup=True) + formatted_required = format_file_size(required_free_space) + formatted_free = format_file_size(free_space) + raise self.InsufficientFreeSpaceError(formatted_required, formatted_free) + def compact_segments(self): """Compact sparse segments by copying data into new segments """ @@ -548,9 +594,11 @@ class Repository: logger.info('Completed repository check, no problems found.') return not error_found or repair - def rollback(self): + def rollback(self, cleanup=False): """ """ + if cleanup: + self.io.cleanup(self.io.get_segments_transaction_id()) self.index = None self._active_txn = False diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 000dfe4c..a7d75714 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -122,6 +122,21 @@ class HashIndexTestCase(BaseTestCase): assert unique_chunks == 3 +class HashIndexSizeTestCase(BaseTestCase): + def test_size_on_disk(self): + idx = ChunkIndex() + assert idx.size() == 18 + 1031 * (32 + 3 * 4) + + def test_size_on_disk_accurate(self): + idx = ChunkIndex() + for i in range(1234): + idx[H(i)] = i, i**2, i**3 + with tempfile.NamedTemporaryFile() as file: + idx.write(file.name) + size = os.path.getsize(file.name) + assert idx.size() == size + + class HashIndexRefcountingTestCase(BaseTestCase): def test_chunkindex_limit(self): idx = ChunkIndex() diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 9cecca55..e3b16072 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -10,7 +10,7 @@ import msgpack import msgpack.fallback from ..helpers import Location -from ..helpers import partial_format, format_file_size, format_timedelta, format_line, PlaceholderError +from ..helpers import partial_format, format_file_size, parse_file_size, format_timedelta, format_line, PlaceholderError from ..helpers import make_path_safe, clean_lines from ..helpers import prune_within, prune_split from ..helpers import get_cache_dir, get_keys_dir @@ -682,6 +682,26 @@ def test_file_size_sign(): assert format_file_size(size, sign=True) == fmt +@pytest.mark.parametrize('string,value', ( + ('1', 1), + ('20', 20), + ('5K', 5000), + ('1.75M', 1750000), + ('1e+9', 1e9), + ('-1T', -1e12), +)) +def test_parse_file_size(string, value): + assert parse_file_size(string) == int(value) + + +@pytest.mark.parametrize('string', ( + '', '5 Äpfel', '4E', '2229 bit', '1B', +)) +def test_parse_file_size_invalid(string): + with pytest.raises(ValueError): + parse_file_size(string) + + def test_is_slow_msgpack(): saved_packer = msgpack.Packer try: diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 5f5ec170..0135cacf 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -6,6 +6,8 @@ import sys import tempfile from unittest.mock import patch +import pytest + from ..hashindex import NSIndex from ..helpers import Location from ..helpers import IntegrityError @@ -35,6 +37,15 @@ class RepositoryTestCaseBase(BaseTestCase): self.repository.close() self.repository = self.open() + def add_keys(self): + self.repository.put(b'00000000000000000000000000000000', b'foo') + self.repository.put(b'00000000000000000000000000000001', b'bar') + self.repository.put(b'00000000000000000000000000000003', b'bar') + self.repository.commit() + self.repository.put(b'00000000000000000000000000000001', b'bar2') + self.repository.put(b'00000000000000000000000000000002', b'boo') + self.repository.delete(b'00000000000000000000000000000003') + class RepositoryTestCase(RepositoryTestCaseBase): @@ -168,15 +179,6 @@ class LocalRepositoryTestCase(RepositoryTestCaseBase): class RepositoryCommitTestCase(RepositoryTestCaseBase): - def add_keys(self): - self.repository.put(b'00000000000000000000000000000000', b'foo') - self.repository.put(b'00000000000000000000000000000001', b'bar') - self.repository.put(b'00000000000000000000000000000003', b'bar') - self.repository.commit() - self.repository.put(b'00000000000000000000000000000001', b'bar2') - self.repository.put(b'00000000000000000000000000000002', b'boo') - self.repository.delete(b'00000000000000000000000000000003') - def test_replay_of_missing_index(self): self.add_keys() for name in os.listdir(self.repository.path): @@ -274,6 +276,19 @@ class RepositoryAppendOnlyTestCase(RepositoryTestCaseBase): assert segments_in_repository() == 6 +class RepositoryFreeSpaceTestCase(RepositoryTestCaseBase): + def test_additional_free_space(self): + self.add_keys() + self.repository.config.set('repository', 'additional_free_space', '1000T') + self.repository.save_key(b'shortcut to save_config') + self.reopen() + + with self.repository: + self.repository.put(b'00000000000000000000000000000000', b'foobar') + with pytest.raises(Repository.InsufficientFreeSpaceError): + self.repository.commit() + + class RepositoryAuxiliaryCorruptionTestCase(RepositoryTestCaseBase): def setUp(self): super().setUp() From 5a83f744180b782e06b4a534ef956d1e9d6074e9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 30 Jul 2016 19:42:25 +0200 Subject: [PATCH 0045/1387] RemoteRepository init: always call close on exceptions, fixes #1370 This fixes the cosmetic issue that the cleanup happened in __del__ and caused an AssertionError there. --- borg/remote.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/borg/remote.py b/borg/remote.py index a3a0afe7..e79e46bc 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -170,12 +170,12 @@ class RemoteRepository: self.x_fds = [self.stdin_fd, self.stdout_fd, self.stderr_fd] try: - version = self.call('negotiate', RPC_PROTOCOL_VERSION) - except ConnectionClosed: - raise ConnectionClosedWithHint('Is borg working on the server?') from None - if version != RPC_PROTOCOL_VERSION: - raise Exception('Server insisted on using unsupported protocol version %d' % version) - try: + try: + version = self.call('negotiate', RPC_PROTOCOL_VERSION) + except ConnectionClosed: + raise ConnectionClosedWithHint('Is borg working on the server?') from None + if version != RPC_PROTOCOL_VERSION: + raise Exception('Server insisted on using unsupported protocol version %d' % version) # Because of protocol versions, only send append_only if necessary if append_only: try: From 4e737cb64b83aded55405aa73ee24a07273cd701 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 30 Jul 2016 21:08:20 +0200 Subject: [PATCH 0046/1387] OS X: install pkg-config to build with FUSE support --- Vagrantfile | 2 +- docs/installation.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index f89a1639..1ed42081 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -72,7 +72,7 @@ def packages_darwin brew install xz # required for python lzma module brew install fakeroot brew install git - brew install pkgconfig + brew install pkg-config touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile EOF end diff --git a/docs/installation.rst b/docs/installation.rst index ce26f5af..523f43cd 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -228,6 +228,7 @@ Assuming you have installed homebrew_, the following steps will install all the dependencies:: brew install python3 lz4 openssl + brew install pkg-config # optional, for FUSE support pip3 install virtualenv For FUSE support to mount the backup archives, you need at least version 3.0 of From 9865509105fe6a2528e3f54873e414d20138abf6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 30 Jul 2016 21:21:45 +0200 Subject: [PATCH 0047/1387] docs: remove borg list example, be more specific about compression heuristics --- docs/usage.rst | 11 ----------- src/borg/archiver.py | 3 ++- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 354b950c..efc5c419 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -367,17 +367,6 @@ Examples -rw-rw-r-- user user 1416192 Sun, 2015-02-01 11:00:00 code/myproject/file.ext ... - # see what is changed between archives, based on file modification time, size and file path - $ borg list /path/to/repo::archiveA --list-format="{mtime:%s}{TAB}{size}{TAB}{path}{LF}" |sort -n > /tmp/list.archiveA - $ borg list /path/to/repo::archiveB --list-format="{mtime:%s}{TAB}{size}{TAB}{path}{LF}" |sort -n > /tmp/list.archiveB - $ diff -y /tmp/list.archiveA /tmp/list.archiveB - 1422781200 0 . 1422781200 0 . - 1422781200 0 code 1422781200 0 code - 1422781200 0 code/myproject 1422781200 0 code/myproject - 1422781200 1416192 code/myproject/file.ext | 1454664653 1416192 code/myproject/file.ext - ... - - .. include:: usage/diff.rst.inc diff --git a/src/borg/archiver.py b/src/borg/archiver.py index aab9ff58..91b37cbe 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1541,7 +1541,8 @@ class Archiver: type=CompressionSpec, default=dict(name='none'), metavar='COMPRESSION', help='select compression algorithm (and level):\n' 'none == no compression (default),\n' - 'auto,C[,L] == built-in heuristic decides between none or C[,L] - with C[,L]\n' + 'auto,C[,L] == built-in heuristic (try with lz4 whether the data is\n' + ' compressible) decides between none or C[,L] - with C[,L]\n' ' being any valid compression algorithm (and optional level),\n' 'lz4 == lz4,\n' 'zlib == zlib (default level 6),\n' From 5f7b466969e3e66bb2ea9bb6d6216026d5f3a624 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 31 Jul 2016 01:33:46 +0200 Subject: [PATCH 0048/1387] implement BORG_FILES_CACHE_TTL, update FAQ raise default ttl to 20 (previously: 10). --- borg/cache.py | 3 ++- docs/faq.rst | 24 ++++++++++++++++++++++++ docs/usage.rst | 3 +++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/borg/cache.py b/borg/cache.py index 27826b46..29d0c8a1 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -193,12 +193,13 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if not self.txn_active: return if self.files is not None: + ttl = int(os.environ.get('BORG_FILES_CACHE_TTL', 20)) with open(os.path.join(self.path, 'files'), 'wb') as fd: for path_hash, item in self.files.items(): # Discard cached files with the newest mtime to avoid # issues with filesystem snapshots and mtime precision item = msgpack.unpackb(item) - if item[0] < 10 and bigint_to_int(item[3]) < self._newest_mtime: + if item[0] < ttl and bigint_to_int(item[3]) < self._newest_mtime: msgpack.pack((path_hash, item), fd) self.config.set('cache', 'manifest', hexlify(self.manifest.id).decode('ascii')) self.config.set('cache', 'timestamp', self.manifest.timestamp) diff --git a/docs/faq.rst b/docs/faq.rst index 0ea3f079..5a2d1989 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -345,6 +345,30 @@ those files are reported as being added when, really, chunks are already used. +It always chunks all my files, even unchanged ones! +--------------------------------------------------- + +|project_name| maintains a files cache where it remembers the mtime, size and +inode of files. When |project_name| does a new backup and starts processing a +file, it first looks whether the file has changed (compared to the values +stored in the files cache). If the values are the same, the file is assumed +unchanged and thus its contents won't get chunked (again). + +|project_name| can't keep an infinite history of files of course, thus entries +in the files cache have a "maximum time to live" which is set via the +environment variable BORG_FILES_CACHE_TTL (and defaults to 20). +Every time you do a backup (on the same machine, using the same user), the +cache entries' ttl values of files that were not "seen" are incremented by 1 +and if they reach BORG_FILES_CACHE_TTL, the entry is removed from the cache. + +So, for example, if you do daily backups of 26 different data sets A, B, +C, ..., Z on one machine (using the default TTL), the files from A will be +already forgotten when you repeat the same backups on the next day and it +will be slow because it would chunk all the files each time. If you set +BORG_FILES_CACHE_TTL to at least 26 (or maybe even a small multiple of that), +it would be much faster. + + Is there a way to limit bandwidth with |project_name|? ------------------------------------------------------ diff --git a/docs/usage.rst b/docs/usage.rst index fd302752..82da1f97 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -86,6 +86,9 @@ General: BORG_REMOTE_PATH When set, use the given path/filename as remote path (default is "borg"). Using ``--remote-path PATH`` commandline option overrides the environment variable. + BORG_FILES_CACHE_TTL + When set to a numeric value, this determines the maximum "time to live" for the files cache + entries (default: 20). The files cache is used to quickly determine whether a file is unchanged. TMPDIR where temporary files are stored (might need a lot of temporary space for some operations) From ad38f794e02da8ba5f6401d14efff6d223ee08e5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 31 Jul 2016 02:04:56 +0200 Subject: [PATCH 0049/1387] add hint about borg with-lock to FAQ, fixes #1353 --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index 791da298..1ac7fed9 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -31,7 +31,7 @@ Can I copy or synchronize my repo to another location? ------------------------------------------------------ Yes, you could just copy all the files. Make sure you do that while no -backup is running. So what you get here is this: +backup is running (use `borg with-lock ...`). So what you get here is this: - client machine ---borg create---> repo1 - repo1 ---copy---> repo2 From b86b5d952a48c7a71d461a92a4b4043d7fa4344e Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Sun, 24 Jul 2016 23:38:28 -0400 Subject: [PATCH 0050/1387] Filesystem feature detection and test skipping --- conftest.py | 33 +++++-- src/borg/archive.py | 12 ++- src/borg/testsuite/__init__.py | 80 +++++++++++++++-- src/borg/testsuite/archiver.py | 156 ++++++++++++++++++++------------- src/borg/testsuite/platform.py | 27 +++++- src/borg/testsuite/upgrader.py | 5 +- 6 files changed, 230 insertions(+), 83 deletions(-) diff --git a/conftest.py b/conftest.py index b5f9d982..5caf8a09 100644 --- a/conftest.py +++ b/conftest.py @@ -1,10 +1,13 @@ +import os + from borg.logger import setup_logging # Ensure that the loggers exist for all tests setup_logging() -from borg.testsuite import has_lchflags, no_lchlfags_because, has_llfuse -from borg.testsuite.platform import fakeroot_detected +from borg.testsuite import has_lchflags, has_llfuse +from borg.testsuite import are_symlinks_supported, are_hardlinks_supported, is_utime_fully_supported +from borg.testsuite.platform import fakeroot_detected, are_acls_working from borg import xattr, constants @@ -14,10 +17,22 @@ def pytest_configure(config): def pytest_report_header(config, startdir): - yesno = ['no', 'yes'] - flags = 'Testing BSD-style flags: %s %s' % (yesno[has_lchflags], no_lchlfags_because) - fakeroot = 'fakeroot: %s (>=1.20.2: %s)' % ( - yesno[fakeroot_detected()], - yesno[xattr.XATTR_FAKEROOT]) - llfuse = 'Testing fuse: %s' % yesno[has_llfuse] - return '\n'.join((flags, llfuse, fakeroot)) + tests = { + "BSD flags": has_lchflags, + "fuse": has_llfuse, + "root": not fakeroot_detected(), + "symlinks": are_symlinks_supported(), + "hardlinks": are_hardlinks_supported(), + "atime/mtime": is_utime_fully_supported(), + "modes": "BORG_TESTS_IGNORE_MODES" not in os.environ + } + enabled = [] + disabled = [] + for test in tests: + if tests[test]: + enabled.append(test) + else: + disabled.append(test) + output = "Tests enabled: " + ", ".join(enabled) + "\n" + output += "Tests disabled: " + ", ".join(disabled) + return output diff --git a/src/borg/archive.py b/src/borg/archive.py index e4565515..6ba90797 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -553,10 +553,14 @@ Number of files: {0.stats.nfiles}'''.format( else: # old archives only had mtime in item metadata atime = mtime - if fd: - os.utime(fd, None, ns=(atime, mtime)) - else: - os.utime(path, None, ns=(atime, mtime), follow_symlinks=False) + try: + if fd: + os.utime(fd, None, ns=(atime, mtime)) + else: + os.utime(path, None, ns=(atime, mtime), follow_symlinks=False) + except OSError: + # some systems don't support calling utime on a symlink + pass acl_set(path, item, self.numeric_owner) if 'bsdflags' in item: try: diff --git a/src/borg/testsuite/__init__.py b/src/borg/testsuite/__init__.py index 3f24c247..4b8114cc 100644 --- a/src/borg/testsuite/__init__.py +++ b/src/borg/testsuite/__init__.py @@ -1,5 +1,6 @@ from contextlib import contextmanager import filecmp +import functools import os import posix import stat @@ -7,6 +8,7 @@ import sys import sysconfig import tempfile import time +import uuid import unittest from ..xattr import get_all @@ -54,6 +56,67 @@ if sys.platform.startswith('netbsd'): st_mtime_ns_round = -4 # only >1 microsecond resolution here? +@contextmanager +def unopened_tempfile(): + with tempfile.TemporaryDirectory() as tempdir: + yield os.path.join(tempdir, "file") + + +@functools.lru_cache() +def are_symlinks_supported(): + with unopened_tempfile() as filepath: + try: + os.symlink('somewhere', filepath) + if os.lstat(filepath) and os.readlink(filepath) == 'somewhere': + return True + except OSError: + pass + return False + + +@functools.lru_cache() +def are_hardlinks_supported(): + with unopened_tempfile() as file1path, unopened_tempfile() as file2path: + open(file1path, 'w').close() + try: + os.link(file1path, file2path) + stat1 = os.stat(file1path) + stat2 = os.stat(file2path) + if stat1.st_nlink == stat2.st_nlink == 2 and stat1.st_ino == stat2.st_ino: + return True + except OSError: + pass + return False + + +@functools.lru_cache() +def are_fifos_supported(): + with unopened_tempfile() as filepath: + try: + os.mkfifo(filepath) + return True + except OSError: + return False + + +@functools.lru_cache() +def is_utime_fully_supported(): + with unopened_tempfile() as filepath: + # Some filesystems (such as SSHFS) don't support utime on symlinks + if are_symlinks_supported(): + os.symlink('something', filepath) + else: + open(filepath, 'w').close() + try: + os.utime(filepath, (1000, 2000), follow_symlinks=False) + new_stats = os.lstat(filepath) + if new_stats.st_atime == 1000 and new_stats.st_mtime == 2000: + return True + except OSError as err: + pass + return False + + class BaseTestCase(unittest.TestCase): """ """ @@ -103,13 +166,16 @@ class BaseTestCase(unittest.TestCase): d1[4] = None if not stat.S_ISCHR(d2[1]) and not stat.S_ISBLK(d2[1]): d2[4] = None - # Older versions of llfuse do not support ns precision properly - if fuse and not have_fuse_mtime_ns: - d1.append(round(s1.st_mtime_ns, -4)) - d2.append(round(s2.st_mtime_ns, -4)) - else: - d1.append(round(s1.st_mtime_ns, st_mtime_ns_round)) - d2.append(round(s2.st_mtime_ns, st_mtime_ns_round)) + # If utime isn't fully supported, borg can't set mtime. + # Therefore, we shouldn't test it in that case. + if is_utime_fully_supported(): + # Older versions of llfuse do not support ns precision properly + if fuse and not have_fuse_mtime_ns: + d1.append(round(s1.st_mtime_ns, -4)) + d2.append(round(s2.st_mtime_ns, -4)) + else: + d1.append(round(s1.st_mtime_ns, st_mtime_ns_round)) + d2.append(round(s2.st_mtime_ns, st_mtime_ns_round)) d1.append(get_all(path1, follow_symlinks=False)) d2.append(get_all(path2, follow_symlinks=False)) self.assert_equal(d1, d2) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 1c29fd40..fbb16dbb 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -36,6 +36,7 @@ from ..remote import RemoteRepository, PathNotAllowed from ..repository import Repository from . import has_lchflags, has_llfuse from . import BaseTestCase, changedir, environment_variable +from . import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, is_utime_fully_supported src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) @@ -274,10 +275,12 @@ class ArchiverTestCaseBase(BaseTestCase): # File mode os.chmod('input/file1', 0o4755) # Hard link - os.link(os.path.join(self.input_path, 'file1'), - os.path.join(self.input_path, 'hardlink')) + if are_hardlinks_supported(): + os.link(os.path.join(self.input_path, 'file1'), + os.path.join(self.input_path, 'hardlink')) # Symlink - os.symlink('somewhere', os.path.join(self.input_path, 'link1')) + if are_symlinks_supported(): + os.symlink('somewhere', os.path.join(self.input_path, 'link1')) if xattr.is_enabled(self.input_path): xattr.setxattr(os.path.join(self.input_path, 'file1'), 'user.foo', b'bar') # XXX this always fails for me @@ -287,7 +290,8 @@ class ArchiverTestCaseBase(BaseTestCase): # so that the test setup for all tests using it does not fail here always for others. # xattr.setxattr(os.path.join(self.input_path, 'link1'), 'user.foo_symlink', b'bar_symlink', follow_symlinks=False) # FIFO node - os.mkfifo(os.path.join(self.input_path, 'fifo1')) + if are_fifos_supported(): + os.mkfifo(os.path.join(self.input_path, 'fifo1')) if has_lchflags: platform.set_flags(os.path.join(self.input_path, 'flagfile'), stat.UF_NODUMP) try: @@ -332,12 +336,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): 'input/dir2', 'input/dir2/file2', 'input/empty', - 'input/fifo1', 'input/file1', 'input/flagfile', - 'input/hardlink', - 'input/link1', ] + if are_fifos_supported(): + expected.append('input/fifo1') + if are_symlinks_supported(): + expected.append('input/link1') + if are_hardlinks_supported(): + expected.append('input/hardlink') if not have_root: # we could not create these device files without (fake)root expected.remove('input/bdev') @@ -373,14 +380,21 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_unix_socket(self): self.cmd('init', self.repository_location) - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.bind(os.path.join(self.input_path, 'unix-socket')) + try: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.bind(os.path.join(self.input_path, 'unix-socket')) + except PermissionError as err: + if err.errno == errno.EPERM: + pytest.skip('unix sockets disabled or not supported') + elif err.errno == errno.EACCES: + pytest.skip('permission denied to create unix sockets') self.cmd('create', self.repository_location + '::test', 'input') sock.close() with changedir('output'): self.cmd('extract', self.repository_location + '::test') assert not os.path.exists('input/unix-socket') + @pytest.mark.skipif(not are_symlinks_supported(), reason='symlinks not supported') def test_symlink_extract(self): self.create_test_files() self.cmd('init', self.repository_location) @@ -389,6 +403,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('extract', self.repository_location + '::test') assert os.readlink('input/link1') == 'somewhere' + @pytest.mark.skipif(not is_utime_fully_supported(), reason='cannot properly setup and execute test without utime') def test_atime(self): def has_noatime(some_file): atime_before = os.stat(some_file).st_atime_ns @@ -557,6 +572,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') + @pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported') def test_strip_components_links(self): self._extract_hardlinks_setup() with changedir('output'): @@ -569,6 +585,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('extract', self.repository_location + '::test') assert os.stat('input/dir1/hardlink').st_nlink == 4 + @pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported') def test_extract_hardlinks(self): self._extract_hardlinks_setup() with changedir('output'): @@ -988,6 +1005,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # Restore permissions so shutil.rmtree is able to delete it os.system('chmod -R u+w ' + self.repository_path) + @pytest.mark.skipif('BORG_TESTS_IGNORE_MODES' in os.environ, reason='modes unreliable') def test_umask(self): self.create_regular_file('file1', size=1024 * 80) self.cmd('init', self.repository_location) @@ -1353,24 +1371,27 @@ class ArchiverTestCase(ArchiverTestCaseBase): else: assert False, "expected OSError(ENOATTR), but no error was raised" # hardlink (to 'input/file1') - in_fn = 'input/hardlink' - out_fn = os.path.join(mountpoint, 'input', 'hardlink') - sti2 = os.stat(in_fn) - sto2 = os.stat(out_fn) - assert sti2.st_nlink == sto2.st_nlink == 2 - assert sto1.st_ino == sto2.st_ino + if are_hardlinks_supported(): + in_fn = 'input/hardlink' + out_fn = os.path.join(mountpoint, 'input', 'hardlink') + sti2 = os.stat(in_fn) + sto2 = os.stat(out_fn) + assert sti2.st_nlink == sto2.st_nlink == 2 + assert sto1.st_ino == sto2.st_ino # symlink - in_fn = 'input/link1' - out_fn = os.path.join(mountpoint, 'input', 'link1') - sti = os.stat(in_fn, follow_symlinks=False) - sto = os.stat(out_fn, follow_symlinks=False) - assert stat.S_ISLNK(sti.st_mode) - assert stat.S_ISLNK(sto.st_mode) - assert os.readlink(in_fn) == os.readlink(out_fn) + if are_symlinks_supported(): + in_fn = 'input/link1' + out_fn = os.path.join(mountpoint, 'input', 'link1') + sti = os.stat(in_fn, follow_symlinks=False) + sto = os.stat(out_fn, follow_symlinks=False) + assert stat.S_ISLNK(sti.st_mode) + assert stat.S_ISLNK(sto.st_mode) + assert os.readlink(in_fn) == os.readlink(out_fn) # FIFO - out_fn = os.path.join(mountpoint, 'input', 'fifo1') - sto = os.stat(out_fn) - assert stat.S_ISFIFO(sto.st_mode) + if are_fifos_supported(): + out_fn = os.path.join(mountpoint, 'input', 'fifo1') + sto = os.stat(out_fn) + assert stat.S_ISFIFO(sto.st_mode) @unittest.skipUnless(has_llfuse, 'llfuse not installed') def test_fuse_allow_damaged_files(self): @@ -1481,6 +1502,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert 'dir2/file2' in listing assert 'dir2/file3' not in listing + @pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported') def test_recreate_subtree_hardlinks(self): # This is essentially the same problem set as in test_extract_hardlinks self._extract_hardlinks_setup() @@ -1884,17 +1906,19 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file_replaced', size=1024) os.mkdir('input/dir_replaced_with_file') os.chmod('input/dir_replaced_with_file', stat.S_IFDIR | 0o755) - os.mkdir('input/dir_replaced_with_link') os.mkdir('input/dir_removed') - os.symlink('input/dir_replaced_with_file', 'input/link_changed') - os.symlink('input/file_unchanged', 'input/link_removed') - os.symlink('input/file_removed2', 'input/link_target_removed') - os.symlink('input/empty', 'input/link_target_contents_changed') - os.symlink('input/empty', 'input/link_replaced_by_file') - os.link('input/empty', 'input/hardlink_contents_changed') - os.link('input/file_removed', 'input/hardlink_removed') - os.link('input/file_removed2', 'input/hardlink_target_removed') - os.link('input/file_replaced', 'input/hardlink_target_replaced') + if are_symlinks_supported(): + os.mkdir('input/dir_replaced_with_link') + os.symlink('input/dir_replaced_with_file', 'input/link_changed') + os.symlink('input/file_unchanged', 'input/link_removed') + os.symlink('input/file_removed2', 'input/link_target_removed') + os.symlink('input/empty', 'input/link_target_contents_changed') + os.symlink('input/empty', 'input/link_replaced_by_file') + if are_hardlinks_supported(): + os.link('input/empty', 'input/hardlink_contents_changed') + os.link('input/file_removed', 'input/hardlink_removed') + os.link('input/file_removed2', 'input/hardlink_target_removed') + os.link('input/file_replaced', 'input/hardlink_target_replaced') # Create the first snapshot self.cmd('create', self.repository_location + '::test0', 'input') @@ -1910,16 +1934,18 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): os.chmod('input/dir_replaced_with_file', stat.S_IFREG | 0o755) os.mkdir('input/dir_added') os.rmdir('input/dir_removed') - os.rmdir('input/dir_replaced_with_link') - os.symlink('input/dir_added', 'input/dir_replaced_with_link') - os.unlink('input/link_changed') - os.symlink('input/dir_added', 'input/link_changed') - os.symlink('input/dir_added', 'input/link_added') - os.unlink('input/link_removed') - os.unlink('input/link_replaced_by_file') - self.create_regular_file('link_replaced_by_file', size=16384) - os.unlink('input/hardlink_removed') - os.link('input/file_added', 'input/hardlink_added') + if are_symlinks_supported(): + os.rmdir('input/dir_replaced_with_link') + os.symlink('input/dir_added', 'input/dir_replaced_with_link') + os.unlink('input/link_changed') + os.symlink('input/dir_added', 'input/link_changed') + os.symlink('input/dir_added', 'input/link_added') + os.unlink('input/link_replaced_by_file') + self.create_regular_file('link_replaced_by_file', size=16384) + os.unlink('input/link_removed') + if are_hardlinks_supported(): + os.unlink('input/hardlink_removed') + os.link('input/file_added', 'input/hardlink_added') with open('input/empty', 'ab') as fd: fd.write(b'appended_data') @@ -1936,49 +1962,57 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): assert 'input/file_unchanged' not in output # Directory replaced with a regular file - assert '[drwxr-xr-x -> -rwxr-xr-x] input/dir_replaced_with_file' in output + if 'BORG_TESTS_IGNORE_MODES' not in os.environ: + assert '[drwxr-xr-x -> -rwxr-xr-x] input/dir_replaced_with_file' in output # Basic directory cases assert 'added directory input/dir_added' in output assert 'removed directory input/dir_removed' in output - # Basic symlink cases - assert 'changed link input/link_changed' in output - assert 'added link input/link_added' in output - assert 'removed link input/link_removed' in output + if are_symlinks_supported(): + # Basic symlink cases + assert 'changed link input/link_changed' in output + assert 'added link input/link_added' in output + assert 'removed link input/link_removed' in output - # Symlink replacing or being replaced - assert '] input/dir_replaced_with_link' in output - assert '] input/link_replaced_by_file' in output + # Symlink replacing or being replaced + assert '] input/dir_replaced_with_link' in output + assert '] input/link_replaced_by_file' in output - # Symlink target removed. Should not affect the symlink at all. - assert 'input/link_target_removed' not in output + # Symlink target removed. Should not affect the symlink at all. + assert 'input/link_target_removed' not in output # The inode has two links and the file contents changed. Borg # should notice the changes in both links. However, the symlink # pointing to the file is not changed. assert '0 B input/empty' in output - assert '0 B input/hardlink_contents_changed' in output - assert 'input/link_target_contents_changed' not in output + if are_hardlinks_supported(): + assert '0 B input/hardlink_contents_changed' in output + if are_symlinks_supported(): + assert 'input/link_target_contents_changed' not in output # Added a new file and a hard link to it. Both links to the same # inode should appear as separate files. assert 'added 2.05 kB input/file_added' in output - assert 'added 2.05 kB input/hardlink_added' in output + if are_hardlinks_supported(): + assert 'added 2.05 kB input/hardlink_added' in output # The inode has two links and both of them are deleted. They should # appear as two deleted files. assert 'removed 256 B input/file_removed' in output - assert 'removed 256 B input/hardlink_removed' in output + if are_hardlinks_supported(): + assert 'removed 256 B input/hardlink_removed' in output # Another link (marked previously as the source in borg) to the # same inode was removed. This should not change this link at all. - assert 'input/hardlink_target_removed' not in output + if are_hardlinks_supported(): + assert 'input/hardlink_target_removed' not in output # Another link (marked previously as the source in borg) to the # same inode was replaced with a new regular file. This should not # change this link at all. - assert 'input/hardlink_target_replaced' not in output + if are_hardlinks_supported(): + assert 'input/hardlink_target_replaced' not in output do_asserts(self.cmd('diff', self.repository_location + '::test0', 'test1a'), '1a') # We expect exit_code=1 due to the chunker params warning diff --git a/src/borg/testsuite/platform.py b/src/borg/testsuite/platform.py index 0001eeec..9bd81d2b 100644 --- a/src/borg/testsuite/platform.py +++ b/src/borg/testsuite/platform.py @@ -1,3 +1,4 @@ +import functools import os import shutil import sys @@ -6,7 +7,7 @@ import pwd import unittest from ..platform import acl_get, acl_set, swidth -from . import BaseTestCase +from . import BaseTestCase, unopened_tempfile ACCESS_ACL = """ @@ -31,6 +32,8 @@ mask::rw- other::r-- """.strip().encode('ascii') +_acls_working = None + def fakeroot_detected(): return 'FAKEROOTKEY' in os.environ @@ -44,6 +47,24 @@ def user_exists(username): return False +@functools.lru_cache() +def are_acls_working(): + with unopened_tempfile() as filepath: + open(filepath, 'w').close() + try: + access = b'user::rw-\ngroup::r--\nmask::rw-\nother::---\nuser:root:rw-:9999\ngroup:root:rw-:9999\n' + acl = {'acl_access': access} + acl_set(filepath, acl) + read_acl = {} + acl_get(filepath, read_acl, os.stat(filepath)) + read_acl_access = read_acl.get('acl_access', None) + if read_acl_access and b'user::rw-' in read_acl_access: + return True + except PermissionError: + pass + return False + + @unittest.skipUnless(sys.platform.startswith('linux'), 'linux only test') @unittest.skipIf(fakeroot_detected(), 'not compatible with fakeroot') class PlatformLinuxTestCase(BaseTestCase): @@ -63,6 +84,7 @@ class PlatformLinuxTestCase(BaseTestCase): item = {'acl_access': access, 'acl_default': default} acl_set(path, item, numeric_owner=numeric_owner) + @unittest.skipIf(not are_acls_working(), 'ACLs do not work') def test_access_acl(self): file = tempfile.NamedTemporaryFile() self.assert_equal(self.get_acl(file.name), {}) @@ -75,6 +97,7 @@ class PlatformLinuxTestCase(BaseTestCase): self.assert_in(b'user:9999:rw-:9999', self.get_acl(file2.name)['acl_access']) self.assert_in(b'group:9999:rw-:9999', self.get_acl(file2.name)['acl_access']) + @unittest.skipIf(not are_acls_working(), 'ACLs do not work') def test_default_acl(self): self.assert_equal(self.get_acl(self.tmpdir), {}) self.set_acl(self.tmpdir, access=ACCESS_ACL, default=DEFAULT_ACL) @@ -82,6 +105,7 @@ class PlatformLinuxTestCase(BaseTestCase): self.assert_equal(self.get_acl(self.tmpdir)['acl_default'], DEFAULT_ACL) @unittest.skipIf(not user_exists('übel'), 'requires übel user') + @unittest.skipIf(not are_acls_working(), 'ACLs do not work') def test_non_ascii_acl(self): # Testing non-ascii ACL processing to see whether our code is robust. # I have no idea whether non-ascii ACLs are allowed by the standard, @@ -138,6 +162,7 @@ class PlatformDarwinTestCase(BaseTestCase): item = {'acl_extended': acl} acl_set(path, item, numeric_owner=numeric_owner) + @unittest.skipIf(not are_acls_working(), 'ACLs do not work') def test_access_acl(self): file = tempfile.NamedTemporaryFile() file2 = tempfile.NamedTemporaryFile() diff --git a/src/borg/testsuite/upgrader.py b/src/borg/testsuite/upgrader.py index 088ee63b..aba3ee9c 100644 --- a/src/borg/testsuite/upgrader.py +++ b/src/borg/testsuite/upgrader.py @@ -14,6 +14,7 @@ from ..upgrader import AtticRepositoryUpgrader, AtticKeyfileKey from ..helpers import get_keys_dir from ..key import KeyfileKey from ..repository import Repository +from . import are_hardlinks_supported def repo_valid(path): @@ -177,12 +178,14 @@ def test_convert_all(tmpdir, attic_repo, attic_key_file, inplace): assert first_inode(repository.path) != first_inode(backup) # i have seen cases where the copied tree has world-readable # permissions, which is wrong - assert stat_segment(backup).st_mode & UMASK_DEFAULT == 0 + if 'BORG_TESTS_IGNORE_MODES' not in os.environ: + assert stat_segment(backup).st_mode & UMASK_DEFAULT == 0 assert key_valid(attic_key_file.path) assert repo_valid(tmpdir) +@pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported') def test_hardlink(tmpdir, inplace): """test that we handle hard links properly From e1a97c76b0eaa9cbfce59a0452a0a67cdd2f4f9b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 31 Jul 2016 14:20:06 +0200 Subject: [PATCH 0051/1387] glibc_check.py: improve / fix docstring --- scripts/glibc_check.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) mode change 100644 => 100755 scripts/glibc_check.py diff --git a/scripts/glibc_check.py b/scripts/glibc_check.py old mode 100644 new mode 100755 index a400bbd1..02be4aac --- a/scripts/glibc_check.py +++ b/scripts/glibc_check.py @@ -2,7 +2,9 @@ """ Check if all given binaries work with the given glibc version. -check_glibc.py 2.11 bin [bin ...] +glibc_check.py 2.11 bin [bin ...] + +rc = 0 means "yes", rc = 1 means "no". """ import re From 8e222d8fd9e284fc500a564e6e708fee21301534 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 31 Jul 2016 14:42:34 +0200 Subject: [PATCH 0052/1387] glibc compatibility: add FAQ entry, fixes #491 --- docs/faq.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index 5a2d1989..a9c69e20 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -414,6 +414,25 @@ If you can reproduce the issue with the proven filesystem, please file an issue in the |project_name| issue tracker about that. +Requirements for the borg single-file binary, esp. (g)libc? +----------------------------------------------------------- + +We try to build the binary on old, but still supported systems - to keep the +minimum requirement for the (g)libc low. The (g)libc can't be bundled into +the binary as it needs to fit your kernel and OS, but Python and all other +required libraries will be bundled into the binary. + +If your system fulfills the minimum (g)libc requirement (see the README that +is released with the binary), there should be no problem. If you are slightly +below the required version, maybe just try. Due to the dynamic loading (or not +loading) of some shared libraries, it might still work depending on what +libraries are actually loaded and used. + +In the borg git repository, there is scripts/glibc_check.py that can determine +(based on the symbols' versions they want to link to) whether a set of given +(Linux) binaries works with a given glibc version. + + Why was Borg forked from Attic? ------------------------------- From 2b454fc54bb51b54d5e8806b30d0960bec9e2d13 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 31 Jul 2016 00:25:53 +0200 Subject: [PATCH 0053/1387] save mountpoint dirs, fixes #1033 --- src/borg/archiver.py | 49 +++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index aab9ff58..4d2b4a8d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -324,9 +324,10 @@ class Archiver: return if (st.st_ino, st.st_dev) in skip_inodes: return - # Entering a new filesystem? - if restrict_dev is not None and st.st_dev != restrict_dev: - return + # if restrict_dev is given, we do not want to recurse into a new filesystem, + # but we WILL save the mountpoint directory (or more precise: the root + # directory of the mounted filesystem that shadows the mountpoint dir). + recurse = restrict_dev is None or st.st_dev == restrict_dev status = None # Ignore if nodump flag is set try: @@ -344,28 +345,30 @@ class Archiver: status = 'E' self.print_warning('%s: %s', path, e) elif stat.S_ISDIR(st.st_mode): - tag_paths = dir_is_tagged(path, exclude_caches, exclude_if_present) - if tag_paths: - if keep_tag_files and not dry_run: - archive.process_dir(path, st) - for tag_path in tag_paths: - self._process(archive, cache, matcher, exclude_caches, exclude_if_present, - keep_tag_files, skip_inodes, tag_path, restrict_dev, - read_special=read_special, dry_run=dry_run) - return + if recurse: + tag_paths = dir_is_tagged(path, exclude_caches, exclude_if_present) + if tag_paths: + if keep_tag_files and not dry_run: + archive.process_dir(path, st) + for tag_path in tag_paths: + self._process(archive, cache, matcher, exclude_caches, exclude_if_present, + keep_tag_files, skip_inodes, tag_path, restrict_dev, + read_special=read_special, dry_run=dry_run) + return if not dry_run: status = archive.process_dir(path, st) - try: - entries = helpers.scandir_inorder(path) - except OSError as e: - status = 'E' - self.print_warning('%s: %s', path, e) - else: - for dirent in entries: - normpath = os.path.normpath(dirent.path) - self._process(archive, cache, matcher, exclude_caches, exclude_if_present, - keep_tag_files, skip_inodes, normpath, restrict_dev, - read_special=read_special, dry_run=dry_run) + if recurse: + try: + entries = helpers.scandir_inorder(path) + except OSError as e: + status = 'E' + self.print_warning('%s: %s', path, e) + else: + for dirent in entries: + normpath = os.path.normpath(dirent.path) + self._process(archive, cache, matcher, exclude_caches, exclude_if_present, + keep_tag_files, skip_inodes, normpath, restrict_dev, + read_special=read_special, dry_run=dry_run) elif stat.S_ISLNK(st.st_mode): if not dry_run: if not read_special: From 54048a339c3a13ae26b590333bd8906b3c9c6afc Mon Sep 17 00:00:00 2001 From: Alexander 'Leo' Bergolth Date: Thu, 28 Jul 2016 09:30:46 +0200 Subject: [PATCH 0054/1387] add new placeholder {borgversion} substitute placeholders in --remote-path add BORG_VERSION environment variable before executing ssh command --- borg/archiver.py | 11 ++++++++--- borg/helpers.py | 3 ++- borg/remote.py | 3 +++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 74c1d1a4..ab9158ab 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -826,7 +826,8 @@ class Archiver: EOF $ borg create --exclude-from exclude.txt backup /\n\n''') helptext['placeholders'] = textwrap.dedent(''' - Repository (or Archive) URLs and --prefix values support these placeholders: + Repository (or Archive) URLs, --prefix and --remote-path values support these + placeholders: {hostname} @@ -852,7 +853,11 @@ class Archiver: The current process ID. - Examples:: + {borgversion} + + The version of borg. + + Examples:: borg create /path/to/repo::{hostname}-{user}-{utcnow} ... borg create /path/to/repo::{hostname}-{now:%Y-%m-%d_%H:%M:%S} ... @@ -1064,7 +1069,7 @@ class Archiver: checkpoints and treated in special ways. In the archive name, you may use the following format tags: - {now}, {utcnow}, {fqdn}, {hostname}, {user}, {pid} + {now}, {utcnow}, {fqdn}, {hostname}, {user}, {pid}, {borgversion} To speed up pulling backups over sshfs and similar network file systems which do not provide correct inode information the --ignore-inode flag can be used. This diff --git a/borg/helpers.py b/borg/helpers.py index 1a7bf8af..bacb434b 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -577,7 +577,8 @@ def replace_placeholders(text): 'hostname': socket.gethostname(), 'now': current_time.now(), 'utcnow': current_time.utcnow(), - 'user': uid2user(os.getuid(), os.getuid()) + 'user': uid2user(os.getuid(), os.getuid()), + 'borgversion': borg_version, } return format_line(text, data) diff --git a/borg/remote.py b/borg/remote.py index a8e64c12..10bde4bb 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -11,6 +11,7 @@ import tempfile from . import __version__ from .helpers import Error, IntegrityError, sysinfo +from .helpers import replace_placeholders from .repository import Repository import msgpack @@ -155,6 +156,7 @@ class RemoteRepository: # that the system's ssh binary picks up (non-matching) libraries from there env.pop('LD_LIBRARY_PATH', None) env.pop('BORG_PASSPHRASE', None) # security: do not give secrets to subprocess + env['BORG_VERSION'] = __version__ self.p = Popen(borg_cmd, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) self.stdin_fd = self.p.stdin.fileno() self.stdout_fd = self.p.stdout.fileno() @@ -221,6 +223,7 @@ class RemoteRepository: return [sys.executable, '-m', 'borg.archiver', 'serve'] + opts + self.extra_test_args else: # pragma: no cover remote_path = args.remote_path or os.environ.get('BORG_REMOTE_PATH', 'borg') + remote_path = replace_placeholders(remote_path) return [remote_path, 'serve'] + opts def ssh_cmd(self, location): From 770a892b2d905d70860f5d60191e7245a6eed614 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 30 Jul 2016 23:16:19 +0200 Subject: [PATCH 0055/1387] implement borg info REPO currently it is just the same global stats also shown in "borg info ARCHIVE", just without the archive-specific stats. also: add separate test for "borg info". --- src/borg/archive.py | 4 +-- src/borg/archiver.py | 62 +++++++++++++++++++++------------- src/borg/testsuite/archive.py | 1 - src/borg/testsuite/archiver.py | 9 +++++ 4 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 13f596c3..639363f1 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -58,9 +58,7 @@ class Statistics: if unique: self.usize += csize - summary = """\ - Original size Compressed size Deduplicated size -{label:15} {stats.osize_fmt:>20s} {stats.csize_fmt:>20s} {stats.usize_fmt:>20s}""" + summary = "{label:15} {stats.osize_fmt:>20s} {stats.csize_fmt:>20s} {stats.usize_fmt:>20s}" def __str__(self): return self.summary.format(stats=self, label='This archive:') diff --git a/src/borg/archiver.py b/src/borg/archiver.py index aab9ff58..94d32546 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -50,6 +50,9 @@ from .selftest import selftest from .upgrader import AtticRepositoryUpgrader, BorgRepositoryUpgrader +STATS_HEADER = " Original size Compressed size Deduplicated size" + + def argument(args, str_or_bool): """If bool is passed, return it. If str is passed, retrieve named attribute from args.""" if isinstance(str_or_bool, str): @@ -289,6 +292,7 @@ class Archiver: log_multi(DASHES, str(archive), DASHES, + STATS_HEADER, str(archive.stats), str(cache), DASHES, logger=logging.getLogger('borg.output.stats')) @@ -713,6 +717,7 @@ class Archiver: logger.info("Archive deleted.") if args.stats: log_multi(DASHES, + STATS_HEADER, stats.summary.format(label='Deleted data:', stats=stats), str(cache), DASHES, logger=logging.getLogger('borg.output.stats')) @@ -812,26 +817,32 @@ class Archiver: return self.exit_code @with_repository(cache=True) - @with_archive - def do_info(self, args, repository, manifest, key, archive, cache): + def do_info(self, args, repository, manifest, key, cache): """Show archive details such as disk space used""" def format_cmdline(cmdline): return remove_surrogates(' '.join(shlex.quote(x) for x in cmdline)) - stats = archive.calc_stats(cache) - print('Archive name: %s' % archive.name) - print('Archive fingerprint: %s' % archive.fpr) - print('Comment: %s' % archive.metadata.get(b'comment', '')) - print('Hostname: %s' % archive.metadata[b'hostname']) - print('Username: %s' % archive.metadata[b'username']) - print('Time (start): %s' % format_time(to_localtime(archive.ts))) - print('Time (end): %s' % format_time(to_localtime(archive.ts_end))) - print('Duration: %s' % archive.duration_from_meta) - print('Number of files: %d' % stats.nfiles) - print('Command line: %s' % format_cmdline(archive.metadata[b'cmdline'])) - print(DASHES) - print(str(stats)) - print(str(cache)) + if args.location.archive: + archive = Archive(repository, key, manifest, args.location.archive, cache=cache, + consider_part_files=args.consider_part_files) + stats = archive.calc_stats(cache) + print('Archive name: %s' % archive.name) + print('Archive fingerprint: %s' % archive.fpr) + print('Comment: %s' % archive.metadata.get(b'comment', '')) + print('Hostname: %s' % archive.metadata[b'hostname']) + print('Username: %s' % archive.metadata[b'username']) + print('Time (start): %s' % format_time(to_localtime(archive.ts))) + print('Time (end): %s' % format_time(to_localtime(archive.ts_end))) + print('Duration: %s' % archive.duration_from_meta) + print('Number of files: %d' % stats.nfiles) + print('Command line: %s' % format_cmdline(archive.metadata[b'cmdline'])) + print(DASHES) + print(STATS_HEADER) + print(str(stats)) + print(str(cache)) + else: + print(STATS_HEADER) + print(str(cache)) return self.exit_code @with_repository() @@ -896,6 +907,7 @@ class Archiver: cache.commit() if args.stats: log_multi(DASHES, + STATS_HEADER, stats.summary.format(label='Deleted data:', stats=stats), str(cache), DASHES, logger=logging.getLogger('borg.output.stats')) @@ -1782,21 +1794,23 @@ class Archiver: help='Extra mount options') info_epilog = textwrap.dedent(""" - This command displays some detailed information about the specified archive. + This command displays detailed information about the specified archive or repository. - The "This archive" line refers exclusively to this archive: - "Deduplicated size" is the size of the unique chunks stored only for this - archive. Non-unique / common chunks show up under "All archives". + The "This archive" line refers exclusively to the given archive: + "Deduplicated size" is the size of the unique chunks stored only for the + given archive. + + The "All archives" line shows global statistics (all chunks). """) subparser = subparsers.add_parser('info', parents=[common_parser], add_help=False, description=self.do_info.__doc__, epilog=info_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help='show archive information') + help='show repository or archive information') subparser.set_defaults(func=self.do_info) - subparser.add_argument('location', metavar='ARCHIVE', - type=location_validator(archive=True), - help='archive to display information about') + subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', + type=location_validator(), + help='archive or repository to display information about') break_lock_epilog = textwrap.dedent(""" This command breaks the repository and cache locks. diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index 527f7bde..30f61974 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -53,7 +53,6 @@ def tests_stats_progress(stats, columns=80): def test_stats_format(stats): assert str(stats) == """\ - Original size Compressed size Deduplicated size This archive: 20 B 10 B 10 B""" s = "{0.osize_fmt}".format(stats) assert s == "20 B" diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 1c29fd40..9a98366d 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -912,6 +912,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('test.3', manifest.archives) self.assert_in('test.4', manifest.archives) + def test_info(self): + self.create_regular_file('file1', size=1024 * 80) + self.cmd('init', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + info_repo = self.cmd('info', self.repository_location) + assert 'All archives:' in info_repo + info_archive = self.cmd('info', self.repository_location + '::test') + assert 'Archive name: test\n' in info_archive + def test_comment(self): self.create_regular_file('file1', size=1024 * 80) self.cmd('init', self.repository_location) From 661e8ebceb910873e4f40b90611089cef46edf39 Mon Sep 17 00:00:00 2001 From: anarcat Date: Tue, 2 Aug 2016 10:21:35 -0400 Subject: [PATCH 0056/1387] point to code, issues and support in devel section real story: users that are also developpers expect to find out where to submit issues and pull requests in the development section, but couldn't. add some meat there and point to the support section for everything else. --- docs/development.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/development.rst b/docs/development.rst index 8a25bcf6..ac9ff9f0 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -10,6 +10,15 @@ This chapter will get you started with |project_name| development. |project_name| is written in Python (with a little bit of Cython and C for the performance critical parts). +Code and issues +--------------- + +Code is stored on Github, in the `Borgbackup organization +`_. `Issues +`_ and `pull requests +`_ should be sent there as +well. See also the :ref:`support` section for more details. + Style guide ----------- From b5d605e5aa14161838c58503ea3c52ec33fe984c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 3 Aug 2016 18:23:43 +0200 Subject: [PATCH 0057/1387] Vagrantfile: use FUSE for macOS 3.4.1 Note: "FUSE for OS X" was renamed to "FUSE for macOS". --- Vagrantfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 1ed42081..f2e0945f 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -61,9 +61,9 @@ def packages_darwin # install all the (security and other) updates sudo softwareupdate --install --all # get osxfuse 3.x pre-release code from github: - curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.3.3/osxfuse-3.3.3.dmg >osxfuse.dmg + curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.4.1/osxfuse-3.4.1.dmg >osxfuse.dmg MOUNTDIR=$(echo `hdiutil mount osxfuse.dmg | tail -1 | awk '{$1="" ; print $0}'` | xargs -0 echo) \ - && sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for OS X 3.3.3.pkg" -target / + && sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for macOS 3.4.1.pkg" -target / sudo chown -R vagrant /usr/local # brew must be able to create stuff here ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew update From 40163c2e9f6b4faec5d1939436176a0cbf4838f3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 4 Aug 2016 01:32:01 +0200 Subject: [PATCH 0058/1387] fix fuse tests on OS X, fixes #1433 NOATIME support needed checking and the flagfile was UF_NODUMP and thus not there in the backup archive. Note: i have just duplicated the has_noatime function instead of refactoring it to be global, to avoid merge conflicts in case we cherry-pick the test improvements from master. --- borg/testsuite/archiver.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index e523e723..7e8381f1 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -998,10 +998,25 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(has_llfuse, 'llfuse not installed') def test_fuse(self): + def has_noatime(some_file): + atime_before = os.stat(some_file).st_atime_ns + try: + os.close(os.open(some_file, flags_noatime)) + except PermissionError: + return False + else: + atime_after = os.stat(some_file).st_atime_ns + noatime_used = flags_noatime != flags_normal + return noatime_used and atime_before == atime_after + self.cmd('init', self.repository_location) self.create_test_files() + have_noatime = has_noatime('input/file1') self.cmd('create', self.repository_location + '::archive', 'input') self.cmd('create', self.repository_location + '::archive2', 'input') + if has_lchflags: + # remove the file we did not backup, so input and mount become equal + os.remove(os.path.join('input', 'flagfile')) mountpoint = os.path.join(self.tmpdir, 'mountpoint') # mount the whole repository, archive contents shall show up in archivename subdirs of mountpoint: with self.fuse_mount(self.repository_location, mountpoint): @@ -1020,7 +1035,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert sti1.st_uid == sto1.st_uid assert sti1.st_gid == sto1.st_gid assert sti1.st_size == sto1.st_size - assert sti1.st_atime == sto1.st_atime + if have_noatime: + assert sti1.st_atime == sto1.st_atime assert sti1.st_ctime == sto1.st_ctime assert sti1.st_mtime == sto1.st_mtime # note: there is another hardlink to this, see below From bc6050bc3cd482458e7641c348e87261c004b966 Mon Sep 17 00:00:00 2001 From: Robert Marcano Date: Wed, 3 Aug 2016 22:54:51 -0400 Subject: [PATCH 0059/1387] Add backup using stable filesystem names recommendation --- docs/faq.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index a9c69e20..88418b18 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -368,6 +368,11 @@ will be slow because it would chunk all the files each time. If you set BORG_FILES_CACHE_TTL to at least 26 (or maybe even a small multiple of that), it would be much faster. +Another possible reason is that files don't always have the same path, for +example if you mount a filesystem without stable mount points for each backup. +If the directory where you mount a filesystem is different every time, +|project_name| assume they are different files. + Is there a way to limit bandwidth with |project_name|? ------------------------------------------------------ From 4fa420ef2956c5e0954a753d38371d41e2d7a28b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 4 Aug 2016 14:45:53 +0200 Subject: [PATCH 0060/1387] borg debug-dump-repo-objs dump all objects stored in the repository (decrypted and decompressed) --- borg/archiver.py | 35 +++++++++++++++++++++++++++++++++++ borg/testsuite/archiver.py | 10 ++++++++++ 2 files changed, 45 insertions(+) diff --git a/borg/archiver.py b/borg/archiver.py index 61318936..4c3bfca0 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -681,6 +681,28 @@ class Archiver: print('Done.') return EXIT_SUCCESS + @with_repository() + def do_debug_dump_repo_objs(self, args, repository, manifest, key): + """dump (decrypted, decompressed) repo objects""" + marker = None + i = 0 + while True: + result = repository.list(limit=10000, marker=marker) + if not result: + break + marker = result[-1] + for id in result: + cdata = repository.get(id) + give_id = id if id != Manifest.MANIFEST_ID else None + data = key.decrypt(give_id, cdata) + filename = '%06d_%s.obj' % (i, hexlify(id).decode('ascii')) + print('Dumping', filename) + with open(filename, 'wb') as fd: + fd.write(data) + i += 1 + print('Done.') + return EXIT_SUCCESS + @with_repository(manifest=False) def do_debug_get_obj(self, args, repository): """get object contents from the repository and write it into file""" @@ -1480,6 +1502,19 @@ class Archiver: type=location_validator(archive=True), help='archive to dump') + debug_dump_repo_objs_epilog = textwrap.dedent(""" + This command dumps raw (but decrypted and decompressed) repo objects to files. + """) + subparser = subparsers.add_parser('debug-dump-repo-objs', parents=[common_parser], + description=self.do_debug_dump_repo_objs.__doc__, + epilog=debug_dump_repo_objs_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='dump repo objects (debug)') + subparser.set_defaults(func=self.do_debug_dump_repo_objs) + subparser.add_argument('location', metavar='REPOSITORY', + type=location_validator(archive=False), + help='repo to dump') + debug_get_obj_epilog = textwrap.dedent(""" This command gets an object from the repository. """) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index e523e723..75f076f9 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1130,6 +1130,16 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert len(output_dir) > 0 and output_dir[0].startswith('000000_') assert 'Done.' in output + def test_debug_dump_repo_objs(self): + self.create_test_files() + self.cmd('init', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + with changedir('output'): + output = self.cmd('debug-dump-repo-objs', self.repository_location) + output_dir = sorted(os.listdir('output')) + assert len(output_dir) > 0 and output_dir[0].startswith('000000_') + assert 'Done.' in output + def test_debug_put_get_delete_obj(self): self.cmd('init', self.repository_location) data = b'some data' From b96bc155ac49a947da810c2b9c6a8a6e88af53b6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 4 Aug 2016 00:06:15 +0200 Subject: [PATCH 0061/1387] fix unintended file cache eviction, fixes #1430 thanks much to e477 for diagnosing this and finding the right fix. --- borg/cache.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/borg/cache.py b/borg/cache.py index 29d0c8a1..7badac50 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -196,10 +196,13 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" ttl = int(os.environ.get('BORG_FILES_CACHE_TTL', 20)) with open(os.path.join(self.path, 'files'), 'wb') as fd: for path_hash, item in self.files.items(): - # Discard cached files with the newest mtime to avoid - # issues with filesystem snapshots and mtime precision + # Only keep files seen in this backup that are older than newest mtime seen in this backup - + # this is to avoid issues with filesystem snapshots and mtime granularity. + # Also keep files from older backups that have not reached BORG_FILES_CACHE_TTL yet. item = msgpack.unpackb(item) - if item[0] < ttl and bigint_to_int(item[3]) < self._newest_mtime: + age = item[0] + if age == 0 and bigint_to_int(item[3]) < self._newest_mtime or \ + age > 0 and age < ttl: msgpack.pack((path_hash, item), fd) self.config.set('cache', 'manifest', hexlify(self.manifest.id).decode('ascii')) self.config.set('cache', 'timestamp', self.manifest.timestamp) From 26007c016202fdfc564ea52f3bb71e36c4486b15 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 23 Jul 2016 13:58:19 +0200 Subject: [PATCH 0062/1387] add Lock.got_exclusive_lock --- borg/locking.py | 5 +++++ borg/testsuite/locking.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/borg/locking.py b/borg/locking.py index 3a88d150..1607d21f 100644 --- a/borg/locking.py +++ b/borg/locking.py @@ -299,6 +299,8 @@ class UpgradableLock: self._roster.modify(SHARED, REMOVE) def upgrade(self): + # WARNING: if multiple read-lockers want to upgrade, it will deadlock because they + # all will wait until the other read locks go away - and that won't happen. if not self.is_exclusive: self.acquire(exclusive=True, remove=SHARED) @@ -306,6 +308,9 @@ class UpgradableLock: if self.is_exclusive: self.acquire(exclusive=False, remove=EXCLUSIVE) + def got_exclusive_lock(self): + return self.is_exclusive and self._lock.is_locked() and self._lock.by_me() + def break_lock(self): self._roster.remove() self._lock.break_lock() diff --git a/borg/testsuite/locking.py b/borg/testsuite/locking.py index bc62650d..b219e98b 100644 --- a/borg/testsuite/locking.py +++ b/borg/testsuite/locking.py @@ -86,6 +86,14 @@ class TestUpgradableLock: assert len(lock._roster.get(SHARED)) == 1 assert len(lock._roster.get(EXCLUSIVE)) == 0 + def test_got_exclusive_lock(self, lockpath): + lock = UpgradableLock(lockpath, exclusive=True, id=ID1) + assert not lock.got_exclusive_lock() + lock.acquire() + assert lock.got_exclusive_lock() + lock.release() + assert not lock.got_exclusive_lock() + def test_break(self, lockpath): lock = UpgradableLock(lockpath, exclusive=True, id=ID1).acquire() lock.break_lock() From 2a355e547e56cd24b2f7b6ca8575ef001b190c3c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 23 Jul 2016 14:13:32 +0200 Subject: [PATCH 0063/1387] make sure we have a excl. lock when starting a transaction if we don't, we try to upgrade the lock. this is to support old clients talking to a new server and also to avoid bad consequences from coding mistakes for new clients. --- borg/repository.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/borg/repository.py b/borg/repository.py index 262467dc..5cf0a4f7 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -207,14 +207,23 @@ class Repository: def prepare_txn(self, transaction_id, do_cleanup=True): self._active_txn = True - try: - self.lock.upgrade() - except (LockError, LockErrorT): - # if upgrading the lock to exclusive fails, we do not have an - # active transaction. this is important for "serve" mode, where - # the repository instance lives on - even if exceptions happened. - self._active_txn = False - raise + if not self.lock.got_exclusive_lock(): + if self.exclusive is not None: + # self.exclusive is either True or False, thus a new client is active here. + # if it is False and we get here, the caller did not use exclusive=True although + # it is needed for a write operation. if it is True and we get here, something else + # went very wrong, because we should have a exclusive lock, but we don't. + raise AssertionError("bug in code, exclusive lock should exist here") + # if we are here, this is an old client talking to a new server (expecting lock upgrade). + # or we are replaying segments and might need a lock upgrade for that. + try: + self.lock.upgrade() + except (LockError, LockErrorT): + # if upgrading the lock to exclusive fails, we do not have an + # active transaction. this is important for "serve" mode, where + # the repository instance lives on - even if exceptions happened. + self._active_txn = False + raise if not self.index or transaction_id is None: self.index = self.open_index(transaction_id) if transaction_id is None: From d3d51e12eae654687e5c29e8734b2770fc2aa616 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 23 Jul 2016 13:56:06 +0200 Subject: [PATCH 0064/1387] rename UpgradableLock to Lock lock upgrading is troublesome / may deadlock, do not advertise it. --- borg/cache.py | 6 +++--- borg/locking.py | 6 +++--- borg/repository.py | 6 +++--- borg/testsuite/locking.py | 32 ++++++++++++++++---------------- borg/testsuite/repository.py | 2 +- borg/upgrader.py | 7 +++---- 6 files changed, 29 insertions(+), 30 deletions(-) diff --git a/borg/cache.py b/borg/cache.py index 7badac50..0cacb2a8 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -11,7 +11,7 @@ from .logger import create_logger logger = create_logger() from .helpers import Error, get_cache_dir, decode_dict, int_to_bigint, \ bigint_to_int, format_file_size, yes -from .locking import UpgradableLock +from .locking import Lock from .hashindex import ChunkIndex import msgpack @@ -35,7 +35,7 @@ class Cache: @staticmethod def break_lock(repository, path=None): path = path or os.path.join(get_cache_dir(), hexlify(repository.id).decode('ascii')) - UpgradableLock(os.path.join(path, 'lock'), exclusive=True).break_lock() + Lock(os.path.join(path, 'lock'), exclusive=True).break_lock() @staticmethod def destroy(repository, path=None): @@ -152,7 +152,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def open(self, lock_wait=None): if not os.path.isdir(self.path): raise Exception('%s Does not look like a Borg cache' % self.path) - self.lock = UpgradableLock(os.path.join(self.path, 'lock'), exclusive=True, timeout=lock_wait).acquire() + self.lock = Lock(os.path.join(self.path, 'lock'), exclusive=True, timeout=lock_wait).acquire() self.rollback() def close(self): diff --git a/borg/locking.py b/borg/locking.py index 1607d21f..2dbb27cb 100644 --- a/borg/locking.py +++ b/borg/locking.py @@ -217,7 +217,7 @@ class LockRoster: self.save(roster) -class UpgradableLock: +class Lock: """ A Lock for a resource that can be accessed in a shared or exclusive way. Typically, write access to a resource needs an exclusive lock (1 writer, @@ -226,7 +226,7 @@ class UpgradableLock: If possible, try to use the contextmanager here like:: - with UpgradableLock(...) as lock: + with Lock(...) as lock: ... This makes sure the lock is released again if the block is left, no @@ -242,7 +242,7 @@ class UpgradableLock: self._roster = LockRoster(path + '.roster', id=id) # an exclusive lock, used for: # - holding while doing roster queries / updates - # - holding while the UpgradableLock itself is exclusive + # - holding while the Lock instance itself is exclusive self._lock = ExclusiveLock(path + '.exclusive', id=id, timeout=timeout) def __enter__(self): diff --git a/borg/repository.py b/borg/repository.py index 5cf0a4f7..525f3abe 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -14,7 +14,7 @@ from zlib import crc32 import msgpack from .helpers import Error, ErrorWithTraceback, IntegrityError, Location, ProgressIndicatorPercent from .hashindex import NSIndex -from .locking import UpgradableLock, LockError, LockErrorT +from .locking import Lock, LockError, LockErrorT from .lrucache import LRUCache from .platform import sync_dir @@ -161,14 +161,14 @@ class Repository: return self.get_index_transaction_id() def break_lock(self): - UpgradableLock(os.path.join(self.path, 'lock')).break_lock() + Lock(os.path.join(self.path, 'lock')).break_lock() def open(self, path, exclusive, lock_wait=None, lock=True): self.path = path if not os.path.isdir(path): raise self.DoesNotExist(path) if lock: - self.lock = UpgradableLock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait).acquire() + self.lock = Lock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait).acquire() else: self.lock = None self.config = ConfigParser(interpolation=None) diff --git a/borg/testsuite/locking.py b/borg/testsuite/locking.py index b219e98b..fcb21f1d 100644 --- a/borg/testsuite/locking.py +++ b/borg/testsuite/locking.py @@ -2,7 +2,7 @@ import time import pytest -from ..locking import get_id, TimeoutTimer, ExclusiveLock, UpgradableLock, LockRoster, \ +from ..locking import get_id, TimeoutTimer, ExclusiveLock, Lock, LockRoster, \ ADD, REMOVE, SHARED, EXCLUSIVE, LockTimeout @@ -58,36 +58,36 @@ class TestExclusiveLock: ExclusiveLock(lockpath, id=ID2, timeout=0.1).acquire() -class TestUpgradableLock: +class TestLock: def test_shared(self, lockpath): - lock1 = UpgradableLock(lockpath, exclusive=False, id=ID1).acquire() - lock2 = UpgradableLock(lockpath, exclusive=False, id=ID2).acquire() + lock1 = Lock(lockpath, exclusive=False, id=ID1).acquire() + lock2 = Lock(lockpath, exclusive=False, id=ID2).acquire() assert len(lock1._roster.get(SHARED)) == 2 assert len(lock1._roster.get(EXCLUSIVE)) == 0 lock1.release() lock2.release() def test_exclusive(self, lockpath): - with UpgradableLock(lockpath, exclusive=True, id=ID1) as lock: + with Lock(lockpath, exclusive=True, id=ID1) as lock: assert len(lock._roster.get(SHARED)) == 0 assert len(lock._roster.get(EXCLUSIVE)) == 1 def test_upgrade(self, lockpath): - with UpgradableLock(lockpath, exclusive=False) as lock: + with Lock(lockpath, exclusive=False) as lock: lock.upgrade() lock.upgrade() # NOP assert len(lock._roster.get(SHARED)) == 0 assert len(lock._roster.get(EXCLUSIVE)) == 1 def test_downgrade(self, lockpath): - with UpgradableLock(lockpath, exclusive=True) as lock: + with Lock(lockpath, exclusive=True) as lock: lock.downgrade() lock.downgrade() # NOP assert len(lock._roster.get(SHARED)) == 1 assert len(lock._roster.get(EXCLUSIVE)) == 0 def test_got_exclusive_lock(self, lockpath): - lock = UpgradableLock(lockpath, exclusive=True, id=ID1) + lock = Lock(lockpath, exclusive=True, id=ID1) assert not lock.got_exclusive_lock() lock.acquire() assert lock.got_exclusive_lock() @@ -95,23 +95,23 @@ class TestUpgradableLock: assert not lock.got_exclusive_lock() def test_break(self, lockpath): - lock = UpgradableLock(lockpath, exclusive=True, id=ID1).acquire() + lock = Lock(lockpath, exclusive=True, id=ID1).acquire() lock.break_lock() assert len(lock._roster.get(SHARED)) == 0 assert len(lock._roster.get(EXCLUSIVE)) == 0 - with UpgradableLock(lockpath, exclusive=True, id=ID2): + with Lock(lockpath, exclusive=True, id=ID2): pass def test_timeout(self, lockpath): - with UpgradableLock(lockpath, exclusive=False, id=ID1): + with Lock(lockpath, exclusive=False, id=ID1): with pytest.raises(LockTimeout): - UpgradableLock(lockpath, exclusive=True, id=ID2, timeout=0.1).acquire() - with UpgradableLock(lockpath, exclusive=True, id=ID1): + Lock(lockpath, exclusive=True, id=ID2, timeout=0.1).acquire() + with Lock(lockpath, exclusive=True, id=ID1): with pytest.raises(LockTimeout): - UpgradableLock(lockpath, exclusive=False, id=ID2, timeout=0.1).acquire() - with UpgradableLock(lockpath, exclusive=True, id=ID1): + Lock(lockpath, exclusive=False, id=ID2, timeout=0.1).acquire() + with Lock(lockpath, exclusive=True, id=ID1): with pytest.raises(LockTimeout): - UpgradableLock(lockpath, exclusive=True, id=ID2, timeout=0.1).acquire() + Lock(lockpath, exclusive=True, id=ID2, timeout=0.1).acquire() @pytest.fixture() diff --git a/borg/testsuite/repository.py b/borg/testsuite/repository.py index b72e8041..e034cf9e 100644 --- a/borg/testsuite/repository.py +++ b/borg/testsuite/repository.py @@ -6,7 +6,7 @@ from unittest.mock import patch from ..hashindex import NSIndex from ..helpers import Location, IntegrityError -from ..locking import UpgradableLock, LockFailed +from ..locking import Lock, LockFailed from ..remote import RemoteRepository, InvalidRPCMethod from ..repository import Repository, LoggedIO, TAG_COMMIT from . import BaseTestCase diff --git a/borg/upgrader.py b/borg/upgrader.py index 75d9fbb4..f4327e34 100644 --- a/borg/upgrader.py +++ b/borg/upgrader.py @@ -7,7 +7,7 @@ import shutil import time from .helpers import get_keys_dir, get_cache_dir, ProgressIndicatorPercent -from .locking import UpgradableLock +from .locking import Lock from .repository import Repository, MAGIC from .key import KeyfileKey, KeyfileNotFoundError @@ -39,7 +39,7 @@ class AtticRepositoryUpgrader(Repository): shutil.copytree(self.path, backup, copy_function=os.link) logger.info("opening attic repository with borg and converting") # now lock the repo, after we have made the copy - self.lock = UpgradableLock(os.path.join(self.path, 'lock'), exclusive=True, timeout=1.0).acquire() + self.lock = Lock(os.path.join(self.path, 'lock'), exclusive=True, timeout=1.0).acquire() segments = [filename for i, filename in self.io.segment_iterator()] try: keyfile = self.find_attic_keyfile() @@ -48,8 +48,7 @@ class AtticRepositoryUpgrader(Repository): else: self.convert_keyfiles(keyfile, dryrun) # partial open: just hold on to the lock - self.lock = UpgradableLock(os.path.join(self.path, 'lock'), - exclusive=True).acquire() + self.lock = Lock(os.path.join(self.path, 'lock'), exclusive=True).acquire() try: self.convert_cache(dryrun) self.convert_repo_index(dryrun=dryrun, inplace=inplace) From 1e739fd52dbe417db225027baa41c8d60679d8d8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 23 Jul 2016 16:16:56 +0200 Subject: [PATCH 0065/1387] fix local repo / upgrader tests --- borg/archiver.py | 10 +++---- borg/testsuite/archiver.py | 4 +-- borg/testsuite/repository.py | 53 +++++++++++++++++++++++++----------- borg/testsuite/upgrader.py | 2 +- 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 4c3bfca0..3c921e96 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -134,7 +134,7 @@ class Archiver: pass return self.exit_code - @with_repository(exclusive='repair', manifest=False) + @with_repository(exclusive=True, manifest=False) def do_check(self, args, repository): """Check repository consistency""" if args.repair: @@ -174,7 +174,7 @@ class Archiver: key_new.change_passphrase() # option to change key protection passphrase, save return EXIT_SUCCESS - @with_repository(fake='dry_run') + @with_repository(fake='dry_run', exclusive=True) def do_create(self, args, repository, manifest=None, key=None): """Create new archive""" matcher = PatternMatcher(fallback=True) @@ -595,7 +595,7 @@ class Archiver: print(str(cache)) return self.exit_code - @with_repository() + @with_repository(exclusive=True) def do_prune(self, args, repository, manifest, key): """Prune repository archives according to specified rules""" if not any((args.hourly, args.daily, @@ -722,7 +722,7 @@ class Archiver: print("object %s fetched." % hex_id) return EXIT_SUCCESS - @with_repository(manifest=False) + @with_repository(manifest=False, exclusive=True) def do_debug_put_obj(self, args, repository): """put file(s) contents into the repository""" for path in args.paths: @@ -734,7 +734,7 @@ class Archiver: repository.commit() return EXIT_SUCCESS - @with_repository(manifest=False) + @with_repository(manifest=False, exclusive=True) def do_debug_delete_obj(self, args, repository): """delete the objects with the given IDs from the repo""" modified = False diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index ff83a225..f1f70fcd 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -236,7 +236,7 @@ class ArchiverTestCaseBase(BaseTestCase): self.cmd('create', self.repository_location + '::' + name, src_dir) def open_archive(self, name): - repository = Repository(self.repository_path) + repository = Repository(self.repository_path, exclusive=True) with repository: manifest, key = Manifest.load(repository) archive = Archive(repository, key, manifest, name) @@ -1288,7 +1288,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): def test_extra_chunks(self): self.cmd('check', self.repository_location, exit_code=0) - with Repository(self.repository_location) as repository: + with Repository(self.repository_location, exclusive=True) as repository: repository.put(b'01234567890123456789012345678901', b'xxxx') repository.commit() self.cmd('check', self.repository_location, exit_code=1) diff --git a/borg/testsuite/repository.py b/borg/testsuite/repository.py index e034cf9e..a012f514 100644 --- a/borg/testsuite/repository.py +++ b/borg/testsuite/repository.py @@ -12,11 +12,17 @@ from ..repository import Repository, LoggedIO, TAG_COMMIT from . import BaseTestCase +UNSPECIFIED = object() # for default values where we can't use None + + class RepositoryTestCaseBase(BaseTestCase): key_size = 32 + exclusive = True - def open(self, create=False): - return Repository(os.path.join(self.tmppath, 'repository'), create=create) + def open(self, create=False, exclusive=UNSPECIFIED): + if exclusive is UNSPECIFIED: + exclusive = self.exclusive + return Repository(os.path.join(self.tmppath, 'repository'), exclusive=exclusive, create=create) def setUp(self): self.tmppath = tempfile.mkdtemp() @@ -27,10 +33,10 @@ class RepositoryTestCaseBase(BaseTestCase): self.repository.close() shutil.rmtree(self.tmppath) - def reopen(self): + def reopen(self, exclusive=UNSPECIFIED): if self.repository: self.repository.close() - self.repository = self.open() + self.repository = self.open(exclusive=exclusive) class RepositoryTestCase(RepositoryTestCaseBase): @@ -156,17 +162,6 @@ class RepositoryCommitTestCase(RepositoryTestCaseBase): self.assert_equal(len(self.repository), 3) self.assert_equal(self.repository.check(), True) - def test_replay_of_readonly_repository(self): - self.add_keys() - for name in os.listdir(self.repository.path): - if name.startswith('index.'): - os.unlink(os.path.join(self.repository.path, name)) - with patch.object(UpgradableLock, 'upgrade', side_effect=LockFailed) as upgrade: - self.reopen() - with self.repository: - self.assert_raises(LockFailed, lambda: len(self.repository)) - upgrade.assert_called_once_with() - def test_crash_before_write_index(self): self.add_keys() self.repository.write_index = None @@ -179,6 +174,32 @@ class RepositoryCommitTestCase(RepositoryTestCaseBase): self.assert_equal(len(self.repository), 3) self.assert_equal(self.repository.check(), True) + def test_replay_lock_upgrade_old(self): + self.add_keys() + for name in os.listdir(self.repository.path): + if name.startswith('index.'): + os.unlink(os.path.join(self.repository.path, name)) + with patch.object(Lock, 'upgrade', side_effect=LockFailed) as upgrade: + self.reopen(exclusive=None) # simulate old client that always does lock upgrades + with self.repository: + # the repo is only locked by a shared read lock, but to replay segments, + # we need an exclusive write lock - check if the lock gets upgraded. + self.assert_raises(LockFailed, lambda: len(self.repository)) + upgrade.assert_called_once_with() + + def test_replay_lock_upgrade(self): + self.add_keys() + for name in os.listdir(self.repository.path): + if name.startswith('index.'): + os.unlink(os.path.join(self.repository.path, name)) + with patch.object(Lock, 'upgrade', side_effect=LockFailed) as upgrade: + self.reopen(exclusive=False) # current client usually does not do lock upgrade, except for replay + with self.repository: + # the repo is only locked by a shared read lock, but to replay segments, + # we need an exclusive write lock - check if the lock gets upgraded. + self.assert_raises(LockFailed, lambda: len(self.repository)) + upgrade.assert_called_once_with() + def test_crash_before_deleting_compacted_segments(self): self.add_keys() self.repository.io.delete_segment = None @@ -202,7 +223,7 @@ class RepositoryCommitTestCase(RepositoryTestCaseBase): class RepositoryAppendOnlyTestCase(RepositoryTestCaseBase): def open(self, create=False): - return Repository(os.path.join(self.tmppath, 'repository'), create=create, append_only=True) + return Repository(os.path.join(self.tmppath, 'repository'), exclusive=True, create=create, append_only=True) def test_destroy_append_only(self): # Can't destroy append only repo (via the API) diff --git a/borg/testsuite/upgrader.py b/borg/testsuite/upgrader.py index 26c34a3c..013a8d00 100644 --- a/borg/testsuite/upgrader.py +++ b/borg/testsuite/upgrader.py @@ -23,7 +23,7 @@ def repo_valid(path): :param path: the path to the repository :returns: if borg can check the repository """ - with Repository(str(path), create=False) as repository: + with Repository(str(path), exclusive=True, create=False) as repository: # can't check raises() because check() handles the error return repository.check() From 64dcbbfdd06fc2187ef2d3d57bd61bbd3af8f371 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 23 Jul 2016 18:22:07 +0200 Subject: [PATCH 0066/1387] change RPC API, fix remote repo tests --- borg/archiver.py | 4 ++-- borg/remote.py | 25 ++++++++++++------------- borg/testsuite/repository.py | 6 ++++-- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 3c921e96..bfd56bf0 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -68,8 +68,8 @@ def with_repository(fake=False, create=False, lock=True, exclusive=False, manife if argument(args, fake): return method(self, args, repository=None, **kwargs) elif location.proto == 'ssh': - repository = RemoteRepository(location, create=create, lock_wait=self.lock_wait, lock=lock, - append_only=append_only, args=args) + repository = RemoteRepository(location, create=create, exclusive=argument(args, exclusive), + lock_wait=self.lock_wait, lock=lock, append_only=append_only, args=args) else: repository = Repository(location.path, create=create, exclusive=argument(args, exclusive), lock_wait=self.lock_wait, lock=lock, diff --git a/borg/remote.py b/borg/remote.py index daaa021c..8d1bf95e 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -114,7 +114,7 @@ class RepositoryServer: # pragma: no cover def negotiate(self, versions): return RPC_PROTOCOL_VERSION - def open(self, path, create=False, lock_wait=None, lock=True, append_only=False): + def open(self, path, create=False, lock_wait=None, lock=True, exclusive=False, append_only=False): path = os.fsdecode(path) if path.startswith('/~'): path = path[1:] @@ -125,7 +125,9 @@ class RepositoryServer: # pragma: no cover break else: raise PathNotAllowed(path) - self.repository = Repository(path, create, lock_wait=lock_wait, lock=lock, append_only=self.append_only or append_only) + self.repository = Repository(path, create, lock_wait=lock_wait, lock=lock, + append_only=self.append_only or append_only, + exclusive=exclusive) self.repository.__enter__() # clean exit handled by serve() method return self.repository.id @@ -141,7 +143,7 @@ class RemoteRepository: class NoAppendOnlyOnServer(Error): """Server does not support --append-only.""" - def __init__(self, location, create=False, lock_wait=None, lock=True, append_only=False, args=None): + def __init__(self, location, create=False, exclusive=False, lock_wait=None, lock=True, append_only=False, args=None): self.location = self._location = location self.preload_ids = [] self.msgid = 0 @@ -178,16 +180,13 @@ class RemoteRepository: raise ConnectionClosedWithHint('Is borg working on the server?') from None if version != RPC_PROTOCOL_VERSION: raise Exception('Server insisted on using unsupported protocol version %d' % version) - # Because of protocol versions, only send append_only if necessary - if append_only: - try: - self.id = self.call('open', self.location.path, create, lock_wait, lock, append_only) - except self.RPCError as err: - if err.remote_type == 'TypeError': - raise self.NoAppendOnlyOnServer() from err - else: - raise - else: + try: + self.id = self.call('open', self.location.path, create, lock_wait, lock, exclusive, append_only) + except self.RPCError as err: + if err.remote_type != 'TypeError': + raise + if append_only: + raise self.NoAppendOnlyOnServer() self.id = self.call('open', self.location.path, create, lock_wait, lock) except Exception: self.close() diff --git a/borg/testsuite/repository.py b/borg/testsuite/repository.py index a012f514..bc08e097 100644 --- a/borg/testsuite/repository.py +++ b/borg/testsuite/repository.py @@ -386,7 +386,8 @@ class RepositoryCheckTestCase(RepositoryTestCaseBase): class RemoteRepositoryTestCase(RepositoryTestCase): def open(self, create=False): - return RemoteRepository(Location('__testsuite__:' + os.path.join(self.tmppath, 'repository')), create=create) + return RemoteRepository(Location('__testsuite__:' + os.path.join(self.tmppath, 'repository')), + exclusive=True, create=create) def test_invalid_rpc(self): self.assert_raises(InvalidRPCMethod, lambda: self.repository.call('__init__', None)) @@ -415,7 +416,8 @@ class RemoteRepositoryTestCase(RepositoryTestCase): class RemoteRepositoryCheckTestCase(RepositoryCheckTestCase): def open(self, create=False): - return RemoteRepository(Location('__testsuite__:' + os.path.join(self.tmppath, 'repository')), create=create) + return RemoteRepository(Location('__testsuite__:' + os.path.join(self.tmppath, 'repository')), + exclusive=True, create=create) def test_crash_before_compact(self): # skip this test, we can't mock-patch a Repository class in another process! From 33e334820824c735d5e19486467abf17a2059d17 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 3 Aug 2016 00:34:22 +0200 Subject: [PATCH 0067/1387] locking: better differentiate new vs. old clients, lock upgrade for replay old clients use self.exclusive = None and do a read->write lock upgrade when needed. new clients use self.exclusive = True/False and never upgrade. replay fakes an old client by setting self.exclusive = None to get a lock upgrade if needed. --- borg/remote.py | 2 +- borg/repository.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/borg/remote.py b/borg/remote.py index 8d1bf95e..47a20412 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -114,7 +114,7 @@ class RepositoryServer: # pragma: no cover def negotiate(self, versions): return RPC_PROTOCOL_VERSION - def open(self, path, create=False, lock_wait=None, lock=True, exclusive=False, append_only=False): + def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False): path = os.fsdecode(path) if path.startswith('/~'): path = path[1:] diff --git a/borg/repository.py b/borg/repository.py index 525f3abe..66c0f638 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -79,7 +79,7 @@ class Repository: if self.do_create: self.do_create = False self.create(self.path) - self.open(self.path, self.exclusive, lock_wait=self.lock_wait, lock=self.do_lock) + self.open(self.path, bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock) return self def __exit__(self, exc_type, exc_val, exc_tb): @@ -317,6 +317,9 @@ class Repository: self.compact = set() def replay_segments(self, index_transaction_id, segments_transaction_id): + # fake an old client, so that in case we do not have an exclusive lock yet, prepare_txn will upgrade the lock: + remember_exclusive = self.exclusive + self.exclusive = None self.prepare_txn(index_transaction_id, do_cleanup=False) try: segment_count = sum(1 for _ in self.io.segment_iterator()) @@ -332,6 +335,7 @@ class Repository: pi.finish() self.write_index() finally: + self.exclusive = remember_exclusive self.rollback() def _update_index(self, segment, objects, report=None): From b79e913244729d87caa0f394c12d9b73ed48db7f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 5 Aug 2016 16:07:33 +0200 Subject: [PATCH 0068/1387] update CHANGES --- docs/changes.rst | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 7d9b47ab..e107fc8f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,6 +50,70 @@ The best check that everything is ok is to run a dry-run extraction:: borg extract -v --dry-run REPO::ARCHIVE +Version 1.0.7 (not released yet) +-------------------------------- + +Security fixes: + +- fix security issue with remote repository access, #1428 + + +Version 1.0.7rc1 (not released yet) +----------------------------------- + +Bug fixes: + +- fix repo lock deadlocks (related to lock upgrade), #1220 +- catch unpacker exceptions, resync, #1351 +- fix borg break-lock ignoring BORG_REPO env var, #1324 +- files cache performance fixes (fixes unneccessary re-reading/chunking/ + hashing of unmodified files for some use cases): + + - fix unintended file cache eviction, #1430 + - implement BORG_FILES_CACHE_TTL, update FAQ, raise default TTL from 10 + to 20, #1338 +- FUSE: + + - cache partially read data chunks (performance), #965, #966 + - always create a root dir, #1125 +- use an OrderedDict for helptext, making the build reproducible, #1346 +- RemoteRepository init: always call close on exceptions, #1370 (cosmetic) +- ignore stdout/stderr broken pipe errors (cosmetic), #1116 + +New features: + +- better borg versions management support (useful esp. for borg servers + wanting to offer multiple borg versions and for clients wanting to choose + a specific server borg version), #1392: + + - add BORG_VERSION environment variable before executing "borg serve" via ssh + - add new placeholder {borgversion} + - substitute placeholders in --remote-path + +- borg init --append-only option (makes using the more secure append-only mode + more convenient. when used remotely, this requires 1.0.7+ also on the borg + server), #1291. + +Other changes: + +- Vagrantfile: + + - darwin64: upgrade to FUSE for macOS 3.4.1 (aka osxfuse), #1378 + - xenial64: use user "ubuntu", not "vagrant" (as usual), #1331 +- tests: + + - fix fuse tests on OS X, #1433 +- docs: + + - FAQ: add backup using stable filesystem names recommendation + - FAQ about glibc compatibility added, #491, glibc-check improved + - FAQ: 'A' unchanged file; remove ambiguous entry age sentence. + - OS X: install pkg-config to build with FUSE support, fixes #1400 + - add notes about shell/sudo pitfalls with env. vars, #1380 + - added platform feature matrix +- implement borg debug-dump-repo-objs + + Version 1.0.6 (2016-07-12) -------------------------- From 5b575f69dc2ccfdf773b3f29c302e4f542a98bbd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 5 Aug 2016 20:23:47 +0200 Subject: [PATCH 0069/1387] CHANGES: add date to 1.0.7rc1 --- docs/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index e107fc8f..ddfdb8f4 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -58,8 +58,8 @@ Security fixes: - fix security issue with remote repository access, #1428 -Version 1.0.7rc1 (not released yet) ------------------------------------ +Version 1.0.7rc1 (2016-08-05) +----------------------------- Bug fixes: From a56b010960e5052cf7c380eec78644799cd8ab58 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 5 Aug 2016 20:26:09 +0200 Subject: [PATCH 0070/1387] ran build_usage --- docs/usage/break-lock.rst.inc | 2 +- docs/usage/create.rst.inc | 2 +- docs/usage/help.rst.inc | 81 +++++++++++++++++++---------------- docs/usage/init.rst.inc | 3 +- docs/usage/mount.rst.inc | 5 +++ 5 files changed, 52 insertions(+), 41 deletions(-) diff --git a/docs/usage/break-lock.rst.inc b/docs/usage/break-lock.rst.inc index 43707040..b3ba522a 100644 --- a/docs/usage/break-lock.rst.inc +++ b/docs/usage/break-lock.rst.inc @@ -9,7 +9,7 @@ borg break-lock usage: borg break-lock [-h] [--critical] [--error] [--warning] [--info] [--debug] [--lock-wait N] [--show-rc] [--no-files-cache] [--umask M] [--remote-path PATH] - REPOSITORY + [REPOSITORY] Break the repository lock (e.g. in case it was left by a dead borg. diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index fbf531bc..c2d42f44 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -90,7 +90,7 @@ The archive name needs to be unique. It must not end in '.checkpoint' or checkpoints and treated in special ways. In the archive name, you may use the following format tags: -{now}, {utcnow}, {fqdn}, {hostname}, {user}, {pid} +{now}, {utcnow}, {fqdn}, {hostname}, {user}, {pid}, {borgversion} To speed up pulling backups over sshfs and similar network file systems which do not provide correct inode information the --ignore-inode flag can be used. This diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index 4d7c776a..b079dd2d 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -1,43 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. _borg_placeholders: - -borg help placeholders -~~~~~~~~~~~~~~~~~~~~~~ - - -Repository (or Archive) URLs and --prefix values support these placeholders: - -{hostname} - - The (short) hostname of the machine. - -{fqdn} - - The full name of the machine. - -{now} - - The current local date and time. - -{utcnow} - - The current UTC date and time. - -{user} - - The user name (or UID, if no name is available) of the user running borg. - -{pid} - - The current process ID. - -Examples:: - - borg create /path/to/repo::{hostname}-{user}-{utcnow} ... - borg create /path/to/repo::{hostname}-{now:%Y-%m-%d_%H:%M:%S} ... - borg prune --prefix '{hostname}-' ... - .. _borg_patterns: borg help patterns @@ -131,3 +93,46 @@ Examples:: EOF $ borg create --exclude-from exclude.txt backup / +.. _borg_placeholders: + +borg help placeholders +~~~~~~~~~~~~~~~~~~~~~~ + + + Repository (or Archive) URLs, --prefix and --remote-path values support these + placeholders: + + {hostname} + + The (short) hostname of the machine. + + {fqdn} + + The full name of the machine. + + {now} + + The current local date and time. + + {utcnow} + + The current UTC date and time. + + {user} + + The user name (or UID, if no name is available) of the user running borg. + + {pid} + + The current process ID. + + {borgversion} + + The version of borg. + +Examples:: + + borg create /path/to/repo::{hostname}-{user}-{utcnow} ... + borg create /path/to/repo::{hostname}-{now:%Y-%m-%d_%H:%M:%S} ... + borg prune --prefix '{hostname}-' ... + diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index 798fe6e5..f9102141 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -8,7 +8,7 @@ borg init usage: borg init [-h] [--critical] [--error] [--warning] [--info] [--debug] [--lock-wait N] [--show-rc] [--no-files-cache] [--umask M] - [--remote-path PATH] [-e {none,keyfile,repokey}] + [--remote-path PATH] [-e {none,keyfile,repokey}] [-a] [REPOSITORY] Initialize an empty repository @@ -32,6 +32,7 @@ borg init --remote-path PATH set remote path to executable (default: "borg") -e {none,keyfile,repokey}, --encryption {none,keyfile,repokey} select encryption key mode (default: "repokey") + -a, --append-only create an append-only mode repository Description ~~~~~~~~~~~ diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index b8d1fb5a..6deef307 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -43,6 +43,11 @@ browsing an archive or restoring individual files. Unless the ``--foreground`` option is given the command will run in the background until the filesystem is ``umounted``. +The BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is meant for advanced users +to tweak the performance. It sets the number of cached data chunks; additional +memory usage can be up to ~8 MiB times this number. The default is the number +of CPU cores. + For mount options, see the fuse(8) manual page. Additional mount options supported by borg: From 6c1c87f7ae8cf3235894f4cec0f40dcd16cc96ba Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 6 Aug 2016 01:28:02 +0200 Subject: [PATCH 0071/1387] add forgotten usage help file from build_usage --- docs/usage/debug-dump-repo-objs.rst.inc | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/usage/debug-dump-repo-objs.rst.inc diff --git a/docs/usage/debug-dump-repo-objs.rst.inc b/docs/usage/debug-dump-repo-objs.rst.inc new file mode 100644 index 00000000..4fcd45ae --- /dev/null +++ b/docs/usage/debug-dump-repo-objs.rst.inc @@ -0,0 +1,38 @@ +.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! + +.. _borg_debug-dump-repo-objs: + +borg debug-dump-repo-objs +------------------------- +:: + + usage: borg debug-dump-repo-objs [-h] [--critical] [--error] [--warning] + [--info] [--debug] [--lock-wait N] + [--show-rc] [--no-files-cache] [--umask M] + [--remote-path PATH] + REPOSITORY + + dump (decrypted, decompressed) repo objects + + positional arguments: + REPOSITORY repo to dump + + optional arguments: + -h, --help show this help message and exit + --critical work on log level CRITICAL + --error work on log level ERROR + --warning work on log level WARNING (default) + --info, -v, --verbose + work on log level INFO + --debug work on log level DEBUG + --lock-wait N wait for the lock, but max. N seconds (default: 1). + --show-rc show/log the return code (rc) + --no-files-cache do not load/update the file metadata cache used to + detect unchanged files + --umask M set umask to M (local and remote, default: 0077) + --remote-path PATH set remote path to executable (default: "borg") + +Description +~~~~~~~~~~~ + +This command dumps raw (but decrypted and decompressed) repo objects to files. From 5fe6c09c3402e4e25e1a4bd27dbefcd31c33e837 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 7 Aug 2016 12:23:37 +0200 Subject: [PATCH 0072/1387] Refactor ArchiveRecreater.try_resume --- src/borg/archive.py | 83 ++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 13f596c3..2a3328be 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1532,6 +1532,51 @@ class ArchiveRecreater: """Try to resume from temporary archive. Return (target archive, resume from path) if successful.""" logger.info('Found %s, will resume interrupted operation', target_name) old_target = self.open_archive(target_name) + if not self.can_resume(archive, old_target, target_name): + return None, None + target = self.create_target_archive(target_name + '.temp') + logger.info('Replaying items from interrupted operation...') + last_old_item = self.copy_items(old_target, target) + if last_old_item: + resume_from = last_old_item.path + else: + resume_from = None + self.incref_partial_chunks(old_target, target) + old_target.delete(Statistics(), progress=self.progress) + logger.info('Done replaying items') + return target, resume_from + + def incref_partial_chunks(self, source_archive, target_archive): + target_archive.recreate_partial_chunks = source_archive.metadata.get(b'recreate_partial_chunks', []) + for chunk_id, size, csize in target_archive.recreate_partial_chunks: + if not self.cache.seen_chunk(chunk_id): + try: + # Repository has __contains__, RemoteRepository doesn't + self.repository.get(chunk_id) + except Repository.ObjectNotFound: + # delete/prune/check between invocations: these chunks are gone. + target_archive.recreate_partial_chunks = None + break + # fast-lane insert into chunks cache + self.cache.chunks[chunk_id] = (1, size, csize) + target_archive.stats.update(size, csize, True) + continue + # incref now, otherwise a source_archive.delete() might delete these chunks + self.cache.chunk_incref(chunk_id, target_archive.stats) + + def copy_items(self, source_archive, target_archive): + item = None + for item in source_archive.iter_items(): + if 'chunks' in item: + for chunk in item.chunks: + self.cache.chunk_incref(chunk.id, target_archive.stats) + target_archive.stats.nfiles += 1 + target_archive.add_item(item) + if self.progress: + source_archive.stats.show_progress(final=True) # XXX target_archive.stats? + return item + + def can_resume(self, archive, old_target, target_name): resume_id = old_target.metadata[b'recreate_source_id'] resume_args = [safe_decode(arg) for arg in old_target.metadata[b'recreate_args']] if resume_id != archive.id: @@ -1539,45 +1584,13 @@ class ArchiveRecreater: logger.warning('Saved fingerprint: %s', bin_to_hex(resume_id)) logger.warning('Current fingerprint: %s', archive.fpr) old_target.delete(Statistics(), progress=self.progress) - return None, None # can't resume + return False if resume_args != sys.argv[1:]: logger.warning('Command line changed, this might lead to inconsistencies') logger.warning('Saved: %s', repr(resume_args)) logger.warning('Current: %s', repr(sys.argv[1:])) - target = self.create_target_archive(target_name + '.temp') - logger.info('Replaying items from interrupted operation...') - item = None - for item in old_target.iter_items(): - if 'chunks' in item: - for chunk in item.chunks: - self.cache.chunk_incref(chunk.id, target.stats) - target.stats.nfiles += 1 - target.add_item(item) - if item: - resume_from = item.path - else: - resume_from = None - if self.progress: - old_target.stats.show_progress(final=True) - target.recreate_partial_chunks = old_target.metadata.get(b'recreate_partial_chunks', []) - for chunk_id, size, csize in target.recreate_partial_chunks: - if not self.cache.seen_chunk(chunk_id): - try: - # Repository has __contains__, RemoteRepository doesn't - self.repository.get(chunk_id) - except Repository.ObjectNotFound: - # delete/prune/check between invocations: these chunks are gone. - target.recreate_partial_chunks = None - break - # fast-lane insert into chunks cache - self.cache.chunks[chunk_id] = (1, size, csize) - target.stats.update(size, csize, True) - continue - # incref now, otherwise old_target.delete() might delete these chunks - self.cache.chunk_incref(chunk_id, target.stats) - old_target.delete(Statistics(), progress=self.progress) - logger.info('Done replaying items') - return target, resume_from + # Just warn in this case, don't start over + return True def create_target_archive(self, name): target = Archive(self.repository, self.key, self.manifest, name, create=True, From 7b70b74ad14d3aaf2131556ed2f6edd0c072a3da Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 7 Aug 2016 12:32:38 +0200 Subject: [PATCH 0073/1387] ArchiveRecreater.incref_partial_chunks try to use __contains__ --- src/borg/archive.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 2a3328be..a1876866 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1537,10 +1537,7 @@ class ArchiveRecreater: target = self.create_target_archive(target_name + '.temp') logger.info('Replaying items from interrupted operation...') last_old_item = self.copy_items(old_target, target) - if last_old_item: - resume_from = last_old_item.path - else: - resume_from = None + resume_from = getattr(last_old_item, 'path', None) self.incref_partial_chunks(old_target, target) old_target.delete(Statistics(), progress=self.progress) logger.info('Done replaying items') @@ -1552,7 +1549,10 @@ class ArchiveRecreater: if not self.cache.seen_chunk(chunk_id): try: # Repository has __contains__, RemoteRepository doesn't - self.repository.get(chunk_id) + # `chunk_id in repo` doesn't read the data though, so we try to use that if possible. + get_or_in = getattr(self.repository, '__contains__', self.repository.get) + if get_or_in(chunk_id) is False: + raise Repository.ObjectNotFound(chunk_id, self.repository) except Repository.ObjectNotFound: # delete/prune/check between invocations: these chunks are gone. target_archive.recreate_partial_chunks = None From 4d7a52d8e0bf7c5cd4ebb36bf1e454d70046978a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 6 Aug 2016 22:37:44 +0200 Subject: [PATCH 0074/1387] --debug-topic for granular debug logging Especially: disables file-compression logging by default --- src/borg/archive.py | 3 ++- src/borg/archiver.py | 13 +++++++++++++ src/borg/helpers.py | 4 +++- src/borg/logger.py | 5 +++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 13f596c3..9a493632 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -254,6 +254,7 @@ class Archive: self.consider_part_files = consider_part_files self.pipeline = DownloadPipeline(self.repository, self.key) if create: + self.file_compression_logger = create_logger('borg.debug.file-compression') self.items_buffer = CacheChunkBuffer(self.cache, self.key, self.stats) self.chunker = Chunker(self.key.chunk_seed, *chunker_params) self.compression_decider1 = CompressionDecider1(compression or CompressionSpec('none'), @@ -818,7 +819,7 @@ Number of files: {0.stats.nfiles}'''.format( item.chunks = chunks else: compress = self.compression_decider1.decide(path) - logger.debug('%s -> compression %s', path, compress['name']) + self.file_compression_logger.debug('%s -> compression %s', path, compress['name']) with backup_io(): fh = Archive._open_rb(path) with os.fdopen(fh, 'rb') as fd: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index aab9ff58..e107da83 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1225,6 +1225,10 @@ class Archiver: common_group.add_argument('--debug', dest='log_level', action='store_const', const='debug', default='warning', help='enable debug output, work on log level DEBUG') + common_group.add_argument('--debug-topic', dest='debug_topics', + action='append', metavar='TOPIC', default=[], + help='enable TOPIC debugging (can be specified multiple times). ' + 'The logger path is borg.debug. if TOPIC is not fully qualified.') common_group.add_argument('--lock-wait', dest='lock_wait', type=int, metavar='N', default=1, help='wait for the lock, but max. N seconds (default: %(default)d).') common_group.add_argument('--show-version', dest='show_version', action='store_true', default=False, @@ -2202,11 +2206,20 @@ class Archiver: if args.get(option, False): logging.getLogger(logger_name).setLevel('INFO') + def _setup_topic_debugging(self, args): + """Turn on DEBUG level logging for specified --debug-topics.""" + for topic in args.debug_topics: + if '.' not in topic: + topic = 'borg.debug.' + topic + logger.debug('Enabling debug topic %s', topic) + logging.getLogger(topic).setLevel('DEBUG') + def run(self, args): os.umask(args.umask) # early, before opening files self.lock_wait = args.lock_wait setup_logging(level=args.log_level, is_serve=args.func == self.do_serve) # do not use loggers before this! self._setup_implied_logging(vars(args)) + self._setup_topic_debugging(args) if args.show_version: logging.getLogger('borg.output.show-version').info('borgbackup version %s' % __version__) self.prerun_checks(logger) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index db345f41..bc0fc692 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1527,6 +1527,8 @@ class CompressionDecider1: class CompressionDecider2: + logger = create_logger('borg.debug.file-compression') + def __init__(self, compression): self.compression = compression @@ -1556,7 +1558,7 @@ class CompressionDecider2: # that marks such data as uncompressible via compression-type metadata. compr_spec = CompressionSpec('none') compr_args.update(compr_spec) - logger.debug("len(data) == %d, len(lz4(data)) == %d, choosing %s", data_len, cdata_len, compr_spec) + self.logger.debug("len(data) == %d, len(lz4(data)) == %d, choosing %s", data_len, cdata_len, compr_spec) return compr_args, Chunk(data, **meta) diff --git a/src/borg/logger.py b/src/borg/logger.py index c75aaef7..20510bb2 100644 --- a/src/borg/logger.py +++ b/src/borg/logger.py @@ -149,8 +149,13 @@ def create_logger(name=None): if not configured: raise Exception("tried to call a logger before setup_logging() was called") self.__real_logger = logging.getLogger(self.__name) + if self.__name.startswith('borg.debug.') and self.__real_logger.level == logging.NOTSET: + self.__real_logger.setLevel('WARNING') return self.__real_logger + def getChild(self, suffix): + return LazyLogger(self.__name + '.' + suffix) + def setLevel(self, *args, **kw): return self.__logger.setLevel(*args, **kw) From 389ca944077d2a44c3e77fcf15bbc2f6cab895b3 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 6 Aug 2016 22:38:53 +0200 Subject: [PATCH 0075/1387] Silence repeated "Processing files..." log entries when --no-files-cache --- src/borg/archive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 9a493632..40909080 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -795,7 +795,7 @@ Number of files: {0.stats.nfiles}'''.format( # there should be no information in the cache about special files processed in # read-special mode, but we better play safe as this was wrong in the past: path_hash = ids = None - first_run = not cache.files + first_run = not cache.files and cache.do_files if first_run: logger.debug('Processing files ...') chunks = None From bec5051ea589d7e2865228bddd318cee6912e925 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 6 Aug 2016 22:41:24 +0200 Subject: [PATCH 0076/1387] helpers.format_timedelta: use timedelta.total_seconds() --- src/borg/helpers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index bc0fc692..67191967 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -597,8 +597,7 @@ def format_time(t): def format_timedelta(td): """Format timedelta in a human friendly format """ - # Since td.total_seconds() requires python 2.7 - ts = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / float(10 ** 6) + ts = td.total_seconds() s = ts % 60 m = int(ts / 60) % 60 h = int(ts / 3600) % 24 From 0a65e83df33dadd3c78dc9ebdaa8cef01bb5139e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 7 Aug 2016 12:10:20 +0200 Subject: [PATCH 0077/1387] check --verify-data: remove incorrect help paragraph (since d0ec7e76bb - doesn't mean this probably useful feature may not be reimplemented, on the contrary) --- src/borg/archiver.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e107da83..fc1da16c 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1368,10 +1368,6 @@ class Archiver: tamper-resistant as well, unless the attacker has access to the keys. It is also very slow. - - --verify-data only verifies data used by the archives specified with --last, - --prefix or an explicitly named archive. If none of these are passed, - all data in the repository is verified. """) subparser = subparsers.add_parser('check', parents=[common_parser], add_help=False, description=self.do_check.__doc__, From d3000a7e5de952ed5096ccb6c46f0211fde93754 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 9 Aug 2016 00:33:12 +0200 Subject: [PATCH 0078/1387] LZ4: dynamically enlarge the (de)compression buffer, fixes #1453 the statically allocated COMPR_BUFFER was right size for chunks, but not for the archive item which could get larger if you have many millions of files/dirs. --- borg/archiver.py | 6 ++-- borg/compress.pyx | 60 ++++++++++++++++++++------------------ borg/helpers.py | 2 -- borg/key.py | 4 +-- borg/testsuite/compress.py | 29 +++++++++++------- 5 files changed, 54 insertions(+), 47 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index bfd56bf0..41373e25 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -25,7 +25,7 @@ from .helpers import Error, location_validator, archivename_validator, format_li EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, log_multi, PatternMatcher, ErrorIgnoringTextIOWrapper from .logger import create_logger, setup_logging logger = create_logger() -from .compress import Compressor, COMPR_BUFFER +from .compress import Compressor from .upgrader import AtticRepositoryUpgrader, BorgRepositoryUpgrader from .repository import Repository from .cache import Cache @@ -240,9 +240,7 @@ class Archiver: dry_run = args.dry_run t0 = datetime.utcnow() if not dry_run: - compr_args = dict(buffer=COMPR_BUFFER) - compr_args.update(args.compression) - key.compressor = Compressor(**compr_args) + key.compressor = Compressor(**args.compression) with Cache(repository, key, manifest, do_files=args.cache_files, lock_wait=self.lock_wait) as cache: archive = Archive(repository, key, manifest, args.location.archive, cache=cache, create=True, checkpoint_interval=args.checkpoint_interval, diff --git a/borg/compress.pyx b/borg/compress.pyx index 3bb88def..1330fbf2 100644 --- a/borg/compress.pyx +++ b/borg/compress.pyx @@ -7,6 +7,7 @@ except ImportError: cdef extern from "lz4.h": int LZ4_compress_limitedOutput(const char* source, char* dest, int inputSize, int maxOutputSize) nogil int LZ4_decompress_safe(const char* source, char* dest, int inputSize, int maxOutputSize) nogil + int LZ4_compressBound(int inputSize) nogil cdef class CompressorBase: @@ -52,40 +53,35 @@ class CNONE(CompressorBase): return data -cdef class LZ4(CompressorBase): +class LZ4(CompressorBase): """ raw LZ4 compression / decompression (liblz4). Features: - lz4 is super fast - wrapper releases CPython's GIL to support multithreaded code - - buffer given by caller, avoiding frequent reallocation and buffer duplication - uses safe lz4 methods that never go beyond the end of the output buffer - - But beware: - - this is not very generic, the given buffer MUST be large enough to - handle all compression or decompression output (or it will fail). - - you must not do method calls to the same LZ4 instance from different - threads at the same time - create one LZ4 instance per thread! """ ID = b'\x01\x00' name = 'lz4' - cdef char *buffer # helper buffer for (de)compression output - cdef int bufsize # size of this buffer + def __init__(self, **kwargs): + self.buffer = None - def __cinit__(self, **kwargs): - buffer = kwargs['buffer'] - self.buffer = buffer - self.bufsize = len(buffer) + def _create_buffer(self, size): + # we keep a reference to the buffer until this instance is destroyed + self.buffer = bytes(int(size)) def compress(self, idata): if not isinstance(idata, bytes): idata = bytes(idata) # code below does not work with memoryview cdef int isize = len(idata) - cdef int osize = self.bufsize + cdef int osize cdef char *source = idata - cdef char *dest = self.buffer + cdef char *dest + osize = LZ4_compressBound(isize) + self._create_buffer(osize) + dest = self.buffer with nogil: osize = LZ4_compress_limitedOutput(source, dest, isize, osize) if not osize: @@ -97,15 +93,26 @@ cdef class LZ4(CompressorBase): idata = bytes(idata) # code below does not work with memoryview idata = super().decompress(idata) cdef int isize = len(idata) - cdef int osize = self.bufsize + cdef int osize + cdef int rsize cdef char *source = idata - cdef char *dest = self.buffer - with nogil: - osize = LZ4_decompress_safe(source, dest, isize, osize) - if osize < 0: - # malformed input data, buffer too small, ... - raise Exception('lz4 decompress failed') - return dest[:osize] + cdef char *dest + # a bit more than 8MB is enough for the usual data sizes yielded by the chunker. + # allocate more if isize * 3 is already bigger, to avoid having to resize often. + osize = max(int(1.1 * 2**23), isize * 3) + while True: + self._create_buffer(osize) + dest = self.buffer + with nogil: + rsize = LZ4_decompress_safe(source, dest, isize, osize) + if rsize >= 0: + break + if osize > 2 ** 30: + # this is insane, get out of here + raise Exception('lz4 decompress failed') + # likely the buffer was too small, get a bigger one: + osize = int(1.5 * osize) + return dest[:rsize] class LZMA(CompressorBase): @@ -192,8 +199,3 @@ class Compressor: return cls(**self.params).decompress(data) else: raise ValueError('No decompressor for this data found: %r.', data[:2]) - - -# a buffer used for (de)compression result, which can be slightly bigger -# than the chunk buffer in the worst (incompressible data) case, add 10%: -COMPR_BUFFER = bytes(int(1.1 * 2 ** 23)) # CHUNK_MAX_EXP == 23 diff --git a/borg/helpers.py b/borg/helpers.py index bacb434b..4275d783 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -492,8 +492,6 @@ def timestamp(s): def ChunkerParams(s): chunk_min, chunk_max, chunk_mask, window_size = s.split(',') if int(chunk_max) > 23: - # do not go beyond 2**23 (8MB) chunk size now, - # COMPR_BUFFER can only cope with up to this size raise ValueError('max. chunk size exponent must not be more than 23 (2^23 = 8MiB max. chunk size)') return int(chunk_min), int(chunk_max), int(chunk_mask), int(window_size) diff --git a/borg/key.py b/borg/key.py index be79dfc1..95178f7c 100644 --- a/borg/key.py +++ b/borg/key.py @@ -12,7 +12,7 @@ from .logger import create_logger logger = create_logger() from .crypto import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks -from .compress import Compressor, COMPR_BUFFER +from .compress import Compressor import msgpack PREFIX = b'\0' * 8 @@ -70,7 +70,7 @@ class KeyBase: self.TYPE_STR = bytes([self.TYPE]) self.repository = repository self.target = None # key location file path / repo obj - self.compressor = Compressor('none', buffer=COMPR_BUFFER) + self.compressor = Compressor('none') def id_hash(self, data): """Return HMAC hash using the "id" HMAC key diff --git a/borg/testsuite/compress.py b/borg/testsuite/compress.py index 1a435358..ff9d4271 100644 --- a/borg/testsuite/compress.py +++ b/borg/testsuite/compress.py @@ -1,3 +1,4 @@ +import os import zlib try: import lzma @@ -11,13 +12,13 @@ from ..compress import get_compressor, Compressor, CNONE, ZLIB, LZ4 buffer = bytes(2**16) data = b'fooooooooobaaaaaaaar' * 10 -params = dict(name='zlib', level=6, buffer=buffer) +params = dict(name='zlib', level=6) def test_get_compressor(): c = get_compressor(name='none') assert isinstance(c, CNONE) - c = get_compressor(name='lz4', buffer=buffer) + c = get_compressor(name='lz4') assert isinstance(c, LZ4) c = get_compressor(name='zlib') assert isinstance(c, ZLIB) @@ -35,13 +36,21 @@ def test_cnull(): def test_lz4(): - c = get_compressor(name='lz4', buffer=buffer) + c = get_compressor(name='lz4') cdata = c.compress(data) assert len(cdata) < len(data) assert data == c.decompress(cdata) assert data == Compressor(**params).decompress(cdata) # autodetect +def test_lz4_buffer_allocation(): + # test with a rather huge data object to see if buffer allocation / resizing works + data = os.urandom(50 * 2**20) # 50MiB incompressible data + c = get_compressor(name='lz4') + cdata = c.compress(data) + assert data == c.decompress(cdata) + + def test_zlib(): c = get_compressor(name='zlib') cdata = c.compress(data) @@ -83,16 +92,16 @@ def test_zlib_compat(): def test_compressor(): params_list = [ - dict(name='none', buffer=buffer), - dict(name='lz4', buffer=buffer), - dict(name='zlib', level=0, buffer=buffer), - dict(name='zlib', level=6, buffer=buffer), - dict(name='zlib', level=9, buffer=buffer), + dict(name='none'), + dict(name='lz4'), + dict(name='zlib', level=0), + dict(name='zlib', level=6), + dict(name='zlib', level=9), ] if lzma: params_list += [ - dict(name='lzma', level=0, buffer=buffer), - dict(name='lzma', level=6, buffer=buffer), + dict(name='lzma', level=0), + dict(name='lzma', level=6), # we do not test lzma on level 9 because of the huge memory needs ] for params in params_list: From b0e7bb5ddc41c71103cc83fbcf5b452133bb700e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 9 Aug 2016 17:05:24 +0200 Subject: [PATCH 0079/1387] fixup: use thread-local buffer start with 0 bytes length (saves memory in case lz4 is not used). always grow when a bigger buffer is needed. avoid per-call reallocation / freeing / garbage. --- borg/compress.pyx | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/borg/compress.pyx b/borg/compress.pyx index 1330fbf2..13955a86 100644 --- a/borg/compress.pyx +++ b/borg/compress.pyx @@ -1,3 +1,4 @@ +import threading import zlib try: import lzma @@ -10,6 +11,17 @@ cdef extern from "lz4.h": int LZ4_compressBound(int inputSize) nogil +thread_local = threading.local() +thread_local.buffer = bytes() + + +cdef char *get_buffer(size): + size = int(size) + if len(thread_local.buffer) < size: + thread_local.buffer = bytes(size) + return thread_local.buffer + + cdef class CompressorBase: """ base class for all (de)compression classes, @@ -66,11 +78,7 @@ class LZ4(CompressorBase): name = 'lz4' def __init__(self, **kwargs): - self.buffer = None - - def _create_buffer(self, size): - # we keep a reference to the buffer until this instance is destroyed - self.buffer = bytes(int(size)) + pass def compress(self, idata): if not isinstance(idata, bytes): @@ -80,8 +88,7 @@ class LZ4(CompressorBase): cdef char *source = idata cdef char *dest osize = LZ4_compressBound(isize) - self._create_buffer(osize) - dest = self.buffer + dest = get_buffer(osize) with nogil: osize = LZ4_compress_limitedOutput(source, dest, isize, osize) if not osize: @@ -101,8 +108,7 @@ class LZ4(CompressorBase): # allocate more if isize * 3 is already bigger, to avoid having to resize often. osize = max(int(1.1 * 2**23), isize * 3) while True: - self._create_buffer(osize) - dest = self.buffer + dest = get_buffer(osize) with nogil: rsize = LZ4_decompress_safe(source, dest, isize, osize) if rsize >= 0: From fc92822b6c5771ae56afd9472017d5aa7efeb95d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 9 Aug 2016 17:35:27 +0200 Subject: [PATCH 0080/1387] explain the confusing TypeError, fixes #1456 --- borg/remote.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/borg/remote.py b/borg/remote.py index 47a20412..3dda2413 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -185,6 +185,16 @@ class RemoteRepository: except self.RPCError as err: if err.remote_type != 'TypeError': raise + msg = """\ +Please note: +If you see a TypeError complaining about the number of positional arguments +given to open(), you can ignore it if it comes from a borg version < 1.0.7. +This TypeError is a cosmetic side effect of the compatibility code borg +clients >= 1.0.7 have to support older borg servers. +This problem will go away as soon as the server has been upgraded to 1.0.7+. +""" + # emit this msg in the same way as the "Remote: ..." lines that show the remote TypeError + sys.stderr.write(msg) if append_only: raise self.NoAppendOnlyOnServer() self.id = self.call('open', self.location.path, create, lock_wait, lock) From a360307938103e3dbd58b38b18c08df009f01ab4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 8 Aug 2016 21:45:53 +0200 Subject: [PATCH 0081/1387] repo: do not put objects that we won't get, fixes #1451 we will not get() objects that have a segment entry larger than MAX_OBJECT_SIZE. thus we should never produce such entries. also: introduce repository.MAX_DATA_SIZE that gives the max payload size. --- borg/repository.py | 9 ++++++++- borg/testsuite/repository.py | 9 ++++++++- docs/changes.rst | 8 ++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/borg/repository.py b/borg/repository.py index 66c0f638..87bb4b16 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -731,8 +731,12 @@ class LoggedIO: return size, tag, key, data def write_put(self, id, data, raise_full=False): + data_size = len(data) + if data_size > MAX_DATA_SIZE: + # this would push the segment entry size beyond MAX_OBJECT_SIZE. + raise IntegrityError('More than allowed put data [{} > {}]'.format(data_size, MAX_DATA_SIZE)) fd = self.get_write_fd(raise_full=raise_full) - size = len(data) + self.put_header_fmt.size + size = data_size + self.put_header_fmt.size offset = self.offset header = self.header_no_crc_fmt.pack(size, TAG_PUT) crc = self.crc_fmt.pack(crc32(data, crc32(id, crc32(header))) & 0xffffffff) @@ -771,3 +775,6 @@ class LoggedIO: self._write_fd.close() sync_dir(os.path.dirname(self._write_fd.name)) self._write_fd = None + + +MAX_DATA_SIZE = MAX_OBJECT_SIZE - LoggedIO.put_header_fmt.size diff --git a/borg/testsuite/repository.py b/borg/testsuite/repository.py index bc08e097..c50e785b 100644 --- a/borg/testsuite/repository.py +++ b/borg/testsuite/repository.py @@ -8,7 +8,7 @@ from ..hashindex import NSIndex from ..helpers import Location, IntegrityError from ..locking import Lock, LockFailed from ..remote import RemoteRepository, InvalidRPCMethod -from ..repository import Repository, LoggedIO, TAG_COMMIT +from ..repository import Repository, LoggedIO, TAG_COMMIT, MAX_DATA_SIZE from . import BaseTestCase @@ -128,6 +128,13 @@ class RepositoryTestCase(RepositoryTestCaseBase): self.assert_equal(second_half, all[50:]) self.assert_equal(len(self.repository.list(limit=50)), 50) + def test_max_data_size(self): + max_data = b'x' * MAX_DATA_SIZE + self.repository.put(b'00000000000000000000000000000000', max_data) + self.assert_equal(self.repository.get(b'00000000000000000000000000000000'), max_data) + self.assert_raises(IntegrityError, + lambda: self.repository.put(b'00000000000000000000000000000001', max_data + b'x')) + class RepositoryCommitTestCase(RepositoryTestCaseBase): diff --git a/docs/changes.rst b/docs/changes.rst index ddfdb8f4..305be063 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -57,6 +57,14 @@ Security fixes: - fix security issue with remote repository access, #1428 +Bug fixes: + +- do not write objects to repository that are bigger than the allowed size, + borg will reject reading them, #1451. + IMPORTANT: if you created archives with many millions of files or + directories, please verify if you can open them successfully, + e.g. try a "borg list REPO::ARCHIVE". + Version 1.0.7rc1 (2016-08-05) ----------------------------- From 20392f8dd960ca23cca17f52ca481b1c9ea4e514 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 8 Aug 2016 22:00:34 +0200 Subject: [PATCH 0082/1387] repo: split size check into too small and too big also add a hint if somebody needs to restore an archive that has too big objects. --- borg/repository.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/borg/repository.py b/borg/repository.py index 87bb4b16..686f30a7 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -712,9 +712,14 @@ class LoggedIO: key = None else: raise TypeError("_read called with unsupported format") - if size > MAX_OBJECT_SIZE or size < fmt.size: - raise IntegrityError('Invalid segment entry size [segment {}, offset {}]'.format( - segment, offset)) + if size > MAX_OBJECT_SIZE: + # if you get this on an archive made with borg < 1.0.7 and millions of files and + # you need to restore it, you can disable this check by using "if False:" above. + raise IntegrityError('Invalid segment entry size {} - too big [segment {}, offset {}]'.format( + size, segment, offset)) + if size < fmt.size: + raise IntegrityError('Invalid segment entry size {} - too small [segment {}, offset {}]'.format( + size, segment, offset)) length = size - fmt.size data = fd.read(length) if len(data) != length: From 2624d6f81858edef821363526b15614960cba736 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 9 Aug 2016 20:30:50 +0200 Subject: [PATCH 0083/1387] larger item metadata stream chunks, fixes #1452 increasing the mask (target chunk size) from 14 (16kiB) to 17 (128kiB). this should reduce the amount of item metadata chunks an archive has to reference to 1/8. this does not completely fix #1452, but at least enables a 8x larger item metadata stream. --- borg/archive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/borg/archive.py b/borg/archive.py index 653e2dd1..4498c72a 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -39,7 +39,7 @@ HASH_MASK_BITS = 21 # results in ~2MiB chunks statistically CHUNKER_PARAMS = (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) # chunker params for the items metadata stream, finer granularity -ITEMS_CHUNKER_PARAMS = (12, 16, 14, HASH_WINDOW_SIZE) +ITEMS_CHUNKER_PARAMS = (15, 19, 17, HASH_WINDOW_SIZE) has_lchmod = hasattr(os, 'lchmod') has_lchflags = hasattr(os, 'lchflags') From 7d9fba5a4118d2aec8c25712600a2d0c2bd3f512 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 9 Aug 2016 21:20:13 +0200 Subject: [PATCH 0084/1387] ArchiveRecreater.copy_items: progress from target instead of source (it produces the same output, just less confusing) --- src/borg/archive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index a1876866..a3eb409f 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1573,7 +1573,7 @@ class ArchiveRecreater: target_archive.stats.nfiles += 1 target_archive.add_item(item) if self.progress: - source_archive.stats.show_progress(final=True) # XXX target_archive.stats? + target_archive.stats.show_progress(final=True) return item def can_resume(self, archive, old_target, target_name): From 7b5772df2d68b544ac90bf670efcf24ebdf7a6c9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 10 Aug 2016 13:56:06 +0200 Subject: [PATCH 0085/1387] add transaction_id assertion an acd_cli (amazon cloud drive fuse filesystem) user had "borg init" crash in the line below. by adding the assertion we tell that we do not expect the transaction_id to be None there, so it is easier to differentiate from a random coding error. --- borg/repository.py | 1 + 1 file changed, 1 insertion(+) diff --git a/borg/repository.py b/borg/repository.py index 686f30a7..c33f49da 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -248,6 +248,7 @@ class Repository: b'segments': self.segments, b'compact': list(self.compact)} transaction_id = self.io.get_segments_transaction_id() + assert transaction_id is not None hints_file = os.path.join(self.path, 'hints.%d' % transaction_id) with open(hints_file + '.tmp', 'wb') as fd: msgpack.pack(hints, fd) From 6e658a5a6cc41f0eac8c71e1ba28e9429cf8508e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 10 Aug 2016 15:45:57 +0200 Subject: [PATCH 0086/1387] docs: improve prune examples --- docs/usage.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 82da1f97..ab92c1cb 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -423,8 +423,8 @@ you restrict its operation to a subset of the archives using ``--prefix``. When using ``--prefix``, be careful to choose a good prefix - e.g. do not use a prefix "foo" if you do not also want to match "foobar". -It is strongly recommended to always run ``prune --dry-run ...`` first so you -will see what it would do without it actually doing anything. +It is strongly recommended to always run ``prune -v --list --dry-run ...`` +first so you will see what it would do without it actually doing anything. There is also a visualized prune example in ``docs/misc/prune-example.txt``. @@ -432,19 +432,19 @@ There is also a visualized prune example in ``docs/misc/prune-example.txt``. # Keep 7 end of day and 4 additional end of week archives. # Do a dry-run without actually deleting anything. - $ borg prune --dry-run --keep-daily=7 --keep-weekly=4 /path/to/repo + $ borg prune -v --list --dry-run --keep-daily=7 --keep-weekly=4 /path/to/repo # Same as above but only apply to archive names starting with the hostname # of the machine followed by a "-" character: - $ borg prune --keep-daily=7 --keep-weekly=4 --prefix='{hostname}-' /path/to/repo + $ borg prune -v --list --keep-daily=7 --keep-weekly=4 --prefix='{hostname}-' /path/to/repo # Keep 7 end of day, 4 additional end of week archives, # and an end of month archive for every month: - $ borg prune --keep-daily=7 --keep-weekly=4 --keep-monthly=-1 /path/to/repo + $ borg prune -v --list --keep-daily=7 --keep-weekly=4 --keep-monthly=-1 /path/to/repo # Keep all backups in the last 10 days, 4 additional end of week archives, # and an end of month archive for every month: - $ borg prune --keep-within=10d --keep-weekly=4 --keep-monthly=-1 /path/to/repo + $ borg prune -v --list --keep-within=10d --keep-weekly=4 --keep-monthly=-1 /path/to/repo .. include:: usage/info.rst.inc From c61a9e8aa0ce34e07ea738f1ef42c4c4e2812cf7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 12 Aug 2016 13:00:53 +0200 Subject: [PATCH 0087/1387] Print active env var override by default Fixes #1467 --- borg/helpers.py | 6 +++--- borg/testsuite/helpers.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/borg/helpers.py b/borg/helpers.py index 4275d783..975ddca8 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -912,7 +912,7 @@ DEFAULTISH = ('Default', 'DEFAULT', 'default', 'D', 'd', '', ) def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, - retry_msg=None, invalid_msg=None, env_msg=None, + retry_msg=None, invalid_msg=None, env_msg='{} (from {})', falsish=FALSISH, truish=TRUISH, defaultish=DEFAULTISH, default=False, retry=True, env_var_override=None, ofile=None, input=input): """Output (usually a question) and let user input an answer. @@ -933,8 +933,8 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, :param true_msg: message to output before returning True [None] :param default_msg: message to output before returning a [None] :param invalid_msg: message to output after a invalid answer was given [None] - :param env_msg: message to output when using input from env_var_override [None], - needs to have 2 placeholders for answer and env var name, e.g.: "{} (from {})" + :param env_msg: message to output when using input from env_var_override ['{} (from {})'], + needs to have 2 placeholders for answer and env var name :param falsish: sequence of answers qualifying as False :param truish: sequence of answers qualifying as True :param defaultish: sequence of answers qualifying as diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py index 35993ef1..9e03019d 100644 --- a/borg/testsuite/helpers.py +++ b/borg/testsuite/helpers.py @@ -805,6 +805,16 @@ def test_yes_output(capfd): assert 'false-msg' in err +def test_yes_env_output(capfd, monkeypatch): + env_var = 'OVERRIDE_SOMETHING' + monkeypatch.setenv(env_var, 'yes') + assert yes(env_var_override=env_var) + out, err = capfd.readouterr() + assert out == '' + assert env_var in err + assert 'yes' in err + + def test_progress_percentage_multiline(capfd): pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=False, msg="%3.0f%%", file=sys.stderr) pi.show(0) From 17c77a5dc51064dfbd4a0ec6129a911ab59279a3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 11 Aug 2016 15:30:50 +0200 Subject: [PATCH 0088/1387] xattr: dynamically grow result buffer until it fits, fixes #1462 this also fixes the race condition seen in #1462 because there is only 1 call now. either it succeeds, then we get the correct length as result and truncate the result value to that length. or it fails with ERANGE, then we grow the buffer to double size and repeat. or it fails with some other error, then we throw OSError. --- borg/testsuite/xattr.py | 22 ++++++++++++- borg/xattr.py | 68 ++++++++++++++++++++++++++++++----------- 2 files changed, 72 insertions(+), 18 deletions(-) diff --git a/borg/testsuite/xattr.py b/borg/testsuite/xattr.py index df0130c9..ecc92fa1 100644 --- a/borg/testsuite/xattr.py +++ b/borg/testsuite/xattr.py @@ -2,7 +2,7 @@ import os import tempfile import unittest -from ..xattr import is_enabled, getxattr, setxattr, listxattr +from ..xattr import is_enabled, getxattr, setxattr, listxattr, get_buffer from . import BaseTestCase @@ -38,3 +38,23 @@ class XattrTestCase(BaseTestCase): self.assert_equal(getxattr(self.tmpfile.fileno(), 'user.foo'), b'bar') self.assert_equal(getxattr(self.symlink, 'user.foo'), b'bar') self.assert_equal(getxattr(self.tmpfile.name, 'user.empty'), None) + + def test_listxattr_buffer_growth(self): + # make it work even with ext4, which imposes rather low limits + get_buffer(size=64, init=True) + # xattr raw key list will be size 9 * (10 + 1), which is > 64 + keys = ['user.attr%d' % i for i in range(9)] + for key in keys: + setxattr(self.tmpfile.name, key, b'x') + got_keys = listxattr(self.tmpfile.name) + self.assert_equal_se(got_keys, keys) + self.assert_equal(len(get_buffer()), 128) + + def test_getxattr_buffer_growth(self): + # make it work even with ext4, which imposes rather low limits + get_buffer(size=64, init=True) + value = b'x' * 126 + setxattr(self.tmpfile.name, 'user.big', value) + got_value = getxattr(self.tmpfile.name, 'user.big') + self.assert_equal(value, got_value) + self.assert_equal(len(get_buffer()), 128) diff --git a/borg/xattr.py b/borg/xattr.py index e88d7ce8..146f3ae7 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -6,6 +6,7 @@ import re import subprocess import sys import tempfile +import threading from ctypes import CDLL, create_string_buffer, c_ssize_t, c_size_t, c_char_p, c_int, c_uint32, get_errno from ctypes.util import find_library from distutils.version import LooseVersion @@ -14,6 +15,18 @@ from .logger import create_logger logger = create_logger() +def get_buffer(size=None, init=False): + if size is not None: + size = int(size) + if init or len(thread_local.buffer) < size: + thread_local.buffer = create_string_buffer(size) + return thread_local.buffer + + +thread_local = threading.local() +get_buffer(size=4096, init=True) + + def is_enabled(path=None): """Determine if xattr is enabled on the filesystem """ @@ -78,9 +91,17 @@ except OSError as e: raise Exception(msg) +class BufferTooSmallError(Exception): + """the buffer given to an xattr function was too small for the result""" + + def _check(rv, path=None): if rv < 0: - raise OSError(get_errno(), path) + e = get_errno() + if e == errno.ERANGE: + raise BufferTooSmallError + else: + raise OSError(e, path) return rv if sys.platform.startswith('linux'): # pragma: linux only @@ -106,14 +127,20 @@ if sys.platform.startswith('linux'): # pragma: linux only func = libc.listxattr else: func = libc.llistxattr - n = _check(func(path, None, 0), path) - if n == 0: - return [] - namebuf = create_string_buffer(n) - n2 = _check(func(path, namebuf, n), path) - if n2 != n: - raise Exception('listxattr failed') - return [os.fsdecode(name) for name in namebuf.raw.split(b'\0')[:-1] if not name.startswith(b'system.posix_acl_')] + size = len(get_buffer()) + while True: + buf = get_buffer(size) + try: + n = _check(func(path, buf, size), path) + except BufferTooSmallError: + size *= 2 + else: + if n == 0: + return [] + names = buf.raw[:n] + return [os.fsdecode(name) + for name in names.split(b'\0')[:-1] + if not name.startswith(b'system.posix_acl_')] def getxattr(path, name, *, follow_symlinks=True): name = os.fsencode(name) @@ -125,14 +152,17 @@ if sys.platform.startswith('linux'): # pragma: linux only func = libc.getxattr else: func = libc.lgetxattr - n = _check(func(path, name, None, 0)) - if n == 0: - return - valuebuf = create_string_buffer(n) - n2 = _check(func(path, name, valuebuf, n), path) - if n2 != n: - raise Exception('getxattr failed') - return valuebuf.raw + size = len(get_buffer()) + while True: + buf = get_buffer(size) + try: + n = _check(func(path, name, buf, size), path) + except BufferTooSmallError: + size *= 2 + else: + if n == 0: + return + return buf.raw[:n] def setxattr(path, name, value, *, follow_symlinks=True): name = os.fsencode(name) @@ -172,6 +202,7 @@ elif sys.platform == 'darwin': # pragma: darwin only func = libc.flistxattr elif not follow_symlinks: flags = XATTR_NOFOLLOW + # TODO: fix, see linux n = _check(func(path, None, 0, flags), path) if n == 0: return [] @@ -191,6 +222,7 @@ elif sys.platform == 'darwin': # pragma: darwin only func = libc.fgetxattr elif not follow_symlinks: flags = XATTR_NOFOLLOW + # TODO: fix, see linux n = _check(func(path, name, None, 0, 0, flags)) if n == 0: return @@ -244,6 +276,7 @@ elif sys.platform.startswith('freebsd'): # pragma: freebsd only func = libc.extattr_list_file else: func = libc.extattr_list_link + # TODO: fix, see linux n = _check(func(path, ns, None, 0), path) if n == 0: return [] @@ -269,6 +302,7 @@ elif sys.platform.startswith('freebsd'): # pragma: freebsd only func = libc.extattr_get_file else: func = libc.extattr_get_link + # TODO: fix, see linux n = _check(func(path, EXTATTR_NAMESPACE_USER, name, None, 0)) if n == 0: return From 67c6c1898cbaf0daf591a8c00749b068b225ac56 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 11 Aug 2016 19:47:04 +0200 Subject: [PATCH 0089/1387] xattr: refactor code, deduplicate this code would be otherwise duplicated 3 times for linux, freebsd, darwin. --- borg/xattr.py | 282 ++++++++++++++++++++++++++------------------------ 1 file changed, 145 insertions(+), 137 deletions(-) diff --git a/borg/xattr.py b/borg/xattr.py index 146f3ae7..94df6b05 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -46,6 +46,7 @@ def get_all(path, follow_symlinks=True): if e.errno in (errno.ENOTSUP, errno.EPERM): return {} + libc_name = find_library('c') if libc_name is None: # find_library didn't work, maybe we are on some minimal system that misses essential @@ -104,6 +105,46 @@ def _check(rv, path=None): raise OSError(e, path) return rv + +def _listxattr_inner(func, path): + if isinstance(path, str): + path = os.fsencode(path) + size = len(get_buffer()) + while True: + buf = get_buffer(size) + try: + n = _check(func(path, buf, size), path) + except BufferTooSmallError: + size *= 2 + assert size < 2 ** 20 + else: + return n, buf.raw + + +def _getxattr_inner(func, path, name): + if isinstance(path, str): + path = os.fsencode(path) + name = os.fsencode(name) + size = len(get_buffer()) + while True: + buf = get_buffer(size) + try: + n = _check(func(path, name, buf, size), path) + except BufferTooSmallError: + size *= 2 + else: + return n, buf.raw + + +def _setxattr_inner(func, path, name, value): + if isinstance(path, str): + path = os.fsencode(path) + name = os.fsencode(name) + value = value and os.fsencode(value) + size = len(value) if value else 0 + _check(func(path, name, value, size), path) + + if sys.platform.startswith('linux'): # pragma: linux only libc.llistxattr.argtypes = (c_char_p, c_char_p, c_size_t) libc.llistxattr.restype = c_ssize_t @@ -119,63 +160,49 @@ if sys.platform.startswith('linux'): # pragma: linux only libc.fgetxattr.restype = c_ssize_t def listxattr(path, *, follow_symlinks=True): - if isinstance(path, str): - path = os.fsencode(path) - if isinstance(path, int): - func = libc.flistxattr - elif follow_symlinks: - func = libc.listxattr - else: - func = libc.llistxattr - size = len(get_buffer()) - while True: - buf = get_buffer(size) - try: - n = _check(func(path, buf, size), path) - except BufferTooSmallError: - size *= 2 + def func(path, buf, size): + if isinstance(path, int): + return libc.flistxattr(path, buf, size) else: - if n == 0: - return [] - names = buf.raw[:n] - return [os.fsdecode(name) - for name in names.split(b'\0')[:-1] - if not name.startswith(b'system.posix_acl_')] + if follow_symlinks: + return libc.listxattr(path, buf, size) + else: + return libc.llistxattr(path, buf, size) + + n, buf = _listxattr_inner(func, path) + if n == 0: + return [] + names = buf[:n].split(b'\0')[:-1] + return [os.fsdecode(name) for name in names + if not name.startswith(b'system.posix_acl_')] def getxattr(path, name, *, follow_symlinks=True): - name = os.fsencode(name) - if isinstance(path, str): - path = os.fsencode(path) - if isinstance(path, int): - func = libc.fgetxattr - elif follow_symlinks: - func = libc.getxattr - else: - func = libc.lgetxattr - size = len(get_buffer()) - while True: - buf = get_buffer(size) - try: - n = _check(func(path, name, buf, size), path) - except BufferTooSmallError: - size *= 2 + def func(path, name, buf, size): + if isinstance(path, int): + return libc.fgetxattr(path, name, buf, size) else: - if n == 0: - return - return buf.raw[:n] + if follow_symlinks: + return libc.getxattr(path, name, buf, size) + else: + return libc.lgetxattr(path, name, buf, size) + + n, buf = _getxattr_inner(func, path, name) + if n == 0: + return + return buf[:n] def setxattr(path, name, value, *, follow_symlinks=True): - name = os.fsencode(name) - value = value and os.fsencode(value) - if isinstance(path, str): - path = os.fsencode(path) - if isinstance(path, int): - func = libc.fsetxattr - elif follow_symlinks: - func = libc.setxattr - else: - func = libc.lsetxattr - _check(func(path, name, value, len(value) if value else 0, 0), path) + def func(path, name, value, size): + flags = 0 + if isinstance(path, int): + return libc.fsetxattr(path, name, value, size, flags) + else: + if follow_symlinks: + return libc.setxattr(path, name, value, size, flags) + else: + return libc.lsetxattr(path, name, value, size, flags) + + _setxattr_inner(func, path, name, value) elif sys.platform == 'darwin': # pragma: darwin only libc.listxattr.argtypes = (c_char_p, c_char_p, c_size_t, c_int) @@ -191,62 +218,53 @@ elif sys.platform == 'darwin': # pragma: darwin only libc.fgetxattr.argtypes = (c_int, c_char_p, c_char_p, c_size_t, c_uint32, c_int) libc.fgetxattr.restype = c_ssize_t + XATTR_NOFLAGS = 0x0000 XATTR_NOFOLLOW = 0x0001 def listxattr(path, *, follow_symlinks=True): - func = libc.listxattr - flags = 0 - if isinstance(path, str): - path = os.fsencode(path) - if isinstance(path, int): - func = libc.flistxattr - elif not follow_symlinks: - flags = XATTR_NOFOLLOW - # TODO: fix, see linux - n = _check(func(path, None, 0, flags), path) + def func(path, buf, size): + if isinstance(path, int): + return libc.flistxattr(path, buf, size, XATTR_NOFLAGS) + else: + if follow_symlinks: + return libc.listxattr(path, buf, size, XATTR_NOFLAGS) + else: + return libc.listxattr(path, buf, size, XATTR_NOFOLLOW) + + n, buf = _listxattr_inner(func, path) if n == 0: return [] - namebuf = create_string_buffer(n) - n2 = _check(func(path, namebuf, n, flags), path) - if n2 != n: - raise Exception('listxattr failed') - return [os.fsdecode(name) for name in namebuf.raw.split(b'\0')[:-1]] + names = buf[:n].split(b'\0')[:-1] + return [os.fsdecode(name) for name in names] def getxattr(path, name, *, follow_symlinks=True): - name = os.fsencode(name) - func = libc.getxattr - flags = 0 - if isinstance(path, str): - path = os.fsencode(path) - if isinstance(path, int): - func = libc.fgetxattr - elif not follow_symlinks: - flags = XATTR_NOFOLLOW - # TODO: fix, see linux - n = _check(func(path, name, None, 0, 0, flags)) + def func(path, name, buf, size): + if isinstance(path, int): + return libc.fgetxattr(path, name, buf, size, 0, XATTR_NOFLAGS) + else: + if follow_symlinks: + return libc.getxattr(path, name, buf, size, 0, XATTR_NOFLAGS) + else: + return libc.getxattr(path, name, buf, size, 0, XATTR_NOFOLLOW) + + n, buf = _getxattr_inner(func, path, name) if n == 0: return - valuebuf = create_string_buffer(n) - n2 = _check(func(path, name, valuebuf, n, 0, flags), path) - if n2 != n: - raise Exception('getxattr failed') - return valuebuf.raw + return buf[:n] def setxattr(path, name, value, *, follow_symlinks=True): - name = os.fsencode(name) - value = value and os.fsencode(value) - func = libc.setxattr - flags = 0 - if isinstance(path, str): - path = os.fsencode(path) - if isinstance(path, int): - func = libc.fsetxattr - elif not follow_symlinks: - flags = XATTR_NOFOLLOW - _check(func(path, name, value, len(value) if value else 0, 0, flags), path) + def func(path, name, value, size): + if isinstance(path, int): + return libc.fsetxattr(path, name, value, size, 0, XATTR_NOFLAGS) + else: + if follow_symlinks: + return libc.setxattr(path, name, value, size, 0, XATTR_NOFLAGS) + else: + return libc.setxattr(path, name, value, size, 0, XATTR_NOFOLLOW) + + _setxattr_inner(func, path, name, value) elif sys.platform.startswith('freebsd'): # pragma: freebsd only - EXTATTR_NAMESPACE_USER = 0x0001 libc.extattr_list_fd.argtypes = (c_int, c_int, c_char_p, c_size_t) libc.extattr_list_fd.restype = c_ssize_t libc.extattr_list_link.argtypes = (c_char_p, c_int, c_char_p, c_size_t) @@ -265,27 +283,23 @@ elif sys.platform.startswith('freebsd'): # pragma: freebsd only libc.extattr_set_link.restype = c_int libc.extattr_set_file.argtypes = (c_char_p, c_int, c_char_p, c_char_p, c_size_t) libc.extattr_set_file.restype = c_int + ns = EXTATTR_NAMESPACE_USER = 0x0001 def listxattr(path, *, follow_symlinks=True): - ns = EXTATTR_NAMESPACE_USER - if isinstance(path, str): - path = os.fsencode(path) - if isinstance(path, int): - func = libc.extattr_list_fd - elif follow_symlinks: - func = libc.extattr_list_file - else: - func = libc.extattr_list_link - # TODO: fix, see linux - n = _check(func(path, ns, None, 0), path) + def func(path, buf, size): + if isinstance(path, int): + return libc.extattr_list_fd(path, ns, buf, size) + else: + if follow_symlinks: + return libc.extattr_list_file(path, ns, buf, size) + else: + return libc.extattr_list_link(path, ns, buf, size) + + n, buf = _listxattr_inner(func, path) if n == 0: return [] - namebuf = create_string_buffer(n) - n2 = _check(func(path, ns, namebuf, n), path) - if n2 != n: - raise Exception('listxattr failed') names = [] - mv = memoryview(namebuf.raw) + mv = memoryview(buf) while mv: length = mv[0] names.append(os.fsdecode(bytes(mv[1:1 + length]))) @@ -293,37 +307,31 @@ elif sys.platform.startswith('freebsd'): # pragma: freebsd only return names def getxattr(path, name, *, follow_symlinks=True): - name = os.fsencode(name) - if isinstance(path, str): - path = os.fsencode(path) - if isinstance(path, int): - func = libc.extattr_get_fd - elif follow_symlinks: - func = libc.extattr_get_file - else: - func = libc.extattr_get_link - # TODO: fix, see linux - n = _check(func(path, EXTATTR_NAMESPACE_USER, name, None, 0)) + def func(path, name, buf, size): + if isinstance(path, int): + return libc.extattr_get_fd(path, ns, name, buf, size) + else: + if follow_symlinks: + return libc.extattr_get_file(path, ns, name, buf, size) + else: + return libc.extattr_get_link(path, ns, name, buf, size) + + n, buf = _getxattr_inner(func, path, name) if n == 0: return - valuebuf = create_string_buffer(n) - n2 = _check(func(path, EXTATTR_NAMESPACE_USER, name, valuebuf, n), path) - if n2 != n: - raise Exception('getxattr failed') - return valuebuf.raw + return buf[:n] def setxattr(path, name, value, *, follow_symlinks=True): - name = os.fsencode(name) - value = value and os.fsencode(value) - if isinstance(path, str): - path = os.fsencode(path) - if isinstance(path, int): - func = libc.extattr_set_fd - elif follow_symlinks: - func = libc.extattr_set_file - else: - func = libc.extattr_set_link - _check(func(path, EXTATTR_NAMESPACE_USER, name, value, len(value) if value else 0), path) + def func(path, name, value, size): + if isinstance(path, int): + return libc.extattr_set_fd(path, ns, name, value, size) + else: + if follow_symlinks: + return libc.extattr_set_file(path, ns, name, value, size) + else: + return libc.extattr_set_link(path, ns, name, value, size) + + _setxattr_inner(func, path, name, value) else: # pragma: unknown platform only def listxattr(path, *, follow_symlinks=True): From 09dbec99a05040244f1e4aca908552ba07a42052 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 11 Aug 2016 20:11:36 +0200 Subject: [PATCH 0090/1387] raise OSError including the error message derived from errno also: deal with path being a integer FD --- borg/xattr.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/borg/xattr.py b/borg/xattr.py index 94df6b05..82a3afb4 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -102,7 +102,13 @@ def _check(rv, path=None): if e == errno.ERANGE: raise BufferTooSmallError else: - raise OSError(e, path) + try: + msg = os.strerror(e) + except ValueError: + msg = '' + if isinstance(path, int): + path = '' % path + raise OSError(e, msg, path) return rv From 418794f66f9b577de67a9ba15618738aa81ba626 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 11 Aug 2016 21:52:48 +0200 Subject: [PATCH 0091/1387] xattr: errno ERANGE has different meanings --- borg/xattr.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/borg/xattr.py b/borg/xattr.py index 82a3afb4..2649eadb 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -18,6 +18,7 @@ logger = create_logger() def get_buffer(size=None, init=False): if size is not None: size = int(size) + assert size < 2 ** 24 if init or len(thread_local.buffer) < size: thread_local.buffer = create_string_buffer(size) return thread_local.buffer @@ -96,10 +97,12 @@ class BufferTooSmallError(Exception): """the buffer given to an xattr function was too small for the result""" -def _check(rv, path=None): +def _check(rv, path=None, detect_buffer_too_small=False): if rv < 0: e = get_errno() - if e == errno.ERANGE: + if detect_buffer_too_small and e == errno.ERANGE: + # listxattr and getxattr signal with ERANGE that they need a bigger result buffer. + # setxattr signals this way that e.g. a xattr key name is too long / inacceptable. raise BufferTooSmallError else: try: @@ -119,10 +122,9 @@ def _listxattr_inner(func, path): while True: buf = get_buffer(size) try: - n = _check(func(path, buf, size), path) + n = _check(func(path, buf, size), path, detect_buffer_too_small=True) except BufferTooSmallError: size *= 2 - assert size < 2 ** 20 else: return n, buf.raw @@ -135,7 +137,7 @@ def _getxattr_inner(func, path, name): while True: buf = get_buffer(size) try: - n = _check(func(path, name, buf, size), path) + n = _check(func(path, name, buf, size), path, detect_buffer_too_small=True) except BufferTooSmallError: size *= 2 else: @@ -148,7 +150,7 @@ def _setxattr_inner(func, path, name, value): name = os.fsencode(name) value = value and os.fsencode(value) size = len(value) if value else 0 - _check(func(path, name, value, size), path) + _check(func(path, name, value, size), path, detect_buffer_too_small=False) if sys.platform.startswith('linux'): # pragma: linux only From 4eac66fe2aa7c18569703ef10149c9171dc784a7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 11 Aug 2016 22:47:12 +0200 Subject: [PATCH 0092/1387] xattr: fix race condition in get_all(), see issue #906 --- borg/xattr.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/borg/xattr.py b/borg/xattr.py index 2649eadb..0439b193 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -15,6 +15,13 @@ from .logger import create_logger logger = create_logger() +try: + ENOATTR = errno.ENOATTR +except AttributeError: + # on some platforms, ENOATTR is missing, use ENODATA there + ENOATTR = errno.ENODATA + + def get_buffer(size=None, init=False): if size is not None: size = int(size) @@ -41,8 +48,17 @@ def is_enabled(path=None): def get_all(path, follow_symlinks=True): try: - return dict((name, getxattr(path, name, follow_symlinks=follow_symlinks)) - for name in listxattr(path, follow_symlinks=follow_symlinks)) + result = {} + names = listxattr(path, follow_symlinks=follow_symlinks) + for name in names: + try: + result[name] = getxattr(path, name, follow_symlinks=follow_symlinks) + except OSError as e: + # if we get ENOATTR, a race has happened: xattr names were deleted after list. + # we just ignore the now missing ones. if you want consistency, do snapshots. + if e.errno != ENOATTR: + raise + return result except OSError as e: if e.errno in (errno.ENOTSUP, errno.EPERM): return {} From 7ea052a5e80ece5fd70640c918d6bcb8f50d3a5a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 12 Aug 2016 02:55:49 +0200 Subject: [PATCH 0093/1387] xattr: buffer full check for freebsd freebsd 10.2: it does not give rc < 0 and errno == ERANGE if the buffer was too small, like linux or mac OS X does. rv == buffer len might be a signal of truncation. rv > buffer len would be even worse not sure if some implementation returns the total length of the data, not just the amount put into the buffer. but as we use the returned length to "truncate" the buffer, we better make sure it is not longer than the buffer. also: freebsd listxattr memoryview len bugfix --- borg/xattr.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/borg/xattr.py b/borg/xattr.py index 0439b193..99f7657b 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -128,6 +128,12 @@ def _check(rv, path=None, detect_buffer_too_small=False): if isinstance(path, int): path = '' % path raise OSError(e, msg, path) + if detect_buffer_too_small and rv >= len(get_buffer()): + # freebsd does not error with ERANGE if the buffer is too small, + # it just fills the buffer, truncates and returns. + # so, we play sure and just assume that result is truncated if + # it happens to be a full buffer. + raise BufferTooSmallError return rv @@ -323,7 +329,7 @@ elif sys.platform.startswith('freebsd'): # pragma: freebsd only if n == 0: return [] names = [] - mv = memoryview(buf) + mv = memoryview(buf)[:n] while mv: length = mv[0] names.append(os.fsdecode(bytes(mv[1:1 + length]))) From b6ead3dce2cabd07b5fdf5f50a228f8e76108c5c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 12 Aug 2016 04:17:27 +0200 Subject: [PATCH 0094/1387] xattr: use some string processing functions, dedup, simplify --- borg/xattr.py | 48 ++++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/borg/xattr.py b/borg/xattr.py index 99f7657b..0099a2ad 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -109,6 +109,22 @@ except OSError as e: raise Exception(msg) +def split_string0(buf): + """split a list of zero-terminated strings into python not-zero-terminated bytes""" + return buf.split(b'\0')[:-1] + + +def split_lstring(buf): + """split a list of length-prefixed strings into python not-length-prefixed bytes""" + result = [] + mv = memoryview(buf) + while mv: + length = mv[0] + result.append(bytes(mv[1:1 + length])) + mv = mv[1 + length:] + return result + + class BufferTooSmallError(Exception): """the buffer given to an xattr function was too small for the result""" @@ -200,10 +216,7 @@ if sys.platform.startswith('linux'): # pragma: linux only return libc.llistxattr(path, buf, size) n, buf = _listxattr_inner(func, path) - if n == 0: - return [] - names = buf[:n].split(b'\0')[:-1] - return [os.fsdecode(name) for name in names + return [os.fsdecode(name) for name in split_string0(buf[:n]) if not name.startswith(b'system.posix_acl_')] def getxattr(path, name, *, follow_symlinks=True): @@ -217,9 +230,7 @@ if sys.platform.startswith('linux'): # pragma: linux only return libc.lgetxattr(path, name, buf, size) n, buf = _getxattr_inner(func, path, name) - if n == 0: - return - return buf[:n] + return buf[:n] or None def setxattr(path, name, value, *, follow_symlinks=True): def func(path, name, value, size): @@ -262,10 +273,7 @@ elif sys.platform == 'darwin': # pragma: darwin only return libc.listxattr(path, buf, size, XATTR_NOFOLLOW) n, buf = _listxattr_inner(func, path) - if n == 0: - return [] - names = buf[:n].split(b'\0')[:-1] - return [os.fsdecode(name) for name in names] + return [os.fsdecode(name) for name in split_string0(buf[:n])] def getxattr(path, name, *, follow_symlinks=True): def func(path, name, buf, size): @@ -278,9 +286,7 @@ elif sys.platform == 'darwin': # pragma: darwin only return libc.getxattr(path, name, buf, size, 0, XATTR_NOFOLLOW) n, buf = _getxattr_inner(func, path, name) - if n == 0: - return - return buf[:n] + return buf[:n] or None def setxattr(path, name, value, *, follow_symlinks=True): def func(path, name, value, size): @@ -326,15 +332,7 @@ elif sys.platform.startswith('freebsd'): # pragma: freebsd only return libc.extattr_list_link(path, ns, buf, size) n, buf = _listxattr_inner(func, path) - if n == 0: - return [] - names = [] - mv = memoryview(buf)[:n] - while mv: - length = mv[0] - names.append(os.fsdecode(bytes(mv[1:1 + length]))) - mv = mv[1 + length:] - return names + return [os.fsdecode(name) for name in split_lstring(buf[:n])] def getxattr(path, name, *, follow_symlinks=True): def func(path, name, buf, size): @@ -347,9 +345,7 @@ elif sys.platform.startswith('freebsd'): # pragma: freebsd only return libc.extattr_get_link(path, ns, name, buf, size) n, buf = _getxattr_inner(func, path, name) - if n == 0: - return - return buf[:n] + return buf[:n] or None def setxattr(path, name, value, *, follow_symlinks=True): def func(path, name, value, size): From 8630ebf3f0bd1a5743f9d8507b12d6661c1b875b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 12 Aug 2016 04:21:21 +0200 Subject: [PATCH 0095/1387] xattr: fix module docstring --- borg/xattr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/borg/xattr.py b/borg/xattr.py index 0099a2ad..7a4a7ae9 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -1,5 +1,5 @@ -"""A basic extended attributes (xattr) implementation for Linux and MacOS X -""" +"""A basic extended attributes (xattr) implementation for Linux, FreeBSD and MacOS X.""" + import errno import os import re From c834b2969cea159cf1a163666381dec63c73ebcf Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 12 Aug 2016 17:54:15 +0200 Subject: [PATCH 0096/1387] document archive limitation, #1452 --- docs/faq.rst | 11 +++++++++++ docs/internals.rst | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 88418b18..c772f5fa 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -62,6 +62,17 @@ Which file types, attributes, etc. are *not* preserved? holes in a sparse file. * filesystem specific attributes, like ext4 immutable bit, see :issue:`618`. +Are there other known limitations? +---------------------------------- + +- A single archive can only reference a limited volume of file/dir metadata, + usually corresponding to tens or hundreds of millions of files/dirs. + When trying to go beyond that limit, you will get a fatal IntegrityError + exception telling that the (archive) object is too big. + An easy workaround is to create multiple archives with less items each. + See also the :ref:`archive_limitation` and :issue:`1452`. + + Why is my backup bigger than with attic? Why doesn't |project_name| do compression by default? ---------------------------------------------------------------------------------------------- diff --git a/docs/internals.rst b/docs/internals.rst index b088f68e..82be188b 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -160,12 +160,40 @@ object that contains: * version * name -* list of chunks containing item metadata +* list of chunks containing item metadata (size: count * ~40B) * cmdline * hostname * username * time +.. _archive_limitation: + +Note about archive limitations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The archive is currently stored as a single object in the repository +and thus limited in size to MAX_OBJECT_SIZE (20MiB). + +As one chunk list entry is ~40B, that means we can reference ~500.000 item +metadata stream chunks per archive. + +Each item metadata stream chunk is ~128kiB (see hardcoded ITEMS_CHUNKER_PARAMS). + +So that means the whole item metadata stream is limited to ~64GiB chunks. +If compression is used, the amount of storable metadata is bigger - by the +compression factor. + +If the medium size of an item entry is 100B (small size file, no ACLs/xattrs), +that means a limit of ~640 million files/directories per archive. + +If the medium size of an item entry is 2kB (~100MB size files or more +ACLs/xattrs), the limit will be ~32 million files/directories per archive. + +If one tries to create an archive object bigger than MAX_OBJECT_SIZE, a fatal +IntegrityError will be raised. + +A workaround is to create multiple archives with less items each, see +also :issue:`1452`. The Item -------- @@ -174,7 +202,7 @@ Each item represents a file, directory or other fs item and is stored as an ``item`` dictionary that contains: * path -* list of data chunks +* list of data chunks (size: count * ~40B) * user * group * uid From 3c7dddcb99586c2e7ac791177598fa99e2fb459f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 12 Aug 2016 18:00:50 +0200 Subject: [PATCH 0097/1387] update changelog --- docs/changes.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 305be063..4972dc38 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -57,6 +57,10 @@ Security fixes: - fix security issue with remote repository access, #1428 + +Version 1.0.7rc2 (not released yet) +----------------------------------- + Bug fixes: - do not write objects to repository that are bigger than the allowed size, @@ -64,6 +68,11 @@ Bug fixes: IMPORTANT: if you created archives with many millions of files or directories, please verify if you can open them successfully, e.g. try a "borg list REPO::ARCHIVE". +- fixed a race condition in extended attributes querying that led to the + entire file not being backed up (while logging the error, exit code = 1), + #1469 +- fixed a race condition in extended attributes querying that led to a crash, + #1462 Version 1.0.7rc1 (2016-08-05) From ef9e8a584bdddcef707546be7b6c9457dd282af4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 12 Aug 2016 19:34:29 +0200 Subject: [PATCH 0098/1387] refactor buffer code into helpers.Buffer class, add tests --- borg/helpers.py | 42 ++++++++++++++++++++++++++++ borg/testsuite/helpers.py | 58 ++++++++++++++++++++++++++++++++++++++- borg/testsuite/xattr.py | 10 +++---- borg/xattr.py | 25 ++++++----------- 4 files changed, 112 insertions(+), 23 deletions(-) diff --git a/borg/helpers.py b/borg/helpers.py index 975ddca8..27b3f0d3 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -10,6 +10,7 @@ import re from shutil import get_terminal_size import sys import platform +import threading import time import unicodedata import io @@ -655,6 +656,47 @@ def memoize(function): return decorated_function +class Buffer: + """ + provide a thread-local buffer + """ + def __init__(self, allocator, size=4096, limit=None): + """ + Initialize the buffer: use allocator(size) call to allocate a buffer. + Optionally, set the upper for the buffer size. + """ + assert callable(allocator), 'must give alloc(size) function as first param' + assert limit is None or size <= limit, 'initial size must be <= limit' + self._thread_local = threading.local() + self.allocator = allocator + self.limit = limit + self.resize(size, init=True) + + def __len__(self): + return len(self._thread_local.buffer) + + def resize(self, size, init=False): + """ + resize the buffer - to avoid frequent reallocation, we usually always grow (if needed). + giving init=True it is possible to first-time initialize or shrink the buffer. + if a buffer size beyond the limit is requested, raise ValueError. + """ + size = int(size) + if self.limit is not None and size > self.limit: + raise ValueError('Requested buffer size %d is above the limit of %d.' % (size, self.limit)) + if init or len(self) < size: + self._thread_local.buffer = self.allocator(size) + + def get(self, size=None, init=False): + """ + return a buffer of at least the requested size (None: any current size). + init=True can be given to trigger shrinking of the buffer to the given size. + """ + if size is not None: + self.resize(size, init) + return self._thread_local.buffer + + @memoize def uid2user(uid, default=None): try: diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py index 9e03019d..cbe4cd9d 100644 --- a/borg/testsuite/helpers.py +++ b/borg/testsuite/helpers.py @@ -15,7 +15,8 @@ from ..helpers import Location, format_file_size, format_timedelta, format_line, yes, TRUISH, FALSISH, DEFAULTISH, \ StableDict, int_to_bigint, bigint_to_int, parse_timestamp, CompressionSpec, ChunkerParams, \ ProgressIndicatorPercent, ProgressIndicatorEndless, load_excludes, parse_pattern, \ - PatternMatcher, RegexPattern, PathPrefixPattern, FnmatchPattern, ShellPattern + PatternMatcher, RegexPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, \ + Buffer from . import BaseTestCase, environment_variable, FakeInputs @@ -714,6 +715,61 @@ def test_is_slow_msgpack(): assert not is_slow_msgpack() +class TestBuffer: + def test_type(self): + buffer = Buffer(bytearray) + assert isinstance(buffer.get(), bytearray) + buffer = Buffer(bytes) # don't do that in practice + assert isinstance(buffer.get(), bytes) + + def test_len(self): + buffer = Buffer(bytearray, size=0) + b = buffer.get() + assert len(buffer) == len(b) == 0 + buffer = Buffer(bytearray, size=1234) + b = buffer.get() + assert len(buffer) == len(b) == 1234 + + def test_resize(self): + buffer = Buffer(bytearray, size=100) + assert len(buffer) == 100 + b1 = buffer.get() + buffer.resize(200) + assert len(buffer) == 200 + b2 = buffer.get() + assert b2 is not b1 # new, bigger buffer + buffer.resize(100) + assert len(buffer) >= 100 + b3 = buffer.get() + assert b3 is b2 # still same buffer (200) + buffer.resize(100, init=True) + assert len(buffer) == 100 # except on init + b4 = buffer.get() + assert b4 is not b3 # new, smaller buffer + + def test_limit(self): + buffer = Buffer(bytearray, size=100, limit=200) + buffer.resize(200) + assert len(buffer) == 200 + with pytest.raises(ValueError): + buffer.resize(201) + assert len(buffer) == 200 + + def test_get(self): + buffer = Buffer(bytearray, size=100, limit=200) + b1 = buffer.get(50) + assert len(b1) >= 50 # == 100 + b2 = buffer.get(100) + assert len(b2) >= 100 # == 100 + assert b2 is b1 # did not need resizing yet + b3 = buffer.get(200) + assert len(b3) == 200 + assert b3 is not b2 # new, resized buffer + with pytest.raises(ValueError): + buffer.get(201) # beyond limit + assert len(buffer) == 200 + + def test_yes_input(): inputs = list(TRUISH) input = FakeInputs(inputs) diff --git a/borg/testsuite/xattr.py b/borg/testsuite/xattr.py index ecc92fa1..5693c753 100644 --- a/borg/testsuite/xattr.py +++ b/borg/testsuite/xattr.py @@ -2,7 +2,7 @@ import os import tempfile import unittest -from ..xattr import is_enabled, getxattr, setxattr, listxattr, get_buffer +from ..xattr import is_enabled, getxattr, setxattr, listxattr, buffer from . import BaseTestCase @@ -41,20 +41,20 @@ class XattrTestCase(BaseTestCase): def test_listxattr_buffer_growth(self): # make it work even with ext4, which imposes rather low limits - get_buffer(size=64, init=True) + buffer.resize(size=64, init=True) # xattr raw key list will be size 9 * (10 + 1), which is > 64 keys = ['user.attr%d' % i for i in range(9)] for key in keys: setxattr(self.tmpfile.name, key, b'x') got_keys = listxattr(self.tmpfile.name) self.assert_equal_se(got_keys, keys) - self.assert_equal(len(get_buffer()), 128) + self.assert_equal(len(buffer), 128) def test_getxattr_buffer_growth(self): # make it work even with ext4, which imposes rather low limits - get_buffer(size=64, init=True) + buffer.resize(size=64, init=True) value = b'x' * 126 setxattr(self.tmpfile.name, 'user.big', value) got_value = getxattr(self.tmpfile.name, 'user.big') self.assert_equal(value, got_value) - self.assert_equal(len(get_buffer()), 128) + self.assert_equal(len(buffer), 128) diff --git a/borg/xattr.py b/borg/xattr.py index 7a4a7ae9..10f99ae6 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -6,11 +6,12 @@ import re import subprocess import sys import tempfile -import threading from ctypes import CDLL, create_string_buffer, c_ssize_t, c_size_t, c_char_p, c_int, c_uint32, get_errno from ctypes.util import find_library from distutils.version import LooseVersion +from .helpers import Buffer + from .logger import create_logger logger = create_logger() @@ -22,17 +23,7 @@ except AttributeError: ENOATTR = errno.ENODATA -def get_buffer(size=None, init=False): - if size is not None: - size = int(size) - assert size < 2 ** 24 - if init or len(thread_local.buffer) < size: - thread_local.buffer = create_string_buffer(size) - return thread_local.buffer - - -thread_local = threading.local() -get_buffer(size=4096, init=True) +buffer = Buffer(create_string_buffer, limit=2**24) def is_enabled(path=None): @@ -144,7 +135,7 @@ def _check(rv, path=None, detect_buffer_too_small=False): if isinstance(path, int): path = '' % path raise OSError(e, msg, path) - if detect_buffer_too_small and rv >= len(get_buffer()): + if detect_buffer_too_small and rv >= len(buffer): # freebsd does not error with ERANGE if the buffer is too small, # it just fills the buffer, truncates and returns. # so, we play sure and just assume that result is truncated if @@ -156,9 +147,9 @@ def _check(rv, path=None, detect_buffer_too_small=False): def _listxattr_inner(func, path): if isinstance(path, str): path = os.fsencode(path) - size = len(get_buffer()) + size = len(buffer) while True: - buf = get_buffer(size) + buf = buffer.get(size) try: n = _check(func(path, buf, size), path, detect_buffer_too_small=True) except BufferTooSmallError: @@ -171,9 +162,9 @@ def _getxattr_inner(func, path, name): if isinstance(path, str): path = os.fsencode(path) name = os.fsencode(name) - size = len(get_buffer()) + size = len(buffer) while True: - buf = get_buffer(size) + buf = buffer.get(size) try: n = _check(func(path, name, buf, size), path, detect_buffer_too_small=True) except BufferTooSmallError: From e1bc7b62f6114dd4960ceaaebd5310dd29de91dd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 12 Aug 2016 21:10:46 +0200 Subject: [PATCH 0099/1387] lz4: reuse helpers.Buffer --- borg/compress.pyx | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/borg/compress.pyx b/borg/compress.pyx index 13955a86..f0dd0b1b 100644 --- a/borg/compress.pyx +++ b/borg/compress.pyx @@ -1,25 +1,18 @@ -import threading import zlib try: import lzma except ImportError: lzma = None +from .helpers import Buffer + cdef extern from "lz4.h": int LZ4_compress_limitedOutput(const char* source, char* dest, int inputSize, int maxOutputSize) nogil int LZ4_decompress_safe(const char* source, char* dest, int inputSize, int maxOutputSize) nogil int LZ4_compressBound(int inputSize) nogil -thread_local = threading.local() -thread_local.buffer = bytes() - - -cdef char *get_buffer(size): - size = int(size) - if len(thread_local.buffer) < size: - thread_local.buffer = bytes(size) - return thread_local.buffer +buffer = Buffer(bytearray, size=0) cdef class CompressorBase: @@ -88,7 +81,8 @@ class LZ4(CompressorBase): cdef char *source = idata cdef char *dest osize = LZ4_compressBound(isize) - dest = get_buffer(osize) + buf = buffer.get(osize) + dest = buf with nogil: osize = LZ4_compress_limitedOutput(source, dest, isize, osize) if not osize: @@ -108,7 +102,8 @@ class LZ4(CompressorBase): # allocate more if isize * 3 is already bigger, to avoid having to resize often. osize = max(int(1.1 * 2**23), isize * 3) while True: - dest = get_buffer(osize) + buf = buffer.get(osize) + dest = buf with nogil: rsize = LZ4_decompress_safe(source, dest, isize, osize) if rsize >= 0: From 95cf337fa54ad5d83efa96651aa6355ce53a8006 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 8 Aug 2016 14:49:25 +0200 Subject: [PATCH 0100/1387] Fix untracked segments made by moved DELETEs Fixes #1442 (note that the segments _still_ get generated, see the comment, they should be collected now on the next compaction run) --- borg/repository.py | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/borg/repository.py b/borg/repository.py index c33f49da..40d73042 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -292,6 +292,8 @@ class Repository: self.io.delete_segment(segment) unused = [] + # The first segment compaction creates, if any + first_new_segment = self.io.get_latest_segment() + 1 for segment in sorted(self.compact): if self.io.segment_exists(segment): for tag, key, offset, data in self.io.iter_objects(segment, include_data=True): @@ -307,15 +309,52 @@ class Repository: segments[segment] -= 1 elif tag == TAG_DELETE: if index_transaction_id is None or segment > index_transaction_id: + # (introduced in 6425d16aa84be1eaaf88) + # This is needed to avoid object un-deletion if we crash between the commit and the deletion + # of old segments in complete_xfer(). + # + # However, this only happens if the crash also affects the FS to the effect that file deletions + # did not materialize consistently after journal recovery. If they always materialize in-order + # then this is not a problem, because the old segment containing a deleted object would be deleted + # before the segment containing the delete. + # + # Consider the following series of operations if we would not do this, ie. this entire if: + # would be removed. + # Columns are segments, lines are different keys (line 1 = some key, line 2 = some other key) + # Legend: P=TAG_PUT, D=TAG_DELETE, c=commit, i=index is written for latest commit + # + # Segment | 1 | 2 | 3 + # --------+-------+-----+------ + # Key 1 | P | D | + # Key 2 | P | | P + # commits | c i | c | c i + # --------+-------+-----+------ + # ^- compact_segments starts + # ^- complete_xfer commits, after that complete_xfer deletes + # segments 1 and 2 (and then the index would be written). + # + # Now we crash. But only segment 2 gets deleted, while segment 1 is still around. Now key 1 + # is suddenly undeleted (because the delete in segment 2 is now missing). + # Again, note the requirement here. We delete these in the correct order that this doesn't happen, + # and only if the FS materialization of these deletes is reordered or parts dropped this can happen. + # In this case it doesn't cause outright corruption, 'just' an index count mismatch, which will be + # fixed by borg-check --repair. + # + # Note that in this check the index state is the proxy for a "most definitely settled" repository state, + # ie. the assumption is that *all* operations on segments <= index state are completed and stable. try: - self.io.write_delete(key, raise_full=save_space) + new_segment = self.io.write_delete(key, raise_full=save_space) except LoggedIO.SegmentFull: complete_xfer() - self.io.write_delete(key) + new_segment = self.io.write_delete(key) + self.compact.add(new_segment) + self.segments.setdefault(new_segment, 0) assert segments[segment] == 0 unused.append(segment) complete_xfer() - self.compact = set() + # Moving of deletes creates new sparse segments, only store these. All other segments + # are compact now. + self.compact = {segment for segment in self.compact if segment >= first_new_segment} def replay_segments(self, index_transaction_id, segments_transaction_id): # fake an old client, so that in case we do not have an exclusive lock yet, prepare_txn will upgrade the lock: From 3b716f98ff6bdce75f7ce51d0123e3551488617b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 13 Aug 2016 01:47:51 +0200 Subject: [PATCH 0101/1387] Add regression test for 95cf337 --- borg/testsuite/repository.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/borg/testsuite/repository.py b/borg/testsuite/repository.py index c50e785b..9c5a5a46 100644 --- a/borg/testsuite/repository.py +++ b/borg/testsuite/repository.py @@ -8,8 +8,9 @@ from ..hashindex import NSIndex from ..helpers import Location, IntegrityError from ..locking import Lock, LockFailed from ..remote import RemoteRepository, InvalidRPCMethod -from ..repository import Repository, LoggedIO, TAG_COMMIT, MAX_DATA_SIZE +from ..repository import Repository, LoggedIO, TAG_DELETE, MAX_DATA_SIZE from . import BaseTestCase +from .hashindex import H UNSPECIFIED = object() # for default values where we can't use None @@ -227,6 +228,28 @@ class RepositoryCommitTestCase(RepositoryTestCaseBase): io = self.repository.io assert not io.is_committed_segment(io.get_latest_segment()) + def test_moved_deletes_are_tracked(self): + self.repository.put(H(1), b'1') + self.repository.put(H(2), b'2') + self.repository.commit() + self.repository.delete(H(1)) + self.repository.commit() + last_segment = self.repository.io.get_latest_segment() + num_deletes = 0 + for tag, key, offset, data in self.repository.io.iter_objects(last_segment, include_data=True): + if tag == TAG_DELETE: + assert key == H(1) + num_deletes += 1 + assert num_deletes == 1 + assert last_segment in self.repository.compact + self.repository.put(H(3), b'3') + self.repository.commit() + assert last_segment not in self.repository.compact + assert not self.repository.io.segment_exists(last_segment) + last_segment = self.repository.io.get_latest_segment() + for tag, key, offset in self.repository.io.iter_objects(last_segment): + assert tag != TAG_DELETE + class RepositoryAppendOnlyTestCase(RepositoryTestCaseBase): def open(self, create=False): From 07b47ef4a54d367162a4c1d2777ac573f5949e43 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 13 Aug 2016 00:49:07 +0200 Subject: [PATCH 0102/1387] update CHANGES --- docs/changes.rst | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 4972dc38..60100841 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -58,8 +58,8 @@ Security fixes: - fix security issue with remote repository access, #1428 -Version 1.0.7rc2 (not released yet) ------------------------------------ +Version 1.0.7rc2 (2016-08-13) +----------------------------- Bug fixes: @@ -68,11 +68,33 @@ Bug fixes: IMPORTANT: if you created archives with many millions of files or directories, please verify if you can open them successfully, e.g. try a "borg list REPO::ARCHIVE". -- fixed a race condition in extended attributes querying that led to the - entire file not being backed up (while logging the error, exit code = 1), - #1469 -- fixed a race condition in extended attributes querying that led to a crash, - #1462 +- lz4 compression: dynamically enlarge the (de)compression buffer, the static + buffer was not big enough for archives with extremely many items, #1453 +- larger item metadata stream chunks, raise archive limit by 8x, #1452 +- fix untracked segments made by moved DELETEs, #1442 +- extended attributes (xattrs) related fixes: + + - fixed a race condition in xattrs querying that led to the entire file not + being backed up (while logging the error, exit code = 1), #1469 + - fixed a race condition in xattrs querying that led to a crash, #1462 + - raise OSError including the error message derived from errno, deal with + path being a integer FD + +Other changes: + +- print active env var override by default, #1467 +- xattr module: refactor code, deduplicate, clean up +- repository: split object size check into too small and too big +- add a transaction_id assertion, so borg init on a broken (inconsistent) + filesystem does not look like a coding error in borg, but points to the + real problem. +- explain confusing TypeError caused by compat support for old servers, #1456 +- add forgotten usage help file from build_usage +- refactor/unify buffer code into helpers.Buffer class, add tests +- docs: + + - document archive limitation, #1452 + - improve prune examples Version 1.0.7rc1 (2016-08-05) From 17aacb971948abf95e0a51f276f2e4759dd33864 Mon Sep 17 00:00:00 2001 From: enkore Date: Sat, 13 Aug 2016 10:18:41 +0200 Subject: [PATCH 0103/1387] Fix changes.rst formatting, clarify changelog --- docs/changes.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 60100841..7da62747 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -65,13 +65,17 @@ Bug fixes: - do not write objects to repository that are bigger than the allowed size, borg will reject reading them, #1451. - IMPORTANT: if you created archives with many millions of files or - directories, please verify if you can open them successfully, - e.g. try a "borg list REPO::ARCHIVE". + + Important: if you created archives with many millions of files or + directories, please verify if you can open them successfully, + e.g. try a "borg list REPO::ARCHIVE". - lz4 compression: dynamically enlarge the (de)compression buffer, the static buffer was not big enough for archives with extremely many items, #1453 -- larger item metadata stream chunks, raise archive limit by 8x, #1452 +- larger item metadata stream chunks, raise archive item limit by 8x, #1452 - fix untracked segments made by moved DELETEs, #1442 + + Impact: Previously (metadata) segments could become untracked when deleting data, + these would never be cleaned up. - extended attributes (xattrs) related fixes: - fixed a race condition in xattrs querying that led to the entire file not From 42b6a838da46c87d85b5b32d45635ac25aec01fd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 14 Aug 2016 15:07:18 +0200 Subject: [PATCH 0104/1387] fix cyclic import issue, fix tests needed to increase ChunkBuffer size due to increased items stream chunk size to get the test working. --- src/borg/archive.py | 2 +- src/borg/helpers.py | 2 +- src/borg/testsuite/archive.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 52a03af8..35ccff0a 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -169,7 +169,7 @@ class DownloadPipeline: class ChunkBuffer: - BUFFER_SIZE = 1 * 1024 * 1024 + BUFFER_SIZE = 8 * 1024 * 1024 def __init__(self, key, chunker_params=ITEMS_CHUNKER_PARAMS): self.buffer = BytesIO() diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 728bc6ef..2324d32a 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -39,7 +39,6 @@ from . import crypto from . import hashindex from . import shellpattern from .constants import * # NOQA -from .compress import get_compressor # meta dict, data bytes _Chunk = namedtuple('_Chunk', 'meta data') @@ -1584,6 +1583,7 @@ class CompressionDecider2: return compr_spec, chunk def heuristic_lz4(self, compr_args, chunk): + from .compress import get_compressor meta, data = chunk lz4 = get_compressor('lz4') cdata = lz4.compress(data) diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index 30f61974..19db1a44 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -109,7 +109,7 @@ class ChunkBufferTestCase(BaseTestCase): self.assert_equal(data, [Item(internal_dict=d) for d in unpacker]) def test_partial(self): - big = "0123456789" * 10000 + big = "0123456789abcdefghijklmnopqrstuvwxyz" * 25000 data = [Item(path='full', source=big), Item(path='partial', source=big)] cache = MockCache() key = PlaintextKey(None) From a80b371d09fba4c045e4e710081f27883cfa408f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 31 Jul 2016 21:47:31 +0200 Subject: [PATCH 0105/1387] Add decompress arg to Key.decrypt --- src/borg/key.py | 16 +++++++++++----- src/borg/testsuite/key.py | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/borg/key.py b/src/borg/key.py index dd64b0fd..5161b7cc 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -105,7 +105,7 @@ class KeyBase: def encrypt(self, chunk): pass - def decrypt(self, id, data): + def decrypt(self, id, data, decompress=True): pass @@ -130,10 +130,13 @@ class PlaintextKey(KeyBase): chunk = self.compress(chunk) return b''.join([self.TYPE_STR, chunk.data]) - def decrypt(self, id, data): + def decrypt(self, id, data, decompress=True): if data[0] != self.TYPE: raise IntegrityError('Invalid encryption envelope') - data = self.compressor.decompress(memoryview(data)[1:]) + payload = memoryview(data)[1:] + if not decompress: + return Chunk(payload) + data = self.compressor.decompress(payload) if id and sha256(data).digest() != id: raise IntegrityError('Chunk id verification failed') return Chunk(data) @@ -166,7 +169,7 @@ class AESKeyBase(KeyBase): hmac = hmac_sha256(self.enc_hmac_key, data) return b''.join((self.TYPE_STR, hmac, data)) - def decrypt(self, id, data): + def decrypt(self, id, data, decompress=True): if not (data[0] == self.TYPE or data[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)): raise IntegrityError('Invalid encryption envelope') @@ -176,7 +179,10 @@ class AESKeyBase(KeyBase): if not compare_digest(hmac_computed, hmac_given): raise IntegrityError('Encryption envelope checksum mismatch') self.dec_cipher.reset(iv=PREFIX + data[33:41]) - data = self.compressor.decompress(self.dec_cipher.decrypt(data_view[41:])) + payload = self.dec_cipher.decrypt(data_view[41:]) + if not decompress: + return Chunk(payload) + data = self.compressor.decompress(payload) if id: hmac_given = id hmac_computed = hmac_sha256(self.id_key, data) diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index c62f5ffe..5c604ee8 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -43,6 +43,14 @@ class TestKey: monkeypatch.setenv('BORG_KEYS_DIR', tmpdir) return tmpdir + @pytest.fixture(params=( + KeyfileKey, + PlaintextKey + )) + def key(self, request, monkeypatch): + monkeypatch.setenv('BORG_PASSPHRASE', 'test') + return request.param.create(self.MockRepository(), self.MockArgs()) + class MockRepository: class _Location: orig = '/some/place' @@ -155,6 +163,12 @@ class TestKey: id[12] = 0 key.decrypt(id, data) + def test_decrypt_decompress(self, key): + plaintext = Chunk(b'123456789') + encrypted = key.encrypt(plaintext) + assert key.decrypt(None, encrypted, decompress=False) != plaintext + assert key.decrypt(None, encrypted) == plaintext + class TestPassphrase: def test_passphrase_new_verification(self, capsys, monkeypatch): From c2c90645adbfced992289c77bc1ed3aef64d6d3e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 31 Jul 2016 21:56:51 +0200 Subject: [PATCH 0106/1387] Add Key.assert_id function --- src/borg/key.py | 15 ++++++++------- src/borg/testsuite/key.py | 12 ++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/borg/key.py b/src/borg/key.py index 5161b7cc..11a50f80 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -108,6 +108,12 @@ class KeyBase: def decrypt(self, id, data, decompress=True): pass + def assert_id(self, id, data): + if id: + id_computed = self.id_hash(data) + if not compare_digest(id_computed, id): + raise IntegrityError('Chunk id verification failed') + class PlaintextKey(KeyBase): TYPE = 0x02 @@ -137,8 +143,7 @@ class PlaintextKey(KeyBase): if not decompress: return Chunk(payload) data = self.compressor.decompress(payload) - if id and sha256(data).digest() != id: - raise IntegrityError('Chunk id verification failed') + self.assert_id(id, data) return Chunk(data) @@ -183,11 +188,7 @@ class AESKeyBase(KeyBase): if not decompress: return Chunk(payload) data = self.compressor.decompress(payload) - if id: - hmac_given = id - hmac_computed = hmac_sha256(self.id_key, data) - if not compare_digest(hmac_computed, hmac_given): - raise IntegrityError('Chunk id verification failed') + self.assert_id(id, data) return Chunk(data) def extract_nonce(self, payload): diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index 5c604ee8..85697010 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -169,6 +169,18 @@ class TestKey: assert key.decrypt(None, encrypted, decompress=False) != plaintext assert key.decrypt(None, encrypted) == plaintext + def test_assert_id(self, key): + plaintext = b'123456789' + id = key.id_hash(plaintext) + key.assert_id(id, plaintext) + id_changed = bytearray(id) + id_changed[0] += 1 + with pytest.raises(IntegrityError): + key.assert_id(id_changed, plaintext) + plaintext_changed = plaintext + b'1' + with pytest.raises(IntegrityError): + key.assert_id(id, plaintext_changed) + class TestPassphrase: def test_passphrase_new_verification(self, capsys, monkeypatch): From 5433b1a1e408013d7ceadfb2fa639f5a65a81b74 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 31 Jul 2016 22:00:58 +0200 Subject: [PATCH 0107/1387] Add Compressor.detect(data) -> CompressBase impl --- src/borg/compress.pyx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index f0dd0b1b..6c42493f 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -194,9 +194,14 @@ class Compressor: return self.compressor.compress(data) def decompress(self, data): + compressor_cls = self.detect(data) + return compressor_cls(**self.params).decompress(data) + + @staticmethod + def detect(data): hdr = bytes(data[:2]) # detect() does not work with memoryview for cls in COMPRESSOR_LIST: if cls.detect(hdr): - return cls(**self.params).decompress(data) + return cls else: raise ValueError('No decompressor for this data found: %r.', data[:2]) From 88798ae94954ed8fe52e57fa26554110d2369e07 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 31 Jul 2016 23:09:57 +0200 Subject: [PATCH 0108/1387] recreate: --always-recompress, --compression-from what a mess --- src/borg/archive.py | 20 +++++++++++++++----- src/borg/archiver.py | 4 ++++ src/borg/compress.pyx | 2 ++ src/borg/helpers.py | 4 +++- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 35ccff0a..fc131d51 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -19,6 +19,7 @@ logger = create_logger() from . import xattr from .cache import ChunkListEntry from .chunker import Chunker +from .compress import Compressor from .constants import * # NOQA from .hashindex import ChunkIndex, ChunkIndexEntry from .helpers import Manifest @@ -1298,7 +1299,7 @@ class ArchiveRecreater: def __init__(self, repository, manifest, key, cache, matcher, exclude_caches=False, exclude_if_present=None, keep_tag_files=False, - chunker_params=None, compression=None, compression_files=None, + chunker_params=None, compression=None, compression_files=None, always_recompress=False, dry_run=False, stats=False, progress=False, file_status_printer=None): self.repository = repository self.key = key @@ -1312,10 +1313,11 @@ class ArchiveRecreater: self.chunker_params = chunker_params or CHUNKER_PARAMS self.recompress = bool(compression) + self.always_recompress = always_recompress self.compression = compression or CompressionSpec('none') self.seen_chunks = set() self.compression_decider1 = CompressionDecider1(compression or CompressionSpec('none'), - compression_files or []) + compression_files or []) key.compression_decider2 = CompressionDecider2(compression or CompressionSpec('none')) self.autocommit_threshold = max(self.AUTOCOMMIT_THRESHOLD, self.cache.chunks_stored_size() / 100) @@ -1404,7 +1406,6 @@ class ArchiveRecreater: def process_chunks(self, archive, target, item): """Return new chunk ID list for 'item'.""" - # TODO: support --compression-from if not self.recompress and not target.recreate_rechunkify: for chunk_id, size, csize in item.chunks: self.cache.chunk_incref(chunk_id, target.stats) @@ -1412,13 +1413,22 @@ class ArchiveRecreater: new_chunks = self.process_partial_chunks(target) chunk_iterator = self.create_chunk_iterator(archive, target, item) consume(chunk_iterator, len(new_chunks)) + compress = self.compression_decider1.decide(item.path) for chunk in chunk_iterator: + chunk.meta['compress'] = compress chunk_id = self.key.id_hash(chunk.data) if chunk_id in self.seen_chunks: new_chunks.append(self.cache.chunk_incref(chunk_id, target.stats)) else: - # TODO: detect / skip / --always-recompress - chunk_id, size, csize = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=self.recompress) + compression_spec, chunk = self.key.compression_decider2.decide(chunk) + overwrite = self.recompress + if self.recompress and not self.always_recompress and chunk_id in self.cache.chunks: + # Check if this chunk is already compressed the way we want it + old_chunk = self.key.decrypt(None, self.repository.get(chunk_id), decompress=False) + if Compressor.detect(old_chunk.data).name == compression_spec['name']: + # Stored chunk has the same compression we wanted + overwrite = False + chunk_id, size, csize = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite) new_chunks.append((chunk_id, size, csize)) self.seen_chunks.add(chunk_id) if self.recompress: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index d58ebe45..b03b0a4c 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -957,6 +957,7 @@ class Archiver: exclude_caches=args.exclude_caches, exclude_if_present=args.exclude_if_present, keep_tag_files=args.keep_tag_files, chunker_params=args.chunker_params, compression=args.compression, compression_files=args.compression_files, + always_recompress=args.always_recompress, progress=args.progress, stats=args.stats, file_status_printer=self.print_file_status, dry_run=args.dry_run) @@ -2098,6 +2099,9 @@ class Archiver: 'zlib,0 .. zlib,9 == zlib (with level 0..9),\n' 'lzma == lzma (default level 6),\n' 'lzma,0 .. lzma,9 == lzma (with level 0..9).') + archive_group.add_argument('--always-recompress', dest='always_recompress', action='store_true', + help='always recompress chunks, don\'t skip chunks already compressed with the same' + 'algorithm.') archive_group.add_argument('--compression-from', dest='compression_files', type=argparse.FileType('r'), action='append', metavar='COMPRESSIONCONFIG', help='read compression patterns from COMPRESSIONCONFIG, one per line') diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index 6c42493f..50785ea1 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -6,6 +6,8 @@ except ImportError: from .helpers import Buffer +API_VERSION = 2 + cdef extern from "lz4.h": int LZ4_compress_limitedOutput(const char* source, char* dest, int inputSize, int maxOutputSize) nogil int LZ4_decompress_safe(const char* source, char* dest, int inputSize, int maxOutputSize) nogil diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 2324d32a..f4c55383 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -84,11 +84,13 @@ class PlaceholderError(Error): def check_extension_modules(): - from . import platform + from . import platform, compress if hashindex.API_VERSION != 3: raise ExtensionModuleError if chunker.API_VERSION != 2: raise ExtensionModuleError + if compress.API_VERSION != 2: + raise ExtensionModuleError if crypto.API_VERSION != 3: raise ExtensionModuleError if platform.API_VERSION != 3: From 93b1cf3453a7902744dc6bec2d9bd953158c80c9 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 2 Aug 2016 15:53:29 +0200 Subject: [PATCH 0109/1387] recreate: --target --- src/borg/archive.py | 18 ++++++++++-------- src/borg/archiver.py | 11 ++++++++++- src/borg/testsuite/archiver.py | 22 ++++++++++++++++++++++ 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index fc131d51..7a4d7e87 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1331,10 +1331,10 @@ class ArchiveRecreater: self.interrupt = False self.errors = False - def recreate(self, archive_name, comment=None): + def recreate(self, archive_name, comment=None, target_name=None): assert not self.is_temporary_archive(archive_name) archive = self.open_archive(archive_name) - target, resume_from = self.create_target_or_resume(archive) + target, resume_from = self.create_target_or_resume(archive, target_name) if self.exclude_if_present or self.exclude_caches: self.matcher_add_tagged_dirs(archive) if self.matcher.empty() and not self.recompress and not target.recreate_rechunkify and comment is None: @@ -1344,7 +1344,8 @@ class ArchiveRecreater: self.process_items(archive, target, resume_from) except self.Interrupted as e: return self.save(archive, target, completed=False, metadata=e.metadata) - return self.save(archive, target, comment) + replace_original = target_name is None + return self.save(archive, target, comment, replace_original=replace_original) def process_items(self, archive, target, resume_from=None): matcher = self.matcher @@ -1475,7 +1476,7 @@ class ArchiveRecreater: logger.debug('Copied %d chunks from a partially processed item', len(partial_chunks)) return partial_chunks - def save(self, archive, target, comment=None, completed=True, metadata=None): + def save(self, archive, target, comment=None, completed=True, metadata=None, replace_original=True): """Save target archive. If completed, replace source. If not, save temporary with additional 'metadata' dict.""" if self.dry_run: return completed @@ -1487,8 +1488,9 @@ class ArchiveRecreater: 'cmdline': archive.metadata[b'cmdline'], 'recreate_cmdline': sys.argv, }) - archive.delete(Statistics(), progress=self.progress) - target.rename(archive.name) + if replace_original: + archive.delete(Statistics(), progress=self.progress) + target.rename(archive.name) if self.stats: target.end = datetime.utcnow() log_multi(DASHES, @@ -1540,11 +1542,11 @@ class ArchiveRecreater: matcher.add(tag_files, True) matcher.add(tagged_dirs, False) - def create_target_or_resume(self, archive): + def create_target_or_resume(self, archive, target_name=None): """Create new target archive or resume from temporary archive, if it exists. Return archive, resume from path""" if self.dry_run: return self.FakeTargetArchive(), None - target_name = archive.name + '.recreate' + target_name = target_name or archive.name + '.recreate' resume = target_name in self.manifest.archives target, resume_from = None, None if resume: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index b03b0a4c..2e6c05f2 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -969,8 +969,11 @@ class Archiver: if recreater.is_temporary_archive(name): self.print_error('Refusing to work on temporary archive of prior recreate: %s', name) return self.exit_code - recreater.recreate(name, args.comment) + recreater.recreate(name, args.comment, args.target) else: + if args.target is not None: + self.print_error('--target: Need to specify single archive') + return self.exit_code for archive in manifest.list_archive_infos(sort_by='ts'): name = archive.name if recreater.is_temporary_archive(name): @@ -2036,6 +2039,8 @@ class Archiver: archive that is built during the operation exists at the same time at ".recreate". The new archive will have a different archive ID. + With --target the original archive is not replaced, instead a new archive is created. + When rechunking space usage can be substantial, expect at least the entire deduplicated size of the archives using the previous chunker params. When recompressing approximately 1 % of the repository size or 512 MB @@ -2081,6 +2086,10 @@ class Archiver: help='keep tag files of excluded caches/directories') archive_group = subparser.add_argument_group('Archive options') + archive_group.add_argument('--target', dest='target', metavar='TARGET', default=None, + type=archivename_validator(), + help='create a new archive with the name ARCHIVE, do not replace existing archive ' + '(only applies for a single archive)') archive_group.add_argument('--comment', dest='comment', metavar='COMMENT', default=None, help='add a comment text to the archive') archive_group.add_argument('--timestamp', dest='timestamp', diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 950256be..2df01b29 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1522,6 +1522,28 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', self.repository_location, exit_code=1) assert not os.path.exists(self.repository_location) + def test_recreate_target_rc(self): + self.cmd('init', self.repository_location) + output = self.cmd('recreate', self.repository_location, '--target=asdf', exit_code=2) + assert 'Need to specify single archive' in output + + def test_recreate_target(self): + self.create_test_files() + self.cmd('init', self.repository_location) + archive = self.repository_location + '::test0' + self.cmd('create', archive, 'input') + original_archive = self.cmd('list', self.repository_location) + self.cmd('recreate', archive, 'input/dir2', '-e', 'input/dir2/file3', '--target=new-archive') + archives = self.cmd('list', self.repository_location) + assert original_archive in archives + assert 'new-archive' in archives + + archive = self.repository_location + '::new-archive' + listing = self.cmd('list', '--short', archive) + assert 'file1' not in listing + assert 'dir2/file2' in listing + assert 'dir2/file3' not in listing + def test_recreate_basic(self): self.create_test_files() self.create_regular_file('dir2/file3', size=1024 * 80) From f0e9a55ebfedd0a80237d5df6ee7dc7a6132e692 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 9 Aug 2016 21:32:03 +0200 Subject: [PATCH 0110/1387] recreate: document that absolute patterns won't match --- src/borg/archiver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 2e6c05f2..98a02df6 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2011,6 +2011,9 @@ class Archiver: as in "borg create". If PATHs are specified the resulting archive will only contain files from these PATHs. + Note that all paths in an archive are relative, therefore absolute patterns/paths + will *not* match (--exclude, --exclude-from, --compression-from, PATHs). + --compression: all chunks seen will be stored using the given method. Due to how Borg stores compressed size information this might display incorrect information for archives that were not recreated at the same time. From 585407f4f4d2016a418a067521fdabb82089cb6a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 12 Jun 2016 19:06:39 +0200 Subject: [PATCH 0111/1387] introduce ArchiveItem, see #1157 --- src/borg/item.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/borg/item.py b/src/borg/item.py index 90289dbe..c062f09c 100644 --- a/src/borg/item.py +++ b/src/borg/item.py @@ -204,3 +204,36 @@ class Key(PropDict): enc_hmac_key = PropDict._make_property('enc_hmac_key', bytes) id_key = PropDict._make_property('id_key', bytes) chunk_seed = PropDict._make_property('chunk_seed', int) + + +class ArchiveItem(PropDict): + """ + ArchiveItem abstraction that deals with validation and the low-level details internally: + + An ArchiveItem is created either from msgpack unpacker output, from another dict, from kwargs or + built step-by-step by setting attributes. + + msgpack gives us a dict with bytes-typed keys, just give it to ArchiveItem(d) and use arch.xxx later. + + If a ArchiveItem shall be serialized, give as_dict() method output to msgpack packer. + """ + + VALID_KEYS = {'version', 'name', 'items', 'cmdline', 'hostname', 'username', 'time', 'time_end', + 'comment', 'chunker_params', + 'recreate_cmdline', 'recreate_source_id', 'recreate_args'} # str-typed keys + + __slots__ = ("_dict", ) # avoid setting attributes not supported by properties + + version = PropDict._make_property('version', int) + name = PropDict._make_property('name', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode) + items = PropDict._make_property('items', list) + cmdline = PropDict._make_property('cmdline', list) # list of s-e-str + hostname = PropDict._make_property('hostname', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode) + username = PropDict._make_property('username', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode) + time = PropDict._make_property('time', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode) + time_end = PropDict._make_property('time_end', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode) + comment = PropDict._make_property('comment', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode) + chunker_params = PropDict._make_property('chunker_params', tuple) + recreate_source_id = PropDict._make_property('recreate_source_id', bytes) + recreate_cmdline = PropDict._make_property('recreate_cmdline', list) # list of s-e-str + recreate_args = PropDict._make_property('recreate_args', list) # list of s-e-str From c8922c8b3dfe79a56ae99587647b2cfc99bf8233 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Aug 2016 01:11:33 +0200 Subject: [PATCH 0112/1387] use ArchiveItem --- src/borg/archive.py | 70 +++++++++++++++++----------------- src/borg/archiver.py | 14 +++---- src/borg/cache.py | 9 ++--- src/borg/constants.py | 2 - src/borg/fuse.py | 2 +- src/borg/item.py | 4 +- src/borg/testsuite/archive.py | 4 +- src/borg/testsuite/archiver.py | 2 +- 8 files changed, 53 insertions(+), 54 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 7a4d7e87..561d5ef8 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -35,7 +35,7 @@ from .helpers import ProgressIndicatorPercent, log_multi from .helpers import PathPrefixPattern, FnmatchPattern from .helpers import consume from .helpers import CompressionDecider1, CompressionDecider2, CompressionSpec -from .item import Item +from .item import Item, ArchiveItem from .key import key_factory from .platform import acl_get, acl_set, set_flags, get_flags, swidth from .remote import cache_if_remote @@ -277,29 +277,28 @@ class Archive: def _load_meta(self, id): _, data = self.key.decrypt(id, self.repository.get(id)) - metadata = msgpack.unpackb(data) - if metadata[b'version'] != 1: + metadata = ArchiveItem(internal_dict=msgpack.unpackb(data)) + if metadata.version != 1: raise Exception('Unknown archive metadata version') return metadata def load(self, id): self.id = id self.metadata = self._load_meta(self.id) - decode_dict(self.metadata, ARCHIVE_TEXT_KEYS) - self.metadata[b'cmdline'] = [safe_decode(arg) for arg in self.metadata[b'cmdline']] - self.name = self.metadata[b'name'] + self.metadata.cmdline = [safe_decode(arg) for arg in self.metadata.cmdline] + self.name = self.metadata.name @property def ts(self): """Timestamp of archive creation (start) in UTC""" - ts = self.metadata[b'time'] + ts = self.metadata.time return parse_timestamp(ts) @property def ts_end(self): """Timestamp of archive creation (end) in UTC""" # fall back to time if there is no time_end present in metadata - ts = self.metadata.get(b'time_end') or self.metadata[b'time'] + ts = self.metadata.get('time_end') or self.metadata.time return parse_timestamp(ts) @property @@ -336,7 +335,7 @@ Number of files: {0.stats.nfiles}'''.format( return filter(item) if filter else True def iter_items(self, filter=None, preload=False): - for item in self.pipeline.unpack_many(self.metadata[b'items'], preload=preload, + for item in self.pipeline.unpack_many(self.metadata.items, preload=preload, filter=lambda item: self.item_filter(item, filter)): yield item @@ -366,7 +365,7 @@ Number of files: {0.stats.nfiles}'''.format( metadata = { 'version': 1, 'name': name, - 'comment': comment, + 'comment': comment or '', 'items': self.items_buffer.chunks, 'cmdline': sys.argv, 'hostname': socket.gethostname(), @@ -376,10 +375,11 @@ Number of files: {0.stats.nfiles}'''.format( 'chunker_params': self.chunker_params, } metadata.update(additional_metadata or {}) - data = msgpack.packb(StableDict(metadata), unicode_errors='surrogateescape') + metadata = ArchiveItem(metadata) + data = msgpack.packb(metadata.as_dict(), unicode_errors='surrogateescape') self.id = self.key.id_hash(data) self.cache.add_chunk(self.id, Chunk(data), self.stats) - self.manifest.archives[name] = {'id': self.id, 'time': metadata['time']} + self.manifest.archives[name] = {'id': self.id, 'time': metadata.time} self.manifest.write() self.repository.commit() self.cache.commit() @@ -400,7 +400,7 @@ Number of files: {0.stats.nfiles}'''.format( cache.begin_txn() stats = Statistics() add(self.id) - for id, chunk in zip(self.metadata[b'items'], self.repository.get_many(self.metadata[b'items'])): + for id, chunk in zip(self.metadata.items, self.repository.get_many(self.metadata.items)): add(id) _, data = self.key.decrypt(id, chunk) unpacker.feed(data) @@ -588,12 +588,12 @@ Number of files: {0.stats.nfiles}'''.format( raise def set_meta(self, key, value): - metadata = StableDict(self._load_meta(self.id)) - metadata[key] = value - data = msgpack.packb(metadata, unicode_errors='surrogateescape') + metadata = self._load_meta(self.id) + setattr(metadata, key, value) + data = msgpack.packb(metadata.as_dict(), unicode_errors='surrogateescape') new_id = self.key.id_hash(data) self.cache.add_chunk(new_id, Chunk(data), self.stats) - self.manifest.archives[self.name] = {'id': new_id, 'time': metadata[b'time']} + self.manifest.archives[self.name] = {'id': new_id, 'time': metadata.time} self.cache.chunk_decref(self.id, self.stats) self.id = new_id @@ -602,7 +602,7 @@ Number of files: {0.stats.nfiles}'''.format( raise self.AlreadyExists(name) oldname = self.name self.name = name - self.set_meta(b'name', name) + self.set_meta('name', name) del self.manifest.archives[oldname] def delete(self, stats, progress=False, forced=False): @@ -625,7 +625,7 @@ Number of files: {0.stats.nfiles}'''.format( error = False try: unpacker = msgpack.Unpacker(use_list=False) - items_ids = self.metadata[b'items'] + items_ids = self.metadata.items pi = ProgressIndicatorPercent(total=len(items_ids), msg="Decrementing references %3.0f%%", same_line=True) for (i, (items_id, data)) in enumerate(zip(items_ids, self.repository.get_many(items_ids))): if progress: @@ -1075,8 +1075,9 @@ class ArchiveChecker: except (TypeError, ValueError, StopIteration): continue if valid_archive(archive): - logger.info('Found archive %s', archive[b'name'].decode('utf-8')) - manifest.archives[archive[b'name'].decode('utf-8')] = {b'id': chunk_id, b'time': archive[b'time']} + archive = ArchiveItem(internal_dict=archive) + logger.info('Found archive %s', archive.name) + manifest.archives[archive.name] = {b'id': chunk_id, b'time': archive.time} logger.info('Manifest rebuild complete.') return manifest @@ -1187,7 +1188,7 @@ class ArchiveChecker: return required_item_keys.issubset(keys) and keys.issubset(item_keys) i = 0 - for state, items in groupby(archive[b'items'], missing_chunk_detector): + for state, items in groupby(archive.items, missing_chunk_detector): items = list(items) if state % 2: for chunk_id in items: @@ -1241,11 +1242,10 @@ class ArchiveChecker: mark_as_possibly_superseded(archive_id) cdata = self.repository.get(archive_id) _, data = self.key.decrypt(archive_id, cdata) - archive = StableDict(msgpack.unpackb(data)) - if archive[b'version'] != 1: + archive = ArchiveItem(internal_dict=msgpack.unpackb(data)) + if archive.version != 1: raise Exception('Unknown archive metadata version') - decode_dict(archive, ARCHIVE_TEXT_KEYS) - archive[b'cmdline'] = [safe_decode(arg) for arg in archive[b'cmdline']] + archive.cmdline = [safe_decode(arg) for arg in archive.cmdline] items_buffer = ChunkBuffer(self.key) items_buffer.write_chunk = add_callback for item in robust_iterator(archive): @@ -1253,10 +1253,10 @@ class ArchiveChecker: verify_file_chunks(item) items_buffer.add(item) items_buffer.flush(flush=True) - for previous_item_id in archive[b'items']: + for previous_item_id in archive.items: mark_as_possibly_superseded(previous_item_id) - archive[b'items'] = items_buffer.chunks - data = msgpack.packb(archive, unicode_errors='surrogateescape') + archive.items = items_buffer.chunks + data = msgpack.packb(archive.as_dict(), unicode_errors='surrogateescape') new_archive_id = self.key.id_hash(data) cdata = self.key.encrypt(Chunk(data)) add_reference(new_archive_id, len(data), len(cdata), cdata) @@ -1483,9 +1483,9 @@ class ArchiveRecreater: if completed: timestamp = archive.ts.replace(tzinfo=None) if comment is None: - comment = archive.metadata.get(b'comment', '') + comment = archive.metadata.get('comment', '') target.save(timestamp=timestamp, comment=comment, additional_metadata={ - 'cmdline': archive.metadata[b'cmdline'], + 'cmdline': archive.metadata.cmdline, 'recreate_cmdline': sys.argv, }) if replace_original: @@ -1554,7 +1554,7 @@ class ArchiveRecreater: if not target: target = self.create_target_archive(target_name) # If the archives use the same chunker params, then don't rechunkify - target.recreate_rechunkify = tuple(archive.metadata.get(b'chunker_params')) != self.chunker_params + target.recreate_rechunkify = tuple(archive.metadata.get('chunker_params')) != self.chunker_params return target, resume_from def try_resume(self, archive, target_name): @@ -1573,7 +1573,7 @@ class ArchiveRecreater: return target, resume_from def incref_partial_chunks(self, source_archive, target_archive): - target_archive.recreate_partial_chunks = source_archive.metadata.get(b'recreate_partial_chunks', []) + target_archive.recreate_partial_chunks = source_archive.metadata.get('recreate_partial_chunks', []) for chunk_id, size, csize in target_archive.recreate_partial_chunks: if not self.cache.seen_chunk(chunk_id): try: @@ -1606,8 +1606,8 @@ class ArchiveRecreater: return item def can_resume(self, archive, old_target, target_name): - resume_id = old_target.metadata[b'recreate_source_id'] - resume_args = [safe_decode(arg) for arg in old_target.metadata[b'recreate_args']] + resume_id = old_target.metadata.recreate_source_id + resume_args = [safe_decode(arg) for arg in old_target.metadata.recreate_args] if resume_id != archive.id: logger.warning('Source archive changed, will discard %s and start over', target_name) logger.warning('Saved fingerprint: %s', bin_to_hex(resume_id)) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 98a02df6..4fc46e42 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -679,8 +679,8 @@ class Archiver: archive2 = Archive(repository, key, manifest, args.archive2, consider_part_files=args.consider_part_files) - can_compare_chunk_ids = archive1.metadata.get(b'chunker_params', False) == archive2.metadata.get( - b'chunker_params', True) or args.same_chunker_params + can_compare_chunk_ids = archive1.metadata.get('chunker_params', False) == archive2.metadata.get( + 'chunker_params', True) or args.same_chunker_params if not can_compare_chunk_ids: self.print_warning('--chunker-params might be different between archives, diff will be slow.\n' 'If you know for certain that they are the same, pass --same-chunker-params ' @@ -831,14 +831,14 @@ class Archiver: stats = archive.calc_stats(cache) print('Archive name: %s' % archive.name) print('Archive fingerprint: %s' % archive.fpr) - print('Comment: %s' % archive.metadata.get(b'comment', '')) - print('Hostname: %s' % archive.metadata[b'hostname']) - print('Username: %s' % archive.metadata[b'username']) + print('Comment: %s' % archive.metadata.get('comment', '')) + print('Hostname: %s' % archive.metadata.hostname) + print('Username: %s' % archive.metadata.username) print('Time (start): %s' % format_time(to_localtime(archive.ts))) print('Time (end): %s' % format_time(to_localtime(archive.ts_end))) print('Duration: %s' % archive.duration_from_meta) print('Number of files: %d' % stats.nfiles) - print('Command line: %s' % format_cmdline(archive.metadata[b'cmdline'])) + print('Command line: %s' % format_cmdline(archive.metadata.cmdline)) print(DASHES) print(STATS_HEADER) print(str(stats)) @@ -1009,7 +1009,7 @@ class Archiver: """dump (decrypted, decompressed) archive items metadata (not: data)""" archive = Archive(repository, key, manifest, args.location.archive, consider_part_files=args.consider_part_files) - for i, item_id in enumerate(archive.metadata[b'items']): + for i, item_id in enumerate(archive.metadata.items): _, data = key.decrypt(item_id, repository.get(item_id)) filename = '%06d_%s.items' % (i, bin_to_hex(item_id)) print('Dumping', filename) diff --git a/src/borg/cache.py b/src/borg/cache.py index df4b9086..3f685a94 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -16,7 +16,7 @@ from .helpers import get_cache_dir from .helpers import decode_dict, int_to_bigint, bigint_to_int, bin_to_hex from .helpers import format_file_size from .helpers import yes -from .item import Item +from .item import Item, ArchiveItem from .key import PlaintextKey from .locking import Lock from .platform import SaveFile @@ -290,12 +290,11 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" cdata = repository.get(archive_id) _, data = key.decrypt(archive_id, cdata) chunk_idx.add(archive_id, 1, len(data), len(cdata)) - archive = msgpack.unpackb(data) - if archive[b'version'] != 1: + archive = ArchiveItem(internal_dict=msgpack.unpackb(data)) + if archive.version != 1: raise Exception('Unknown archive metadata version') - decode_dict(archive, (b'name',)) unpacker = msgpack.Unpacker() - for item_id, chunk in zip(archive[b'items'], repository.get_many(archive[b'items'])): + for item_id, chunk in zip(archive.items, repository.get_many(archive.items)): _, data = key.decrypt(item_id, chunk) chunk_idx.add(item_id, 1, len(data), len(chunk)) unpacker.feed(data) diff --git a/src/borg/constants.py b/src/borg/constants.py index d83c41f2..d6f26d11 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -15,8 +15,6 @@ ARCHIVE_KEYS = frozenset(['version', 'name', 'items', 'cmdline', 'hostname', 'us # this is the set of keys that are always present in archives: REQUIRED_ARCHIVE_KEYS = frozenset(['version', 'name', 'items', 'cmdline', 'time', ]) -ARCHIVE_TEXT_KEYS = (b'name', b'comment', b'hostname', b'username', b'time', b'time_end') - # default umask, overriden by --umask, defaults to read/write only for owner UMASK_DEFAULT = 0o077 diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 3113515f..f5375924 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -117,7 +117,7 @@ class FuseOperations(llfuse.Operations): """Build fuse inode hierarchy from archive metadata """ unpacker = msgpack.Unpacker() - for key, chunk in zip(archive.metadata[b'items'], self.repository.get_many(archive.metadata[b'items'])): + for key, chunk in zip(archive.metadata.items, self.repository.get_many(archive.metadata.items)): _, data = self.key.decrypt(key, chunk) unpacker.feed(data) for item in unpacker: diff --git a/src/borg/item.py b/src/borg/item.py index c062f09c..93999e20 100644 --- a/src/borg/item.py +++ b/src/borg/item.py @@ -220,7 +220,8 @@ class ArchiveItem(PropDict): VALID_KEYS = {'version', 'name', 'items', 'cmdline', 'hostname', 'username', 'time', 'time_end', 'comment', 'chunker_params', - 'recreate_cmdline', 'recreate_source_id', 'recreate_args'} # str-typed keys + 'recreate_cmdline', 'recreate_source_id', 'recreate_args', 'recreate_partial_chunks', + } # str-typed keys __slots__ = ("_dict", ) # avoid setting attributes not supported by properties @@ -237,3 +238,4 @@ class ArchiveItem(PropDict): recreate_source_id = PropDict._make_property('recreate_source_id', bytes) recreate_cmdline = PropDict._make_property('recreate_cmdline', list) # list of s-e-str recreate_args = PropDict._make_property('recreate_args', list) # list of s-e-str + recreate_partial_chunks = PropDict._make_property('recreate_partial_chunks', list) # list of tuples diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index 19db1a44..49648ef4 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -8,7 +8,7 @@ import msgpack from ..archive import Archive, CacheChunkBuffer, RobustUnpacker, valid_msgpacked_dict, ITEM_KEYS, Statistics from ..archive import BackupOSError, backup_io, backup_io_iter -from ..item import Item +from ..item import Item, ArchiveItem from ..key import PlaintextKey from ..helpers import Manifest from . import BaseTestCase @@ -77,7 +77,7 @@ class ArchiveTimestampTestCase(BaseTestCase): key = PlaintextKey(repository) manifest = Manifest(repository, key) a = Archive(repository, key, manifest, 'test', create=True) - a.metadata = {b'time': isoformat} + a.metadata = ArchiveItem(time=isoformat) self.assert_equal(a.ts, expected) def test_with_microseconds(self): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 2df01b29..1901b8d4 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1859,7 +1859,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): def test_missing_archive_item_chunk(self): archive, repository = self.open_archive('archive1') with repository: - repository.delete(archive.metadata[b'items'][-5]) + repository.delete(archive.metadata.items[-5]) repository.commit() self.cmd('check', self.repository_location, exit_code=1) self.cmd('check', '--repair', self.repository_location, exit_code=0) From b6d0eb99a5a24b824e0e38d8e607400bdd1c7b0b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Aug 2016 02:01:13 +0200 Subject: [PATCH 0113/1387] add and use ManifestItem --- src/borg/helpers.py | 31 ++++++++++++++++--------------- src/borg/item.py | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index f4c55383..ef42ee57 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -114,6 +114,7 @@ class Manifest: @classmethod def load(cls, repository, key=None): + from .item import ManifestItem from .key import key_factory from .repository import Repository try: @@ -125,27 +126,27 @@ class Manifest: manifest = cls(key, repository) _, data = key.decrypt(None, cdata) manifest.id = key.id_hash(data) - m = msgpack.unpackb(data) - if not m.get(b'version') == 1: + m = ManifestItem(internal_dict=msgpack.unpackb(data)) + if m.get('version') != 1: raise ValueError('Invalid manifest version') - manifest.archives = dict((k.decode('utf-8'), v) for k, v in m[b'archives'].items()) - manifest.timestamp = m.get(b'timestamp') - if manifest.timestamp: - manifest.timestamp = manifest.timestamp.decode('ascii') - manifest.config = m[b'config'] + manifest.archives = {safe_decode(k): v for k, v in m.archives.items()} + manifest.timestamp = m.get('timestamp') + manifest.config = m.config # valid item keys are whatever is known in the repo or every key we know - manifest.item_keys = ITEM_KEYS | frozenset(key.decode() for key in m.get(b'item_keys', [])) + manifest.item_keys = ITEM_KEYS | frozenset(key.decode() for key in m.get('item_keys', [])) return manifest, key def write(self): + from .item import ManifestItem self.timestamp = datetime.utcnow().isoformat() - data = msgpack.packb(StableDict({ - 'version': 1, - 'archives': self.archives, - 'timestamp': self.timestamp, - 'config': self.config, - 'item_keys': tuple(self.item_keys), - })) + manifest = ManifestItem( + version=1, + archives=self.archives, + timestamp=self.timestamp, + config=self.config, + item_keys=tuple(self.item_keys), + ) + data = msgpack.packb(manifest.as_dict()) self.id = self.key.id_hash(data) self.repository.put(self.MANIFEST_ID, self.key.encrypt(Chunk(data))) diff --git a/src/borg/item.py b/src/borg/item.py index 93999e20..0a0908c0 100644 --- a/src/borg/item.py +++ b/src/borg/item.py @@ -239,3 +239,26 @@ class ArchiveItem(PropDict): recreate_cmdline = PropDict._make_property('recreate_cmdline', list) # list of s-e-str recreate_args = PropDict._make_property('recreate_args', list) # list of s-e-str recreate_partial_chunks = PropDict._make_property('recreate_partial_chunks', list) # list of tuples + + +class ManifestItem(PropDict): + """ + ManifestItem abstraction that deals with validation and the low-level details internally: + + A ManifestItem is created either from msgpack unpacker output, from another dict, from kwargs or + built step-by-step by setting attributes. + + msgpack gives us a dict with bytes-typed keys, just give it to ManifestItem(d) and use manifest.xxx later. + + If a ManifestItem shall be serialized, give as_dict() method output to msgpack packer. + """ + + VALID_KEYS = {'version', 'archives', 'timestamp', 'config', 'item_keys', } # str-typed keys + + __slots__ = ("_dict", ) # avoid setting attributes not supported by properties + + version = PropDict._make_property('version', int) + archives = PropDict._make_property('archives', dict) # name -> dict + timestamp = PropDict._make_property('time', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode) + config = PropDict._make_property('config', dict) + item_keys = PropDict._make_property('item_keys', tuple) From 1f056d9e8ab54066a9242cde7d89c83f2fd6c1dd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Aug 2016 04:17:41 +0200 Subject: [PATCH 0114/1387] more safe interface for manifest.archives --- src/borg/archive.py | 38 ++++++++++---------- src/borg/archiver.py | 8 ++--- src/borg/cache.py | 8 ++--- src/borg/fuse.py | 6 ++-- src/borg/helpers.py | 84 +++++++++++++++++++++++++++++++++++--------- 5 files changed, 99 insertions(+), 45 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 561d5ef8..09a3d3f7 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -269,10 +269,10 @@ class Archive: break i += 1 else: - if name not in self.manifest.archives: + info = self.manifest.archives.get(name) + if info is None: raise self.DoesNotExist(name) - info = self.manifest.archives[name] - self.load(info[b'id']) + self.load(info.id) self.zeros = b'\0' * (1 << chunker_params[1]) def _load_meta(self, id): @@ -379,7 +379,7 @@ Number of files: {0.stats.nfiles}'''.format( data = msgpack.packb(metadata.as_dict(), unicode_errors='surrogateescape') self.id = self.key.id_hash(data) self.cache.add_chunk(self.id, Chunk(data), self.stats) - self.manifest.archives[name] = {'id': self.id, 'time': metadata.time} + self.manifest.archives[name] = (self.id, metadata.time) self.manifest.write() self.repository.commit() self.cache.commit() @@ -593,7 +593,7 @@ Number of files: {0.stats.nfiles}'''.format( data = msgpack.packb(metadata.as_dict(), unicode_errors='surrogateescape') new_id = self.key.id_hash(data) self.cache.add_chunk(new_id, Chunk(data), self.stats) - self.manifest.archives[self.name] = {'id': new_id, 'time': metadata.time} + self.manifest.archives[self.name] = (new_id, metadata.time) self.cache.chunk_decref(self.id, self.stats) self.id = new_id @@ -844,7 +844,7 @@ Number of files: {0.stats.nfiles}'''.format( @staticmethod def list_archives(repository, key, manifest, cache=None): # expensive! see also Manifest.list_archive_infos. - for name, info in manifest.archives.items(): + for name in manifest.archives: yield Archive(repository, key, manifest, name, cache=cache) @staticmethod @@ -1077,7 +1077,7 @@ class ArchiveChecker: if valid_archive(archive): archive = ArchiveItem(internal_dict=archive) logger.info('Found archive %s', archive.name) - manifest.archives[archive.name] = {b'id': chunk_id, b'time': archive.time} + manifest.archives[archive.name] = (chunk_id, archive.time) logger.info('Manifest rebuild complete.') return manifest @@ -1216,28 +1216,30 @@ class ArchiveChecker: if archive is None: # we need last N or all archives - archive_items = sorted(self.manifest.archives.items(), reverse=True, - key=lambda name_info: name_info[1][b'time']) + archive_infos = self.manifest.archives.list(sort_by='ts', reverse=True) if prefix is not None: - archive_items = [item for item in archive_items if item[0].startswith(prefix)] - num_archives = len(archive_items) + archive_infos = [info for info in archive_infos if info.name.startswith(prefix)] + num_archives = len(archive_infos) end = None if last is None else min(num_archives, last) else: # we only want one specific archive - archive_items = [item for item in self.manifest.archives.items() if item[0] == archive] - if not archive_items: + info = self.manifest.archives.get(archive) + if info is None: logger.error("Archive '%s' not found.", archive) + archive_infos = [] + else: + archive_infos = [info] num_archives = 1 end = 1 with cache_if_remote(self.repository) as repository: - for i, (name, info) in enumerate(archive_items[:end]): - logger.info('Analyzing archive {} ({}/{})'.format(name, num_archives - i, num_archives)) - archive_id = info[b'id'] + for i, info in enumerate(archive_infos[:end]): + logger.info('Analyzing archive {} ({}/{})'.format(info.name, num_archives - i, num_archives)) + archive_id = info.id if archive_id not in self.chunks: logger.error('Archive metadata block is missing!') self.error_found = True - del self.manifest.archives[name] + del self.manifest.archives[info.name] continue mark_as_possibly_superseded(archive_id) cdata = self.repository.get(archive_id) @@ -1260,7 +1262,7 @@ class ArchiveChecker: new_archive_id = self.key.id_hash(data) cdata = self.key.encrypt(Chunk(data)) add_reference(new_archive_id, len(data), len(cdata), cdata) - info[b'id'] = new_archive_id + self.manifest.archives[info.name] = (new_archive_id, info.ts) def orphan_chunks_check(self): if self.check_all: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 4fc46e42..78fdfe7a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -734,7 +734,7 @@ class Archiver: msg.append("This repository seems to have no manifest, so we can't tell anything about its contents.") else: msg.append("You requested to completely DELETE the repository *including* all archives it contains:") - for archive_info in manifest.list_archive_infos(sort_by='ts'): + for archive_info in manifest.archives.list(sort_by='ts'): msg.append(format_archive(archive_info)) msg.append("Type 'YES' if you understand this and want to continue: ") msg = '\n'.join(msg) @@ -812,7 +812,7 @@ class Archiver: format = "{archive:<36} {time} [{id}]{NL}" formatter = ArchiveFormatter(format) - for archive_info in manifest.list_archive_infos(sort_by='ts'): + for archive_info in manifest.archives.list(sort_by='ts'): if args.prefix and not archive_info.name.startswith(args.prefix): continue write(safe_encode(formatter.format_item(archive_info))) @@ -857,7 +857,7 @@ class Archiver: '"keep-secondly", "keep-minutely", "keep-hourly", "keep-daily", ' '"keep-weekly", "keep-monthly" or "keep-yearly" settings must be specified.') return self.exit_code - archives_checkpoints = manifest.list_archive_infos(sort_by='ts', reverse=True) # just a ArchiveInfo list + archives_checkpoints = manifest.archives.list(sort_by='ts', reverse=True) # just a ArchiveInfo list if args.prefix: archives_checkpoints = [arch for arch in archives_checkpoints if arch.name.startswith(args.prefix)] is_checkpoint = re.compile(r'\.checkpoint(\.\d+)?$').search @@ -974,7 +974,7 @@ class Archiver: if args.target is not None: self.print_error('--target: Need to specify single archive') return self.exit_code - for archive in manifest.list_archive_infos(sort_by='ts'): + for archive in manifest.archives.list(sort_by='ts'): name = archive.name if recreater.is_temporary_archive(name): continue diff --git a/src/borg/cache.py b/src/borg/cache.py index 3f685a94..f325ff25 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -279,7 +279,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" return set() def repo_archives(): - return set(info[b'id'] for info in self.manifest.archives.values()) + return set(info.id for info in self.manifest.archives.list()) def cleanup_outdated(ids): for id in ids: @@ -318,9 +318,9 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" return chunk_idx def lookup_name(archive_id): - for name, info in self.manifest.archives.items(): - if info[b'id'] == archive_id: - return name + for info in self.manifest.archives.list(): + if info.id == archive_id: + return info.name def create_master_idx(chunk_idx): logger.info('Synchronizing chunks cache...') diff --git a/src/borg/fuse.py b/src/borg/fuse.py index f5375924..4e7cf10c 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -73,11 +73,11 @@ class FuseOperations(llfuse.Operations): if archive: self.process_archive(archive) else: - for archive_name in manifest.archives: + for name in manifest.archives: # Create archive placeholder inode archive_inode = self._create_dir(parent=1) - self.contents[1][os.fsencode(archive_name)] = archive_inode - self.pending_archives[archive_inode] = Archive(repository, key, manifest, archive_name) + self.contents[1][os.fsencode(name)] = archive_inode + self.pending_archives[archive_inode] = Archive(repository, key, manifest, name) def mount(self, mountpoint, mount_options, foreground=False): """Mount filesystem on *mountpoint* with *mount_options*.""" diff --git a/src/borg/helpers.py b/src/borg/helpers.py index ef42ee57..75929144 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -18,7 +18,7 @@ import time import unicodedata import uuid from binascii import hexlify -from collections import namedtuple, deque +from collections import namedtuple, deque, abc from contextlib import contextmanager from datetime import datetime, timezone, timedelta from fnmatch import translate @@ -97,12 +97,76 @@ def check_extension_modules(): raise ExtensionModuleError +ArchiveInfo = namedtuple('ArchiveInfo', 'name id ts') + + +class Archives(abc.MutableMapping): + """ + Nice wrapper around the archives dict, making sure only valid types/values get in + and we can deal with str keys (and it internally encodes to byte keys) and eiter + str timestamps or datetime timestamps. + """ + def __init__(self): + # key: encoded archive name, value: dict(b'id': bytes_id, b'time': bytes_iso_ts) + self._archives = {} + + def __len__(self): + return len(self._archives) + + def __iter__(self): + return iter(safe_decode(name) for name in self._archives) + + def __getitem__(self, name): + assert isinstance(name, str) + _name = safe_encode(name) + values = self._archives.get(_name) + if values is None: + raise KeyError + ts = parse_timestamp(values[b'time'].decode('utf-8')) + return ArchiveInfo(name=name, id=values[b'id'], ts=ts) + + def __setitem__(self, name, info): + assert isinstance(name, str) + name = safe_encode(name) + assert isinstance(info, tuple) + id, ts = info + assert isinstance(id, bytes) + if isinstance(ts, datetime): + ts = ts.replace(tzinfo=None).isoformat() + assert isinstance(ts, str) + ts = ts.encode() + self._archives[name] = {b'id': id, b'time': ts} + + def __delitem__(self, name): + assert isinstance(name, str) + name = safe_encode(name) + del self._archives[name] + + def list(self, sort_by=None, reverse=False): + # inexpensive Archive.list_archives replacement if we just need .name, .id, .ts + archives = self.values() # [self[name] for name in self] + if sort_by is not None: + archives = sorted(archives, key=attrgetter(sort_by), reverse=reverse) + return archives + + def set_raw_dict(self, d): + """set the dict we get from the msgpack unpacker""" + for k, v in d.items(): + assert isinstance(k, bytes) + assert isinstance(v, dict) and b'id' in v and b'time' in v + self._archives[k] = v + + def get_raw_dict(self): + """get the dict we can give to the msgpack packer""" + return self._archives + + class Manifest: MANIFEST_ID = b'\0' * 32 def __init__(self, key, repository, item_keys=None): - self.archives = {} + self.archives = Archives() self.config = {} self.key = key self.repository = repository @@ -129,7 +193,7 @@ class Manifest: m = ManifestItem(internal_dict=msgpack.unpackb(data)) if m.get('version') != 1: raise ValueError('Invalid manifest version') - manifest.archives = {safe_decode(k): v for k, v in m.archives.items()} + manifest.archives.set_raw_dict(m.archives) manifest.timestamp = m.get('timestamp') manifest.config = m.config # valid item keys are whatever is known in the repo or every key we know @@ -141,7 +205,7 @@ class Manifest: self.timestamp = datetime.utcnow().isoformat() manifest = ManifestItem( version=1, - archives=self.archives, + archives=self.archives.get_raw_dict(), timestamp=self.timestamp, config=self.config, item_keys=tuple(self.item_keys), @@ -150,18 +214,6 @@ class Manifest: self.id = self.key.id_hash(data) self.repository.put(self.MANIFEST_ID, self.key.encrypt(Chunk(data))) - def list_archive_infos(self, sort_by=None, reverse=False): - # inexpensive Archive.list_archives replacement if we just need .name, .id, .ts - ArchiveInfo = namedtuple('ArchiveInfo', 'name id ts') - archives = [] - for name, values in self.archives.items(): - ts = parse_timestamp(values[b'time'].decode('utf-8')) - id = values[b'id'] - archives.append(ArchiveInfo(name=name, id=id, ts=ts)) - if sort_by is not None: - archives = sorted(archives, key=attrgetter(sort_by), reverse=reverse) - return archives - def prune_within(archives, within): multiplier = {'H': 1, 'd': 24, 'w': 24 * 7, 'm': 24 * 31, 'y': 24 * 365} From e3c155e75afe0a01f22bd678d8f00747383f1b19 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Aug 2016 15:41:24 +0200 Subject: [PATCH 0115/1387] use a clean repo to test / build the release --- docs/development.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/development.rst b/docs/development.rst index 05da98b4..480a1706 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -178,6 +178,14 @@ Checklist: git tag -s -m "tagged/signed release X.Y.Z" X.Y.Z +- create a clean repo and use it for the following steps:: + + git clone borg borg-clean + + This makes sure no uncommitted files get into the release archive. + It also will find if you forgot to commit something that is needed. + It also makes sure the vagrant machines only get committed files and + do a fresh start based on that. - run tox and/or binary builds on all supported platforms via vagrant, check for test failures - create a release on PyPi:: From 3c8d86354b4215d7f48b9c367f75a50299fcd2eb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Aug 2016 15:52:19 +0200 Subject: [PATCH 0116/1387] src cleanup: get rid of text-as-bytes in borg.remote --- src/borg/remote.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index ff057b7b..18637cae 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -283,22 +283,23 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. return msgid def handle_error(error, res): - if error == b'DoesNotExist': + error = error.decode('utf-8') + if error == 'DoesNotExist': raise Repository.DoesNotExist(self.location.orig) - elif error == b'AlreadyExists': + elif error == 'AlreadyExists': raise Repository.AlreadyExists(self.location.orig) - elif error == b'CheckNeeded': + elif error == 'CheckNeeded': raise Repository.CheckNeeded(self.location.orig) - elif error == b'IntegrityError': + elif error == 'IntegrityError': raise IntegrityError(res) - elif error == b'PathNotAllowed': + elif error == 'PathNotAllowed': raise PathNotAllowed(*res) - elif error == b'ObjectNotFound': + elif error == 'ObjectNotFound': raise Repository.ObjectNotFound(res[0], self.location.orig) - elif error == b'InvalidRPCMethod': + elif error == 'InvalidRPCMethod': raise InvalidRPCMethod(*res) else: - raise self.RPCError(res.decode('utf-8'), error.decode('utf-8')) + raise self.RPCError(res.decode('utf-8'), error) calls = list(calls) waiting_for = [] From 2f70e3f69a115105e72c0a1571ff028f14629b08 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Aug 2016 16:01:23 +0200 Subject: [PATCH 0117/1387] src cleanup: do not use XXX in strings if just any other string works also XXX is usually used in comments to mark questionable places in the source. --- src/borg/testsuite/key.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index 85697010..b85650a4 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -69,9 +69,9 @@ class TestKey: monkeypatch.setenv('BORG_PASSPHRASE', 'test') key = KeyfileKey.create(self.MockRepository(), self.MockArgs()) assert bytes_to_long(key.enc_cipher.iv, 8) == 0 - manifest = key.encrypt(Chunk(b'XXX')) + manifest = key.encrypt(Chunk(b'ABC')) assert key.extract_nonce(manifest) == 0 - manifest2 = key.encrypt(Chunk(b'XXX')) + manifest2 = key.encrypt(Chunk(b'ABC')) assert manifest != manifest2 assert key.decrypt(None, manifest) == key.decrypt(None, manifest2) assert key.extract_nonce(manifest2) == 1 @@ -91,7 +91,7 @@ class TestKey: assert not keyfile.exists() key = KeyfileKey.create(self.MockRepository(), self.MockArgs()) assert keyfile.exists() - chunk = Chunk(b'XXX') + chunk = Chunk(b'ABC') chunk_id = key.id_hash(chunk.data) chunk_cdata = key.encrypt(chunk) key = KeyfileKey.detect(self.MockRepository(), chunk_cdata) @@ -124,9 +124,9 @@ class TestKey: assert hexlify(key.enc_hmac_key) == b'b885a05d329a086627412a6142aaeb9f6c54ab7950f996dd65587251f6bc0901' assert hexlify(key.enc_key) == b'2ff3654c6daf7381dbbe718d2b20b4f1ea1e34caa6cc65f6bb3ac376b93fed2a' assert key.chunk_seed == -775740477 - manifest = key.encrypt(Chunk(b'XXX')) + manifest = key.encrypt(Chunk(b'ABC')) assert key.extract_nonce(manifest) == 0 - manifest2 = key.encrypt(Chunk(b'XXX')) + manifest2 = key.encrypt(Chunk(b'ABC')) assert manifest != manifest2 assert key.decrypt(None, manifest) == key.decrypt(None, manifest2) assert key.extract_nonce(manifest2) == 1 From e50646a07b5c4617a264107c5596d33383f90289 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Aug 2016 19:20:51 +0200 Subject: [PATCH 0118/1387] implement borg debug-info, fixes #1122 In tracebacks we have it already, but with the command, you can have it without a traceback also. --- borg/archiver.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/borg/archiver.py b/borg/archiver.py index 41373e25..728ece67 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -666,6 +666,11 @@ class Archiver: print("warning: %s" % e) return self.exit_code + def do_debug_info(self, args): + """display system information for debugging / bug reports""" + print(sysinfo()) + return EXIT_SUCCESS + @with_repository() def do_debug_dump_archive_items(self, args, repository, manifest, key): """dump (decrypted, decompressed) archive items metadata (not: data)""" @@ -1487,6 +1492,18 @@ class Archiver: subparser.add_argument('topic', metavar='TOPIC', type=str, nargs='?', help='additional help on TOPIC') + debug_info_epilog = textwrap.dedent(""" + This command displays some system information that might be useful for bug + reports and debugging problems. If a traceback happens, this information is + already appended at the end of the traceback. + """) + subparser = subparsers.add_parser('debug-info', parents=[common_parser], + description=self.do_debug_info.__doc__, + epilog=debug_info_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='show system infos for debugging / bug reports (debug)') + subparser.set_defaults(func=self.do_debug_info) + debug_dump_archive_items_epilog = textwrap.dedent(""" This command dumps raw (but decrypted and decompressed) archive items (only metadata) to files. """) From 2a434c3928d3145385d3ec71b6513096ee8ce1fc Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 15 Aug 2016 19:54:40 +0200 Subject: [PATCH 0119/1387] skip the O_NOATIME test on GNU Hurd, fixes #1315 GNU Hurd needs to fix their O_NOATIME, after that we can re-enable the test on that platform. --- borg/testsuite/archiver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index f1f70fcd..4591a1b2 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -362,6 +362,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): # the interesting parts of info_output2 and info_output should be same self.assert_equal(filter(info_output), filter(info_output2)) + # Search for O_NOATIME there: https://www.gnu.org/software/hurd/contributing.html - we just + # skip the test on Hurd, it is not critical anyway, just testing a performance optimization. + @pytest.mark.skipif(sys.platform == 'gnu0', reason="O_NOATIME is strangely broken on GNU Hurd") def test_atime(self): def has_noatime(some_file): atime_before = os.stat(some_file).st_atime_ns From 30cd7f3f21846a82928665727c74275094b35c88 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 16 Aug 2016 20:36:29 +0200 Subject: [PATCH 0120/1387] fix timestamp key name --- src/borg/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/item.py b/src/borg/item.py index 0a0908c0..d74bfdb5 100644 --- a/src/borg/item.py +++ b/src/borg/item.py @@ -259,6 +259,6 @@ class ManifestItem(PropDict): version = PropDict._make_property('version', int) archives = PropDict._make_property('archives', dict) # name -> dict - timestamp = PropDict._make_property('time', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode) + timestamp = PropDict._make_property('timestamp', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode) config = PropDict._make_property('config', dict) item_keys = PropDict._make_property('item_keys', tuple) From 146d531d9e1836fb160c9dbf4e4da4d292469836 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 16 Aug 2016 20:46:54 +0200 Subject: [PATCH 0121/1387] Fix borg-list --list-format --- src/borg/archiver.py | 2 +- src/borg/testsuite/archiver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index d58ebe45..9e7a8014 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1247,7 +1247,7 @@ class Archiver: for old_name, new_name, warning in deprecations: if arg.startswith(old_name): args[i] = arg.replace(old_name, new_name) - self.print_warning(warning) + print(warning, file=sys.stderr) return args def build_parser(self, prog=None): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 950256be..dd035970 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1203,7 +1203,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', self.repository_location) test_archive = self.repository_location + '::test' self.cmd('create', test_archive, src_dir) - self.cmd('list', '--list-format', '-', test_archive, exit_code=1) + self.cmd('list', '--list-format', '-', test_archive) self.archiver.exit_code = 0 # reset exit code for following tests output_1 = self.cmd('list', test_archive) output_2 = self.cmd('list', '--format', '{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}', test_archive) From f3defb02de0a1fcc47195ef02a4825c6fa330db0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 16 Aug 2016 20:45:32 +0200 Subject: [PATCH 0122/1387] Fix borg-list empty format Should produce empty output, not default output. --- src/borg/archiver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 9e7a8014..03765674 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -793,7 +793,7 @@ class Archiver: archive = Archive(repository, key, manifest, args.location.archive, cache=cache, consider_part_files=args.consider_part_files) - if args.format: + if args.format is not None: format = args.format elif args.short: format = "{path}{NL}" @@ -804,7 +804,7 @@ class Archiver: for item in archive.iter_items(lambda item: matcher.match(item.path)): write(safe_encode(formatter.format_item(item))) else: - if args.format: + if args.format is not None: format = args.format elif args.short: format = "{archive}{NL}" From 21c3fb3b933f9ab380192ce7a92f0ffd3161345f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 14 Aug 2016 23:24:53 +0200 Subject: [PATCH 0123/1387] compact_segments: add segment-level logging --- src/borg/repository.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 794f6fdc..745ee48a 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -22,6 +22,7 @@ from .helpers import Location from .helpers import ProgressIndicatorPercent from .helpers import bin_to_hex from .locking import Lock, LockError, LockErrorT +from .logger import create_logger from .lrucache import LRUCache from .platform import SaveFile, SyncFile, sync_dir @@ -413,29 +414,36 @@ class Repository: index_transaction_id = self.get_index_transaction_id() segments = self.segments unused = [] # list of segments, that are not used anymore + logger = create_logger('borg.debug.compact_segments') def complete_xfer(intermediate=True): # complete the current transfer (when some target segment is full) nonlocal unused # commit the new, compact, used segments self.io.write_commit(intermediate=intermediate) + logger.debug('complete_xfer: wrote %scommit at segment %d', 'intermediate ' if intermediate else '', + self.io.get_latest_segment()) # get rid of the old, sparse, unused segments. free space. for segment in unused: + logger.debug('complete_xfer: deleting unused segment %d', segment) assert self.segments.pop(segment) == 0 self.io.delete_segment(segment) del self.compact[segment] unused = [] + logger.debug('compaction started.') for segment, freeable_space in sorted(self.compact.items()): if not self.io.segment_exists(segment): + logger.warning('segment %d not found, but listed in compaction data', segment) del self.compact[segment] continue segment_size = self.io.segment_size(segment) if segment_size > 0.2 * self.max_segment_size and freeable_space < 0.15 * segment_size: - logger.debug('not compacting segment %d for later (only %d bytes are sparse)', - segment, freeable_space) + logger.debug('not compacting segment %d (only %d bytes are sparse)', segment, freeable_space) continue segments.setdefault(segment, 0) + logger.debug('compacting segment %d with usage count %d and %d freeable bytes', + segment, segments[segment], freeable_space) for tag, key, offset, data in self.io.iter_objects(segment, include_data=True): if tag == TAG_PUT and self.index.get(key, (-1, -1)) == (segment, offset): try: @@ -492,6 +500,7 @@ class Repository: assert segments[segment] == 0 unused.append(segment) complete_xfer(intermediate=False) + logger.debug('compaction completed.') def replay_segments(self, index_transaction_id, segments_transaction_id): # fake an old client, so that in case we do not have an exclusive lock yet, prepare_txn will upgrade the lock: From ac41ebcf782626a6e9018d8a6e7ebe406cce03cf Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 16 Aug 2016 02:04:17 +0200 Subject: [PATCH 0124/1387] Track shadowing of log entries Fixes (benign) index vs. log inconsistencies when segments are skipped during compaction. --- src/borg/repository.py | 32 ++++++++++++++++++++++----- src/borg/testsuite/repository.py | 37 +++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 745ee48a..1dc075ee 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -306,6 +306,10 @@ class Repository: except RuntimeError: self.check_transaction() self.index = self.open_index(transaction_id, False) + # This is an index of shadowed log entries during this transaction. Consider the following sequence: + # segment_n PUT A, segment_x DELETE A + # After the "DELETE A" in segment_x the shadow index will contain "A -> (n,)". + self.shadow_index = defaultdict(list) if transaction_id is None: self.segments = {} # XXX bad name: usage_count_of_segment_x = self.segments[x] self.compact = FreeSpace() # XXX bad name: freeable_space_of_segment_x = self.compact[x] @@ -420,9 +424,8 @@ class Repository: # complete the current transfer (when some target segment is full) nonlocal unused # commit the new, compact, used segments - self.io.write_commit(intermediate=intermediate) - logger.debug('complete_xfer: wrote %scommit at segment %d', 'intermediate ' if intermediate else '', - self.io.get_latest_segment()) + segment = self.io.write_commit(intermediate=intermediate) + logger.debug('complete_xfer: wrote %scommit at segment %d', 'intermediate ' if intermediate else '', segment) # get rid of the old, sparse, unused segments. free space. for segment in unused: logger.debug('complete_xfer: deleting unused segment %d', segment) @@ -445,7 +448,10 @@ class Repository: logger.debug('compacting segment %d with usage count %d and %d freeable bytes', segment, segments[segment], freeable_space) for tag, key, offset, data in self.io.iter_objects(segment, include_data=True): - if tag == TAG_PUT and self.index.get(key, (-1, -1)) == (segment, offset): + if tag == TAG_COMMIT: + continue + in_index = self.index.get(key) == (segment, offset) + if tag == TAG_PUT and in_index: try: new_segment, offset = self.io.write_put(key, data, raise_full=True) except LoggedIO.SegmentFull: @@ -455,8 +461,22 @@ class Repository: segments.setdefault(new_segment, 0) segments[new_segment] += 1 segments[segment] -= 1 + elif tag == TAG_PUT and not in_index: + # If this is a PUT shadowed by a later tag, then it will be gone when this segment is deleted after + # this loop. Therefore it is removed from the shadow index. + try: + self.shadow_index[key].remove(segment) + except (KeyError, ValueError): + pass elif tag == TAG_DELETE: - if index_transaction_id is None or segment > index_transaction_id: + # If the shadow index doesn't contain this key, then we can't say if there's a shadowed older tag, + # therefore we do not drop the delete, but write it to a current segment. + shadowed_put_exists = key not in self.shadow_index or any( + # If the key is in the shadow index and there is any segment with an older PUT of this + # key, we have a shadowed put. + shadowed < segment for shadowed in self.shadow_index[key]) + + if shadowed_put_exists or index_transaction_id is None or segment > index_transaction_id: # (introduced in 6425d16aa84be1eaaf88) # This is needed to avoid object un-deletion if we crash between the commit and the deletion # of old segments in complete_xfer(). @@ -714,6 +734,7 @@ class Repository: segment, offset = self.index.pop(id) except KeyError: raise self.ObjectNotFound(id, self.path) from None + self.shadow_index[id].append(segment) self.segments[segment] -= 1 size = self.io.read(segment, offset, id, read_data=False) self.compact[segment] += size @@ -1026,6 +1047,7 @@ class LoggedIO: crc = self.crc_fmt.pack(crc32(header) & 0xffffffff) fd.write(b''.join((crc, header))) self.close_segment() + return self.segment - 1 # close_segment() increments it MAX_DATA_SIZE = MAX_OBJECT_SIZE - LoggedIO.put_header_fmt.size diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 25e93101..5069b07a 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -293,7 +293,42 @@ class RepositoryCommitTestCase(RepositoryTestCaseBase): assert not self.repository.io.segment_exists(last_segment) for segment, _ in self.repository.io.segment_iterator(): for tag, key, offset, size in self.repository.io.iter_objects(segment): - assert tag != TAG_DELETE + if tag == TAG_DELETE: + assert segment in self.repository.compact + + def test_shadowed_entries_are_preserved(self): + get_latest_segment = self.repository.io.get_latest_segment + self.repository.put(H(1), b'1') + # This is the segment with our original PUT of interest + put_segment = get_latest_segment() + self.repository.commit() + + # We now delete H(1), and force this segment to not be compacted, which can happen + # if it's not sparse enough (symbolized by H(2) here). + self.repository.delete(H(1)) + self.repository.put(H(2), b'1') + delete_segment = get_latest_segment() + + # We pretend these are mostly dense (not sparse) and won't be compacted + del self.repository.compact[put_segment] + del self.repository.compact[delete_segment] + + self.repository.commit() + + # Now we perform an unrelated operation on the segment containing the DELETE, + # causing it to be compacted. + self.repository.delete(H(2)) + self.repository.commit() + + assert self.repository.io.segment_exists(put_segment) + assert not self.repository.io.segment_exists(delete_segment) + + # Basic case, since the index survived this must be ok + assert H(1) not in self.repository + # Nuke index, force replay + os.unlink(os.path.join(self.repository.path, 'index.%d' % get_latest_segment())) + # Must not reappear + assert H(1) not in self.repository class RepositoryAppendOnlyTestCase(RepositoryTestCaseBase): From 833f8d1373f5a6086ce66ccc2d443b587cbb0290 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 16 Aug 2016 12:39:51 +0200 Subject: [PATCH 0125/1387] Track entire session Note how this enables the much stricter check in test_moved_deletes_are_tracked --- src/borg/repository.py | 15 +++++++++++---- src/borg/testsuite/repository.py | 19 +++++++++++++++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 1dc075ee..097c64bc 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -111,7 +111,12 @@ class Repository: self.io = None self.lock = None self.index = None + # This is an index of shadowed log entries during this transaction. Consider the following sequence: + # segment_n PUT A, segment_x DELETE A + # After the "DELETE A" in segment_x the shadow index will contain "A -> (n,)". + self.shadow_index = defaultdict(list) self._active_txn = False + self.lock_wait = lock_wait self.do_lock = lock self.do_create = create @@ -306,13 +311,10 @@ class Repository: except RuntimeError: self.check_transaction() self.index = self.open_index(transaction_id, False) - # This is an index of shadowed log entries during this transaction. Consider the following sequence: - # segment_n PUT A, segment_x DELETE A - # After the "DELETE A" in segment_x the shadow index will contain "A -> (n,)". - self.shadow_index = defaultdict(list) if transaction_id is None: self.segments = {} # XXX bad name: usage_count_of_segment_x = self.segments[x] self.compact = FreeSpace() # XXX bad name: freeable_space_of_segment_x = self.compact[x] + self.shadow_index.clear() else: if do_cleanup: self.io.cleanup(transaction_id) @@ -343,6 +345,11 @@ class Repository: else: self.segments = hints[b'segments'] self.compact = FreeSpace(hints[b'compact']) + # Drop uncommitted segments in the shadow index + for key, shadowed_segments in self.shadow_index.items(): + for segment in list(shadowed_segments): + if segment > transaction_id: + shadowed_segments.remove(segment) def write_index(self): hints = {b'version': 2, diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 5069b07a..67c250e8 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -293,8 +293,7 @@ class RepositoryCommitTestCase(RepositoryTestCaseBase): assert not self.repository.io.segment_exists(last_segment) for segment, _ in self.repository.io.segment_iterator(): for tag, key, offset, size in self.repository.io.iter_objects(segment): - if tag == TAG_DELETE: - assert segment in self.repository.compact + assert tag != TAG_DELETE def test_shadowed_entries_are_preserved(self): get_latest_segment = self.repository.io.get_latest_segment @@ -330,6 +329,22 @@ class RepositoryCommitTestCase(RepositoryTestCaseBase): # Must not reappear assert H(1) not in self.repository + def test_shadow_index_rollback(self): + self.repository.put(H(1), b'1') + self.repository.delete(H(1)) + assert self.repository.shadow_index[H(1)] == [0] + self.repository.commit() + # note how an empty list means that nothing is shadowed for sure + assert self.repository.shadow_index[H(1)] == [] + self.repository.put(H(1), b'1') + self.repository.delete(H(1)) + # 0 put/delete; 1 commit; 2 compacted; 3 commit; 4 put/delete + assert self.repository.shadow_index[H(1)] == [4] + self.repository.rollback() + self.repository.put(H(2), b'1') + # After the rollback segment 4 shouldn't be considered anymore + assert self.repository.shadow_index[H(1)] == [] + class RepositoryAppendOnlyTestCase(RepositoryTestCaseBase): def open(self, create=False): From 26e8ff2cbc449bc49bc5e3bad3d51e8c757cc763 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 16 Aug 2016 12:45:33 +0200 Subject: [PATCH 0126/1387] Repository: don't use defaultdict for shadow index avoids errors by accidentally inserting an empty list and makes it more clear. --- src/borg/repository.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 097c64bc..a095ca7c 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -113,10 +113,9 @@ class Repository: self.index = None # This is an index of shadowed log entries during this transaction. Consider the following sequence: # segment_n PUT A, segment_x DELETE A - # After the "DELETE A" in segment_x the shadow index will contain "A -> (n,)". - self.shadow_index = defaultdict(list) + # After the "DELETE A" in segment_x the shadow index will contain "A -> [n]". + self.shadow_index = {} self._active_txn = False - self.lock_wait = lock_wait self.do_lock = lock self.do_create = create @@ -741,7 +740,7 @@ class Repository: segment, offset = self.index.pop(id) except KeyError: raise self.ObjectNotFound(id, self.path) from None - self.shadow_index[id].append(segment) + self.shadow_index.setdefault(id, []).append(segment) self.segments[segment] -= 1 size = self.io.read(segment, offset, id, read_data=False) self.compact[segment] += size From c84ad6b7b1ffc9b6c126e73e358ee57e6706168e Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Fri, 5 Aug 2016 22:26:59 +0200 Subject: [PATCH 0127/1387] Archiver.do_extract: Fix leak of downloaded chunk contents caused by preloading Include condition that path is non empty after applying strip_components into filter passed to iter_items. All filtering of files to extract must be done in the filter callable used in archive.iter_items because iter_items will preload all chunks used in items it returns. If they are not actually extracted the accumulate in the responsed dict. --- borg/archiver.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 728ece67..dc364b65 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -371,12 +371,13 @@ class Archiver: sparse = args.sparse strip_components = args.strip_components dirs = [] - for item in archive.iter_items(lambda item: matcher.match(item[b'path']), preload=True): + filter = lambda item: matcher.match(item[b'path']) + if strip_components: + filter = lambda item: matcher.match(item[b'path']) and os.sep.join(item[b'path'].split(os.sep)[strip_components:]) + for item in archive.iter_items(filter, preload=True): orig_path = item[b'path'] if strip_components: item[b'path'] = os.sep.join(orig_path.split(os.sep)[strip_components:]) - if not item[b'path']: - continue if not args.dry_run: while dirs and not item[b'path'].startswith(dirs[-1][b'path']): dir_item = dirs.pop(-1) From 724586e965b7a61955dc5baafb092308cb8642ce Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Tue, 9 Aug 2016 23:26:56 +0200 Subject: [PATCH 0128/1387] Add test for preloading releated leaks on extract with --strip-components --- borg/remote.py | 2 ++ borg/testsuite/archiver.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/borg/remote.py b/borg/remote.py index 3dda2413..6a611019 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -203,6 +203,8 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. raise def __del__(self): + if len(self.responses): + logging.debug("still %d cached responses left in RemoteRepository" % (len(self.responses),)) if self.p: self.close() assert False, "cleanup happened in Repository.__del__" diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 4591a1b2..0cca1b69 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1327,6 +1327,29 @@ class RemoteArchiverTestCase(ArchiverTestCase): def test_debug_put_get_delete_obj(self): pass + def test_strip_components_doesnt_leak(self): + self.cmd('init', self.repository_location) + self.create_regular_file('dir/file', contents=b"test file contents 123") + self.create_regular_file('dir/file2', contents=b"test file contents 345") + self.create_regular_file('skipped', contents=b"test file contents 567") + self.create_regular_file('skipped2', contents=b"test file contentsasdasd") + self.create_regular_file('skipped4', contents=b"sdfdsgdgfhttztu") + self.cmd('create', self.repository_location + '::test', 'input') + marker = 'cached responses left in RemoteRepository' + with changedir('output'): + #import rpdb2; rpdb2.start_embedded_debugger("nopass") + res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '3') + self.assert_true(marker not in res) + with self.assert_creates_file('file'): + res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '2') + self.assert_true(marker not in res) + with self.assert_creates_file('dir/file'): + res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '1') + self.assert_true(marker not in res) + with self.assert_creates_file('input/dir/file'): + res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '0') + self.assert_true(marker not in res) + def test_get_args(): archiver = Archiver() From 8268e26c6b184c736d334c1bc7c7778c082acad6 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 17 Aug 2016 22:36:25 +0200 Subject: [PATCH 0129/1387] extract: refactor filter building --- borg/archiver.py | 14 +++++++++++--- borg/testsuite/archiver.py | 26 ++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index dc364b65..43ec093a 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -343,6 +343,16 @@ class Archiver: status = '-' # dry run, item was not backed up self.print_file_status(status, path) + @staticmethod + def build_filter(matcher, strip_components=0): + if strip_components: + def item_filter(item): + return matcher.match(item[b'path']) and os.sep.join(item[b'path'].split(os.sep)[strip_components:]) + else: + def item_filter(item): + return matcher.match(item[b'path']) + return item_filter + @with_repository() @with_archive def do_extract(self, args, repository, manifest, key, archive): @@ -371,9 +381,7 @@ class Archiver: sparse = args.sparse strip_components = args.strip_components dirs = [] - filter = lambda item: matcher.match(item[b'path']) - if strip_components: - filter = lambda item: matcher.match(item[b'path']) and os.sep.join(item[b'path'].split(os.sep)[strip_components:]) + filter = self.build_filter(matcher, strip_components) for item in archive.iter_items(filter, preload=True): orig_path = item[b'path'] if strip_components: diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 0cca1b69..db1d5c9e 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -21,7 +21,7 @@ from ..archive import Archive, ChunkBuffer, CHUNK_MAX_EXP, flags_noatime, flags_ from ..archiver import Archiver from ..cache import Cache from ..crypto import bytes_to_long, num_aes_blocks -from ..helpers import Manifest, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR +from ..helpers import Manifest, PatternMatcher, parse_pattern, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from ..remote import RemoteRepository, PathNotAllowed from ..repository import Repository from . import BaseTestCase, changedir, environment_variable @@ -1337,7 +1337,6 @@ class RemoteArchiverTestCase(ArchiverTestCase): self.cmd('create', self.repository_location + '::test', 'input') marker = 'cached responses left in RemoteRepository' with changedir('output'): - #import rpdb2; rpdb2.start_embedded_debugger("nopass") res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '3') self.assert_true(marker not in res) with self.assert_creates_file('file'): @@ -1370,3 +1369,26 @@ def test_get_args(): args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ], 'borg init /') assert args.func == archiver.do_serve + + +class TestBuildFilter: + def test_basic(self): + matcher = PatternMatcher() + matcher.add([parse_pattern('included')], True) + filter = Archiver.build_filter(matcher) + assert filter({b'path': 'included'}) + assert filter({b'path': 'included/file'}) + assert not filter({b'path': 'something else'}) + + def test_empty(self): + matcher = PatternMatcher(fallback=True) + filter = Archiver.build_filter(matcher) + assert filter({b'path': 'anything'}) + + def test_strip_components(self): + matcher = PatternMatcher(fallback=True) + filter = Archiver.build_filter(matcher, strip_components=1) + assert not filter({b'path': 'shallow'}) + assert not filter({b'path': 'shallow/'}) # can this even happen? paths are normalized... + assert filter({b'path': 'deep enough/file'}) + assert filter({b'path': 'something/dir/file'}) From c39e395ecf68e685aa6131860a300bc4d1801734 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 17 Aug 2016 23:01:20 +0200 Subject: [PATCH 0130/1387] Exclude incompatible tests for ArchiverTestCaseBinary --- src/borg/testsuite/archiver.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index bd26eeb9..0d4a5441 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1204,7 +1204,6 @@ class ArchiverTestCase(ArchiverTestCaseBase): test_archive = self.repository_location + '::test' self.cmd('create', test_archive, src_dir) self.cmd('list', '--list-format', '-', test_archive) - self.archiver.exit_code = 0 # reset exit code for following tests output_1 = self.cmd('list', test_archive) output_2 = self.cmd('list', '--format', '{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}', test_archive) output_3 = self.cmd('list', '--format', '{mtime:%s} {path}{NL}', test_archive) @@ -1772,6 +1771,10 @@ class ArchiverTestCaseBinary(ArchiverTestCase): def test_recreate_interrupt(self): pass + @unittest.skip('patches objects') + def test_recreate_interrupt2(self): + pass + @unittest.skip('patches objects') def test_recreate_changed_source(self): pass From adaeb32cd4af4bc2275964e185c6b9064b204a70 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 17 Aug 2016 22:50:38 +0200 Subject: [PATCH 0131/1387] Repository: fix repo not closed cleanly on InvalidRepository exception --- borg/repository.py | 1 + 1 file changed, 1 insertion(+) diff --git a/borg/repository.py b/borg/repository.py index 40d73042..71e9040a 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -174,6 +174,7 @@ class Repository: self.config = ConfigParser(interpolation=None) self.config.read(os.path.join(self.path, 'config')) if 'repository' not in self.config.sections() or self.config.getint('repository', 'version') != 1: + self.close() raise self.InvalidRepository(path) self.max_segment_size = self.config.getint('repository', 'max_segment_size') self.segments_per_dir = self.config.getint('repository', 'segments_per_dir') From 928f6e0ca4dc684631d0d79d039dfe9b33917f7e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 17 Aug 2016 22:55:45 +0200 Subject: [PATCH 0132/1387] repository: fix spurious, empty lock.roster on InvalidRepository exception --- borg/locking.py | 7 +++++++ borg/testsuite/locking.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/borg/locking.py b/borg/locking.py index 2dbb27cb..ff0d2484 100644 --- a/borg/locking.py +++ b/borg/locking.py @@ -201,6 +201,9 @@ class LockRoster: roster = self.load() return set(tuple(e) for e in roster.get(key, [])) + def empty(self, *keys): + return all(not self.get(key) for key in keys) + def modify(self, key, op): roster = self.load() try: @@ -293,10 +296,14 @@ class Lock: def release(self): if self.is_exclusive: self._roster.modify(EXCLUSIVE, REMOVE) + if self._roster.empty(EXCLUSIVE, SHARED): + self._roster.remove() self._lock.release() else: with self._lock: self._roster.modify(SHARED, REMOVE) + if self._roster.empty(EXCLUSIVE, SHARED): + self._roster.remove() def upgrade(self): # WARNING: if multiple read-lockers want to upgrade, it will deadlock because they diff --git a/borg/testsuite/locking.py b/borg/testsuite/locking.py index fcb21f1d..850c0ac5 100644 --- a/borg/testsuite/locking.py +++ b/borg/testsuite/locking.py @@ -64,6 +64,8 @@ class TestLock: lock2 = Lock(lockpath, exclusive=False, id=ID2).acquire() assert len(lock1._roster.get(SHARED)) == 2 assert len(lock1._roster.get(EXCLUSIVE)) == 0 + assert not lock1._roster.empty(SHARED, EXCLUSIVE) + assert lock1._roster.empty(EXCLUSIVE) lock1.release() lock2.release() @@ -71,6 +73,7 @@ class TestLock: with Lock(lockpath, exclusive=True, id=ID1) as lock: assert len(lock._roster.get(SHARED)) == 0 assert len(lock._roster.get(EXCLUSIVE)) == 1 + assert not lock._roster.empty(SHARED, EXCLUSIVE) def test_upgrade(self, lockpath): with Lock(lockpath, exclusive=False) as lock: @@ -78,6 +81,7 @@ class TestLock: lock.upgrade() # NOP assert len(lock._roster.get(SHARED)) == 0 assert len(lock._roster.get(EXCLUSIVE)) == 1 + assert not lock._roster.empty(SHARED, EXCLUSIVE) def test_downgrade(self, lockpath): with Lock(lockpath, exclusive=True) as lock: From ef13d392c7564ce7865a7642cf9ed0a57a19ce4e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 18 Aug 2016 12:26:14 +0200 Subject: [PATCH 0133/1387] Cache: release lock if cache is invalid --- borg/cache.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/borg/cache.py b/borg/cache.py index 0cacb2a8..763a262a 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -137,9 +137,11 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" cache_version = self.config.getint('cache', 'version') wanted_version = 1 if cache_version != wanted_version: + self.close() raise Exception('%s has unexpected cache version %d (wanted: %d).' % ( config_path, cache_version, wanted_version)) except configparser.NoSectionError: + self.close() raise Exception('%s does not look like a Borg cache.' % config_path) from None self.id = self.config.get('cache', 'repository') self.manifest_id = unhexlify(self.config.get('cache', 'manifest')) From b46713224bb19a1e9f13b21ff9cc38f4df6a7a9f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 18 Aug 2016 16:23:36 +0200 Subject: [PATCH 0134/1387] Document DownloadPipeline.unpack_many precautions --- borg/archive.py | 9 +++++++++ borg/testsuite/archiver.py | 10 +++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index 4498c72a..a3a13317 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -99,6 +99,15 @@ class DownloadPipeline: self.key = key def unpack_many(self, ids, filter=None, preload=False): + """ + Return iterator of items. + + *ids* is a chunk ID list of an item stream. *filter* is a callable + to decide whether an item will be yielded. *preload* preloads the data chunks of every yielded item. + + Warning: if *preload* is True then all data chunks of every yielded item have to be retrieved, + otherwise preloaded chunks will accumulate in RemoteRepository and create a memory leak. + """ unpacker = msgpack.Unpacker(use_list=False) for data in self.fetch_many(ids): unpacker.feed(data) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index db1d5c9e..f14edd53 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1329,11 +1329,11 @@ class RemoteArchiverTestCase(ArchiverTestCase): def test_strip_components_doesnt_leak(self): self.cmd('init', self.repository_location) - self.create_regular_file('dir/file', contents=b"test file contents 123") - self.create_regular_file('dir/file2', contents=b"test file contents 345") - self.create_regular_file('skipped', contents=b"test file contents 567") - self.create_regular_file('skipped2', contents=b"test file contentsasdasd") - self.create_regular_file('skipped4', contents=b"sdfdsgdgfhttztu") + self.create_regular_file('dir/file', contents=b"test file contents 1") + self.create_regular_file('dir/file2', contents=b"test file contents 2") + self.create_regular_file('skipped-file1', contents=b"test file contents 3") + self.create_regular_file('skipped-file2', contents=b"test file contents 4") + self.create_regular_file('skipped-file3', contents=b"test file contents 5") self.cmd('create', self.repository_location + '::test', 'input') marker = 'cached responses left in RemoteRepository' with changedir('output'): From 2aae0b17c69f13c69db8f8c1f838328c46c7eaec Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 16 Aug 2016 00:56:52 +0200 Subject: [PATCH 0135/1387] update CHANGES --- docs/changes.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 7da62747..8a75e05d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -57,6 +57,30 @@ Security fixes: - fix security issue with remote repository access, #1428 +Bug fixes: + +- fixed repeated LockTimeout exceptions when borg serve tried to write into + a already write-locked repo (e.g. by a borg mount), #502 part b) + This was solved by the fix for #1220 in 1.0.7rc1 already. +- fix cosmetics + file leftover for "not a valid borg repository", #1490 +- Cache: release lock if cache is invalid, #1501 +- borg extract --strip-components: fix leak of preloaded chunk contents +- Repository, when a InvalidRepository exception happens: + + - fix spurious, empty lock.roster + - fix repo not closed cleanly + +New features: + +- implement borg debug-info, fixes #1122 + (just calls already existing code via cli, same output as below tracebacks) + +Other changes: + +- skip the O_NOATIME test on GNU Hurd, fixes #1315 + (this is a very minor issue and the GNU Hurd project knows the bug) +- document using a clean repo to test / build the release + Version 1.0.7rc2 (2016-08-13) ----------------------------- From 28cbf24815649b5dcb453498dae64948abbdf411 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 2 Aug 2016 15:01:30 +0200 Subject: [PATCH 0136/1387] more tests for --restrict-to-path especially test that other directory names sharing same name prefix are not allowed. --- borg/testsuite/archiver.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index f14edd53..1774f347 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1305,12 +1305,21 @@ class RemoteArchiverTestCase(ArchiverTestCase): prefix = '__testsuite__:' def test_remote_repo_restrict_to_path(self): - self.cmd('init', self.repository_location) - path_prefix = os.path.dirname(self.repository_path) + # restricted to repo directory itself: + with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', self.repository_path]): + self.cmd('init', self.repository_location) + # restricted to repo directory itself, fail for other directories with same prefix: + with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', self.repository_path]): + self.assert_raises(PathNotAllowed, lambda: self.cmd('init', self.repository_location + '_0')) + + # restricted to a completely different path: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo']): self.assert_raises(PathNotAllowed, lambda: self.cmd('init', self.repository_location + '_1')) + path_prefix = os.path.dirname(self.repository_path) + # restrict to repo directory's parent directory: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', path_prefix]): self.cmd('init', self.repository_location + '_2') + # restrict to repo directory's parent directory and another directory: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]): self.cmd('init', self.repository_location + '_3') From dde18d6a7660837ce7b4f30d31960bdc74252570 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 2 Aug 2016 15:50:21 +0200 Subject: [PATCH 0137/1387] security fix: --restrict-to-path must not accept pathes with same name prefix bug: --restrict-to-path /foo erroneously allowed /foobar. even worse: --restrict-to-path /foo/ erroneously allowed /foobar. --- borg/remote.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/borg/remote.py b/borg/remote.py index 6a611019..472d1ac3 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -120,8 +120,13 @@ class RepositoryServer: # pragma: no cover path = path[1:] path = os.path.realpath(os.path.expanduser(path)) if self.restrict_to_paths: + # if --restrict-to-path P is given, we make sure that we only operate in/below path P. + # for the prefix check, it is important that the compared pathes both have trailing slashes, + # so that a path /foobar will NOT be accepted with --restrict-to-path /foo option. + path_with_sep = os.path.join(path, '') # make sure there is a trailing slash (os.sep) for restrict_to_path in self.restrict_to_paths: - if path.startswith(os.path.realpath(restrict_to_path)): + restrict_to_path_with_sep = os.path.join(os.path.realpath(restrict_to_path), '') # trailing slash + if path_with_sep.startswith(restrict_to_path_with_sep): break else: raise PathNotAllowed(path) From f32c8858ad3f6637fca35ef814f6cd584d1cc658 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 18 Aug 2016 23:05:58 +0200 Subject: [PATCH 0138/1387] update CHANGES with description of issue #1428 --- docs/changes.rst | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 8a75e05d..debf4feb 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,12 +50,29 @@ The best check that everything is ok is to run a dry-run extraction:: borg extract -v --dry-run REPO::ARCHIVE -Version 1.0.7 (not released yet) --------------------------------- +Version 1.0.7 (2016-08-19) +-------------------------- Security fixes: -- fix security issue with remote repository access, #1428 +- borg serve: fix security issue with remote repository access, #1428 + If you used e.g. --restrict-to-path /path/client1/ (with or without trailing + slash does not make a difference), it acted like a path prefix match using + /path/client1 (note the missing trailing slash) - the code then also allowed + working in e.g. /path/client13 or /path/client1000. + + As this could accidentally lead to major security/privacy issues depending on + the pathes you use, the behaviour was changed to be a strict directory match. + That means --restrict-to-path /path/client1 (with or without trailing slash + does not make a difference) now uses /path/client1/ internally (note the + trailing slash here!) for matching and allows precisely that path AND any + path below it. So, /path/client1 is allowed, /path/client1/repo1 is allowed, + but not /path/client13 or /path/client1000. + + If you willingly used the undocumented (dangerous) previous behaviour, you + may need to rearrange your --restrict-to-path pathes now. We are sorry if + that causes work for you, but we did not want a potentially dangerous + behaviour in the software (not even using a for-backwards-compat option). Bug fixes: From 53d14e96de9741ea2fdcba9a74c68b455631b7d4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 19 Aug 2016 14:55:49 +0200 Subject: [PATCH 0139/1387] borg list: test for --list-format deprecation --- src/borg/testsuite/archiver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 0d4a5441..12a7f417 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1203,7 +1203,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', self.repository_location) test_archive = self.repository_location + '::test' self.cmd('create', test_archive, src_dir) - self.cmd('list', '--list-format', '-', test_archive) + output_warn = self.cmd('list', '--list-format', '-', test_archive) + self.assert_in('--list-format" has been deprecated.', output_warn) output_1 = self.cmd('list', test_archive) output_2 = self.cmd('list', '--format', '{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}', test_archive) output_3 = self.cmd('list', '--format', '{mtime:%s} {path}{NL}', test_archive) From 13761c4ec25e7f5b496d00eba06e390bb8c412dd Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Sat, 25 Jun 2016 23:56:51 +0100 Subject: [PATCH 0140/1387] daemonize(): use os.devnull instead of hardcoded /dev/null --- src/borg/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 75929144..d6237c5b 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1001,7 +1001,7 @@ def daemonize(): os.close(0) os.close(1) os.close(2) - fd = os.open('/dev/null', os.O_RDWR) + fd = os.open(os.devnull, os.O_RDWR) os.dup2(fd, 0) os.dup2(fd, 1) os.dup2(fd, 2) From 28076ee5887f152aa93f0c0a053d8d8281250460 Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Sat, 25 Jun 2016 23:07:21 +0100 Subject: [PATCH 0141/1387] helpers.py: replace memoize with functools.lru_cache --- src/borg/helpers.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 75929144..32ebf968 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -22,7 +22,7 @@ from collections import namedtuple, deque, abc from contextlib import contextmanager from datetime import datetime, timezone, timedelta from fnmatch import translate -from functools import wraps, partial +from functools import wraps, partial, lru_cache from itertools import islice from operator import attrgetter from string import Formatter @@ -722,17 +722,7 @@ def format_archive(archive): ) -def memoize(function): - cache = {} - - def decorated_function(*args): - try: - return cache[args] - except KeyError: - val = function(*args) - cache[args] = val - return val - return decorated_function +memoize = lru_cache(maxsize=None) class Buffer: From 821fba8cb0bd56db35b6bc0c7b45f5a24b27a47c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 17 Aug 2016 17:50:01 +0200 Subject: [PATCH 0142/1387] use patched LDLP-preserving pyinstaller, fixes #1416 see https://github.com/pyinstaller/pyinstaller/pull/2148 also: use waf --no-lsb (the bootloader does not build without it on our wheezy32 vagrant VM) --- Vagrantfile | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 17853bfa..539ed381 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -248,10 +248,13 @@ def install_pyinstaller_bootloader(boxname) . borg-env/bin/activate git clone https://github.com/pyinstaller/pyinstaller.git cd pyinstaller - git checkout v3.1.1 + # develop branch, merge commit of ThomasWaldmann/do-not-overwrite-LD_LP + git checkout 639fcec992d753db2058314b843bccc37b815265 # build bootloader, if it is not included cd bootloader - python ./waf all + # XXX temporarily use --no-lsb as we have no LSB environment + # XXX https://github.com/borgbackup/borg/issues/1506 + python ./waf --no-lsb all cd .. pip install -e . EOF @@ -392,7 +395,8 @@ Vagrant.configure(2) do |config| b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("wheezy32") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("wheezy32") b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("wheezy32") - b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller("wheezy32") + # XXX https://github.com/borgbackup/borg/issues/1506 + b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller_bootloader("wheezy32") b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("wheezy32") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("wheezy32") end @@ -405,7 +409,8 @@ Vagrant.configure(2) do |config| b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("wheezy64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("wheezy64") b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("wheezy64") - b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller("wheezy64") + # XXX https://github.com/borgbackup/borg/issues/1506 + b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller_bootloader("wheezy64") b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("wheezy64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("wheezy64") end From e7fccaccb2ee46da252f616ac7a57dde45873826 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 20 Aug 2016 12:28:27 +0200 Subject: [PATCH 0143/1387] restore original LDLP, if possible, fixes #1498 see https://github.com/pyinstaller/pyinstaller/pull/2148 --- src/borg/remote.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 18637cae..6825caeb 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -162,9 +162,15 @@ class RemoteRepository: env = dict(os.environ) if not testing: borg_cmd = self.ssh_cmd(location) + borg_cmd - # pyinstaller binary adds LD_LIBRARY_PATH=/tmp/_ME... but we do not want - # that the system's ssh binary picks up (non-matching) libraries from there - env.pop('LD_LIBRARY_PATH', None) + # pyinstaller binary modifies LD_LIBRARY_PATH=/tmp/_ME... but we do not want + # that the system's ssh binary picks up (non-matching) libraries from there. + # thus we install the original LDLP, before pyinstaller has modified it: + lp_key = 'LD_LIBRARY_PATH' + lp_orig = env.get(lp_key + '_ORIG') # pyinstaller >= 20160820 has this + if lp_orig is not None: + env[lp_key] = lp_orig + else: + env.pop(lp_key, None) env.pop('BORG_PASSPHRASE', None) # security: do not give secrets to subprocess env['BORG_VERSION'] = __version__ self.p = Popen(borg_cmd, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) From 61af307ad4bb317503b1eb3ee60f7421881e5f5d Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Sat, 20 Aug 2016 13:06:16 +0100 Subject: [PATCH 0144/1387] helpers.py: replace memoize usages with lru_cache --- src/borg/helpers.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 32ebf968..153225fa 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -722,9 +722,6 @@ def format_archive(archive): ) -memoize = lru_cache(maxsize=None) - - class Buffer: """ provide a thread-local buffer @@ -766,7 +763,7 @@ class Buffer: return self._thread_local.buffer -@memoize +@lru_cache(maxsize=None) def uid2user(uid, default=None): try: return pwd.getpwuid(uid).pw_name @@ -774,7 +771,7 @@ def uid2user(uid, default=None): return default -@memoize +@lru_cache(maxsize=None) def user2uid(user, default=None): try: return user and pwd.getpwnam(user).pw_uid @@ -782,7 +779,7 @@ def user2uid(user, default=None): return default -@memoize +@lru_cache(maxsize=None) def gid2group(gid, default=None): try: return grp.getgrgid(gid).gr_name @@ -790,7 +787,7 @@ def gid2group(gid, default=None): return default -@memoize +@lru_cache(maxsize=None) def group2gid(group, default=None): try: return group and grp.getgrnam(group).gr_gid From a0c8c40f27110f906ef64c02282a7ab045e694a6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 20 Aug 2016 17:23:02 +0200 Subject: [PATCH 0145/1387] improve error logging, fixes #1440 archiver: split traceback and msg, have separate log level for traceback, log LockTimeout at debug level, for "Error" exceptions: always log the traceback, either at ERROR or DEBUG level. remote: if we have an "Error" typed exception instance, we can use its traceback flag and .get_message() as we do locally. --- src/borg/archiver.py | 32 +++++++++++++++++++++++--------- src/borg/remote.py | 22 ++++++++++++++++------ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e8e1568f..cb12309f 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2354,37 +2354,51 @@ def main(): # pragma: no cover sys.stderr = ErrorIgnoringTextIOWrapper(sys.stderr.buffer, sys.stderr.encoding, 'replace', line_buffering=True) setup_signal_handlers() archiver = Archiver() - msg = None + msg = tb = None + tb_log_level = logging.ERROR try: args = archiver.get_args(sys.argv, os.environ.get('SSH_ORIGINAL_COMMAND')) except Error as e: msg = e.get_message() - if e.traceback: - msg += "\n%s\n%s" % (traceback.format_exc(), sysinfo()) + tb_log_level = logging.ERROR if e.traceback else logging.DEBUG + tb = '%s\n%s' % (traceback.format_exc(), sysinfo()) # we might not have logging setup yet, so get out quickly print(msg, file=sys.stderr) + if tb_log_level == logging.ERROR: + print(tb, file=sys.stderr) sys.exit(e.exit_code) try: exit_code = archiver.run(args) except Error as e: msg = e.get_message() - if e.traceback: - msg += "\n%s\n%s" % (traceback.format_exc(), sysinfo()) + tb_log_level = logging.ERROR if e.traceback else logging.DEBUG + tb = "%s\n%s" % (traceback.format_exc(), sysinfo()) exit_code = e.exit_code except RemoteRepository.RPCError as e: - msg = '%s\n%s' % (str(e), sysinfo()) + msg = "%s %s" % (e.remote_type, e.name) + important = e.remote_type not in ('LockTimeout', ) + tb_log_level = logging.ERROR if important else logging.DEBUG + tb = sysinfo() exit_code = EXIT_ERROR except Exception: - msg = 'Local Exception.\n%s\n%s' % (traceback.format_exc(), sysinfo()) + msg = 'Local Exception' + tb_log_level = logging.ERROR + tb = '%s\n%s' % (traceback.format_exc(), sysinfo()) exit_code = EXIT_ERROR except KeyboardInterrupt: - msg = 'Keyboard interrupt.\n%s\n%s' % (traceback.format_exc(), sysinfo()) + msg = 'Keyboard interrupt' + tb_log_level = logging.DEBUG + tb = '%s\n%s' % (traceback.format_exc(), sysinfo()) exit_code = EXIT_ERROR except SIGTERMReceived: - msg = 'Received SIGTERM.' + msg = 'Received SIGTERM' + tb_log_level = logging.DEBUG + tb = '%s\n%s' % (traceback.format_exc(), sysinfo()) exit_code = EXIT_ERROR if msg: logger.error(msg) + if traceback: + logger.log(tb_log_level, tb) if args.show_rc: rc_logger = logging.getLogger('borg.output.show-rc') exit_msg = 'terminating with %s status, rc %d' diff --git a/src/borg/remote.py b/src/borg/remote.py index 18637cae..b0a31946 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -6,6 +6,7 @@ import select import shlex import sys import tempfile +import traceback from subprocess import Popen, PIPE import msgpack @@ -101,12 +102,21 @@ class RepositoryServer: # pragma: no cover f = getattr(self.repository, method) res = f(*args) except BaseException as e: - # These exceptions are reconstructed on the client end in RemoteRepository.call_many(), - # and will be handled just like locally raised exceptions. Suppress the remote traceback - # for these, except ErrorWithTraceback, which should always display a traceback. - if not isinstance(e, (Repository.DoesNotExist, Repository.AlreadyExists, PathNotAllowed)): - logging.exception('Borg %s: exception in RPC call:', __version__) - logging.error(sysinfo()) + if isinstance(e, (Repository.DoesNotExist, Repository.AlreadyExists, PathNotAllowed)): + # These exceptions are reconstructed on the client end in RemoteRepository.call_many(), + # and will be handled just like locally raised exceptions. Suppress the remote traceback + # for these, except ErrorWithTraceback, which should always display a traceback. + pass + else: + if isinstance(e, Error): + tb_log_level = logging.ERROR if e.traceback else logging.DEBUG + msg = e.get_message() + else: + tb_log_level = logging.ERROR + msg = '%s Exception in RPC call' % e.__class__.__name__ + tb = '%s\n%s' % (traceback.format_exc(), sysinfo()) + logging.error(msg) + logging.log(tb_log_level, tb) exc = "Remote Exception (see remote log for the traceback)" os.write(stdout_fd, msgpack.packb((1, msgid, e.__class__.__name__, exc))) else: From 41e4707d6a4d0437d6610cfb3151ae80adb0390b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 20 Aug 2016 23:09:42 +0200 Subject: [PATCH 0146/1387] fixup: meant "tb", not "traceback" --- src/borg/archiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index cb12309f..0db6963a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2397,7 +2397,7 @@ def main(): # pragma: no cover exit_code = EXIT_ERROR if msg: logger.error(msg) - if traceback: + if tb: logger.log(tb_log_level, tb) if args.show_rc: rc_logger = logging.getLogger('borg.output.show-rc') From 1c666222a746c215227315cd510fe4cb72cf0541 Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Sat, 25 Jun 2016 22:38:47 +0100 Subject: [PATCH 0147/1387] internals.rst: fix typos --- docs/internals.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/internals.rst b/docs/internals.rst index 82be188b..798ce856 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -48,7 +48,7 @@ Lock files the repository. The locking system is based on creating a directory `lock.exclusive` (for -exclusive locks). Inside the lock directory, there is a file indication +exclusive locks). Inside the lock directory, there is a file indicating hostname, process id and thread id of the lock holder. There is also a json file `lock.roster` that keeps a directory of all shared @@ -338,7 +338,7 @@ more chunks than estimated above, because 1 file is at least 1 chunk). If a remote repository is used the repo index will be allocated on the remote side. -E.g. backing up a total count of 1 Mi (IEC binary prefix e.g. 2^20) files with a total size of 1TiB. +E.g. backing up a total count of 1 Mi (IEC binary prefix i.e. 2^20) files with a total size of 1TiB. a) with ``create --chunker-params 10,23,16,4095`` (custom, like borg < 1.0 or attic): From 8709cec57cfe66a915702155d10ad0f213f19854 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 7 Aug 2016 14:17:56 +0200 Subject: [PATCH 0148/1387] borg-extract --progress --- src/borg/archive.py | 7 ++++++- src/borg/archiver.py | 17 +++++++++++++++-- src/borg/helpers.py | 8 ++++---- src/borg/item.py | 8 ++++++++ src/borg/testsuite/archiver.py | 9 +++++++++ 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 8548e3aa..5a150970 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -422,7 +422,7 @@ Number of files: {0.stats.nfiles}'''.format( return stats def extract_item(self, item, restore_attrs=True, dry_run=False, stdout=False, sparse=False, - hardlink_masters=None, original_path=None): + hardlink_masters=None, original_path=None, pi=None): """ Extract archive item. @@ -433,11 +433,14 @@ Number of files: {0.stats.nfiles}'''.format( :param sparse: write sparse files (chunk-granularity, independent of the original being sparse) :param hardlink_masters: maps paths to (chunks, link_target) for extracting subtrees with hardlinks correctly :param original_path: 'path' key as stored in archive + :param pi: ProgressIndicatorPercent (or similar) for file extraction progress (in bytes) """ has_damaged_chunks = 'chunks_healthy' in item if dry_run or stdout: if 'chunks' in item: for _, data in self.pipeline.fetch_many([c.id for c in item.chunks], is_preloaded=True): + if pi: + pi.show(increase=len(data)) if stdout: sys.stdout.buffer.write(data) if stdout: @@ -489,6 +492,8 @@ Number of files: {0.stats.nfiles}'''.format( with fd: ids = [c.id for c in item.chunks] for _, data in self.pipeline.fetch_many(ids, is_preloaded=True): + if pi: + pi.show(increase=len(data)) with backup_io(): if sparse and self.zeros.startswith(data): # all-zero chunk: create a hole in a sparse file diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 5c26314e..d607940a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -41,6 +41,7 @@ from .helpers import log_multi from .helpers import parse_pattern, PatternMatcher, PathPrefixPattern from .helpers import signal_handler from .helpers import ErrorIgnoringTextIOWrapper +from .helpers import ProgressIndicatorPercent from .item import Item from .key import key_creator, RepoKey, PassphraseKey from .platform import get_flags @@ -439,6 +440,7 @@ class Archiver: matcher, include_patterns = self.build_matcher(args.excludes, args.paths) + progress = args.progress output_list = args.output_list dry_run = args.dry_run stdout = args.stdout @@ -453,6 +455,12 @@ class Archiver: item.get('hardlink_master', True) and 'source' not in item) filter = self.build_filter(matcher, item_is_hardlink_master, strip_components) + if progress: + extracted_size = sum(item.file_size() for item in archive.iter_items(filter)) + pi = ProgressIndicatorPercent(total=extracted_size, msg='Extracting files %5.1f%%', step=0.1) + else: + pi = None + for item in archive.iter_items(filter, preload=True): orig_path = item.path if item_is_hardlink_master(item): @@ -472,19 +480,21 @@ class Archiver: logging.getLogger('borg.output.list').info(remove_surrogates(orig_path)) try: if dry_run: - archive.extract_item(item, dry_run=True) + archive.extract_item(item, dry_run=True, pi=pi) else: if stat.S_ISDIR(item.mode): dirs.append(item) archive.extract_item(item, restore_attrs=False) else: archive.extract_item(item, stdout=stdout, sparse=sparse, hardlink_masters=hardlink_masters, - original_path=orig_path) + original_path=orig_path, pi=pi) except BackupOSError as e: self.print_warning('%s: %s', remove_surrogates(orig_path), e) if not args.dry_run: + pi = ProgressIndicatorPercent(total=len(dirs), msg='Setting directory permissions %3.0f%%', same_line=True) while dirs: + pi.show() dir_item = dirs.pop(-1) try: archive.extract_item(dir_item) @@ -1641,6 +1651,9 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='extract archive contents') subparser.set_defaults(func=self.do_extract) + subparser.add_argument('-p', '--progress', dest='progress', + action='store_true', default=False, + help='show progress while extracting (may be slower)') subparser.add_argument('--list', dest='output_list', action='store_true', default=False, help='output verbose list of items (files, dirs, ...)') diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 6751285d..c3251881 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1142,17 +1142,17 @@ class ProgressIndicatorPercent: self.logger.removeHandler(self.handler) self.handler.close() - def progress(self, current=None): + def progress(self, current=None, increase=1): if current is not None: self.counter = current pct = self.counter * 100 / self.total - self.counter += 1 + self.counter += increase if pct >= self.trigger_at: self.trigger_at += self.step return pct - def show(self, current=None): - pct = self.progress(current) + def show(self, current=None, increase=1): + pct = self.progress(current, increase) if pct is not None: return self.output(pct) diff --git a/src/borg/item.py b/src/borg/item.py index d74bfdb5..b97b470f 100644 --- a/src/borg/item.py +++ b/src/borg/item.py @@ -157,6 +157,14 @@ class Item(PropDict): part = PropDict._make_property('part', int) + def file_size(self): + if 'chunks' not in self: + return 0 + total_size = 0 + for chunk_id, size, csize in self.chunks: + total_size += size + return total_size + class EncryptedKey(PropDict): """ diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 22438da0..45daceda 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -751,6 +751,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): output = self.cmd('extract', '--list', '--info', self.repository_location + '::test') self.assert_in("input/file", output) + def test_extract_progress(self): + self.cmd('init', self.repository_location) + self.create_regular_file('file', size=1024 * 80) + self.cmd('create', self.repository_location + '::test', 'input') + + with changedir('output'): + output = self.cmd('extract', self.repository_location + '::test', '--progress') + assert 'Extracting files' in output + def _create_test_caches(self): self.cmd('init', self.repository_location) self.create_regular_file('file1', size=1024 * 80) From 5924915d3579e38a8f8c86b16f427dd32d1f206f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 7 Aug 2016 14:24:30 +0200 Subject: [PATCH 0149/1387] Flip ProgressIndicatorPercent same_line default to True Every production use of this uses same_line=True --- src/borg/archive.py | 4 ++-- src/borg/archiver.py | 2 +- src/borg/helpers.py | 2 +- src/borg/repository.py | 4 ++-- src/borg/testsuite/helpers.py | 2 +- src/borg/upgrader.py | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 5a150970..b1768542 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -640,7 +640,7 @@ Number of files: {0.stats.nfiles}'''.format( try: unpacker = msgpack.Unpacker(use_list=False) items_ids = self.metadata.items - pi = ProgressIndicatorPercent(total=len(items_ids), msg="Decrementing references %3.0f%%", same_line=True) + pi = ProgressIndicatorPercent(total=len(items_ids), msg="Decrementing references %3.0f%%") for (i, (items_id, data)) in enumerate(zip(items_ids, self.repository.get_many(items_ids))): if progress: pi.show(i) @@ -1033,7 +1033,7 @@ class ArchiveChecker: logger.info('Starting cryptographic data integrity verification...') count = len(self.chunks) errors = 0 - pi = ProgressIndicatorPercent(total=count, msg="Verifying data %6.2f%%", step=0.01, same_line=True) + pi = ProgressIndicatorPercent(total=count, msg="Verifying data %6.2f%%", step=0.01) for chunk_id, (refcount, *_) in self.chunks.iteritems(): pi.show() try: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index d607940a..417f055e 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -492,7 +492,7 @@ class Archiver: self.print_warning('%s: %s', remove_surrogates(orig_path), e) if not args.dry_run: - pi = ProgressIndicatorPercent(total=len(dirs), msg='Setting directory permissions %3.0f%%', same_line=True) + pi = ProgressIndicatorPercent(total=len(dirs), msg='Setting directory permissions %3.0f%%') while dirs: pi.show() dir_item = dirs.pop(-1) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index c3251881..72f03ac6 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1105,7 +1105,7 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, class ProgressIndicatorPercent: - def __init__(self, total, step=5, start=0, same_line=False, msg="%3.0f%%"): + def __init__(self, total, step=5, start=0, same_line=True, msg="%3.0f%%"): """ Percentage-based progress indicator diff --git a/src/borg/repository.py b/src/borg/repository.py index 9ae7e36f..8b5f85db 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -536,7 +536,7 @@ class Repository: self.prepare_txn(index_transaction_id, do_cleanup=False) try: segment_count = sum(1 for _ in self.io.segment_iterator()) - pi = ProgressIndicatorPercent(total=segment_count, msg="Replaying segments %3.0f%%", same_line=True) + pi = ProgressIndicatorPercent(total=segment_count, msg="Replaying segments %3.0f%%") for i, (segment, filename) in enumerate(self.io.segment_iterator()): pi.show(i) if index_transaction_id is not None and segment <= index_transaction_id: @@ -636,7 +636,7 @@ class Repository: segments_transaction_id = self.io.get_segments_transaction_id() self.prepare_txn(None) # self.index, self.compact, self.segments all empty now! segment_count = sum(1 for _ in self.io.segment_iterator()) - pi = ProgressIndicatorPercent(total=segment_count, msg="Checking segments %3.1f%%", step=0.1, same_line=True) + pi = ProgressIndicatorPercent(total=segment_count, msg="Checking segments %3.1f%%", step=0.1) for i, (segment, filename) in enumerate(self.io.segment_iterator()): pi.show(i) if segment > transaction_id: diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 0f8a853e..c398ee28 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -887,7 +887,7 @@ def test_progress_percentage_multiline(capfd): def test_progress_percentage_sameline(capfd): - pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=True, msg="%3.0f%%") + pi = ProgressIndicatorPercent(1000, step=5, start=0, msg="%3.0f%%") pi.show(0) out, err = capfd.readouterr() assert err == ' 0%\r' diff --git a/src/borg/upgrader.py b/src/borg/upgrader.py index 42f9a469..69712832 100644 --- a/src/borg/upgrader.py +++ b/src/borg/upgrader.py @@ -77,7 +77,7 @@ class AtticRepositoryUpgrader(Repository): replace the 8 first bytes of all regular files in there.""" logger.info("converting %d segments..." % len(segments)) segment_count = len(segments) - pi = ProgressIndicatorPercent(total=segment_count, msg="Converting segments %3.0f%%", same_line=True) + pi = ProgressIndicatorPercent(total=segment_count, msg="Converting segments %3.0f%%") for i, filename in enumerate(segments): if progress: pi.show(i) From 4d214e2503bc28f4b75b89288935f8fecf7cda4e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 9 Aug 2016 20:49:56 +0200 Subject: [PATCH 0150/1387] Simplify and test Item.file_size --- src/borg/item.py | 5 +---- src/borg/testsuite/item.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/borg/item.py b/src/borg/item.py index b97b470f..0bc33623 100644 --- a/src/borg/item.py +++ b/src/borg/item.py @@ -160,10 +160,7 @@ class Item(PropDict): def file_size(self): if 'chunks' not in self: return 0 - total_size = 0 - for chunk_id, size, csize in self.chunks: - total_size += size - return total_size + return sum(chunk.size for chunk in self.chunks) class EncryptedKey(PropDict): diff --git a/src/borg/testsuite/item.py b/src/borg/testsuite/item.py index b0b7569e..fc60e91d 100644 --- a/src/borg/testsuite/item.py +++ b/src/borg/testsuite/item.py @@ -1,5 +1,6 @@ import pytest +from ..cache import ChunkListEntry from ..item import Item from ..helpers import StableDict @@ -145,3 +146,16 @@ def test_unknown_property(): item = Item() with pytest.raises(AttributeError): item.unknown_attribute = None + + +def test_item_file_size(): + item = Item(chunks=[ + ChunkListEntry(csize=1, size=1000, id=None), + ChunkListEntry(csize=1, size=2000, id=None), + ]) + assert item.file_size() == 3000 + + +def test_item_file_size_no_chunks(): + item = Item() + assert item.file_size() == 0 From f8bb73732ce5ba47ab3efa54f7b7b492f5d5d488 Mon Sep 17 00:00:00 2001 From: Andrew Engelbrecht Date: Sun, 21 Aug 2016 01:04:16 -0400 Subject: [PATCH 0151/1387] added doc for "pull" type backup over sshfs Fixes #900 --- docs/usage.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index d9212ef5..e9fa7179 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -271,6 +271,14 @@ Examples # use zlib compression (good, but slow) - default is no compression $ borg create -C zlib,6 /path/to/repo::root-{now:%Y-%m-%d} / --one-file-system + # Backup a remote host locally ("pull" style) using sshfs + $ mkdir sshfs-mount + $ sshfs root@example.com:/ sshfs-mount + $ cd sshfs-mount + $ borg create /path/to/repo::example.com-root-{now:%Y-%m-%d} . + $ cd .. + $ fusermount -u sshfs-mount + # Make a big effort in fine granular deduplication (big chunk management # overhead, needs a lot of RAM and disk space, see formula in internals # docs - same parameters as borg < 1.0 or attic): From 273bd57cd821b23614293fac56b9ec7fd57ba0d5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 21 Aug 2016 18:13:23 +0200 Subject: [PATCH 0152/1387] re-enable fuse tests for RemoteArchiver at some time they had deadlock issues, but it worked for me now. --- borg/testsuite/archiver.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 1774f347..d67cb879 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1323,15 +1323,6 @@ class RemoteArchiverTestCase(ArchiverTestCase): with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]): self.cmd('init', self.repository_location + '_3') - # skip fuse tests here, they deadlock since this change in exec_cmd: - # -output = subprocess.check_output(borg + args, stderr=None) - # +output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT) - # this was introduced because some tests expect stderr contents to show up - # in "output" also. Also, the non-forking exec_cmd catches both, too. - @unittest.skip('deadlock issues') - def test_fuse(self): - pass - @unittest.skip('only works locally') def test_debug_put_get_delete_obj(self): pass From e7d44cec39ae96229ce36e20fe81ef281691aea8 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 21 Aug 2016 17:35:00 +0200 Subject: [PATCH 0153/1387] extract: --progress: Calculating size --- src/borg/archiver.py | 2 ++ src/borg/helpers.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 417f055e..6c46daf6 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -456,6 +456,8 @@ class Archiver: filter = self.build_filter(matcher, item_is_hardlink_master, strip_components) if progress: + progress_logger = logging.getLogger(ProgressIndicatorPercent.LOGGER) + progress_logger.info('Calculating size') extracted_size = sum(item.file_size() for item in archive.iter_items(filter)) pi = ProgressIndicatorPercent(total=extracted_size, msg='Extracting files %5.1f%%', step=0.1) else: diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 72f03ac6..c9d99ef9 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1105,6 +1105,8 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, class ProgressIndicatorPercent: + LOGGER = 'borg.output.progress' + def __init__(self, total, step=5, start=0, same_line=True, msg="%3.0f%%"): """ Percentage-based progress indicator @@ -1122,7 +1124,7 @@ class ProgressIndicatorPercent: self.msg = msg self.same_line = same_line self.handler = None - self.logger = logging.getLogger('borg.output.progress') + self.logger = logging.getLogger(self.LOGGER) # If there are no handlers, set one up explicitly because the # terminator and propagation needs to be set. If there are, From ab31ffaa85d4871f405f38e8f1e7385f19f6205f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 21 Aug 2016 17:36:51 +0200 Subject: [PATCH 0154/1387] ProgressIndicatorPercent: remove same_line --- src/borg/helpers.py | 9 +++------ src/borg/testsuite/helpers.py | 29 +++++++---------------------- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index c9d99ef9..3d30692f 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1107,14 +1107,13 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, class ProgressIndicatorPercent: LOGGER = 'borg.output.progress' - def __init__(self, total, step=5, start=0, same_line=True, msg="%3.0f%%"): + def __init__(self, total, step=5, start=0, msg="%3.0f%%"): """ Percentage-based progress indicator :param total: total amount of items :param step: step size in percent :param start: at which percent value to start - :param same_line: if True, emit output always on same line :param msg: output message, must contain one %f placeholder for the percentage """ self.counter = 0 # 0 .. (total-1) @@ -1122,7 +1121,6 @@ class ProgressIndicatorPercent: self.trigger_at = start # output next percentage value when reaching (at least) this self.step = step self.msg = msg - self.same_line = same_line self.handler = None self.logger = logging.getLogger(self.LOGGER) @@ -1132,7 +1130,7 @@ class ProgressIndicatorPercent: if not self.logger.handlers: self.handler = logging.StreamHandler(stream=sys.stderr) self.handler.setLevel(logging.INFO) - self.handler.terminator = '\r' if self.same_line else '\n' + self.handler.terminator = '\r' self.logger.addHandler(self.handler) if self.logger.level == logging.NOTSET: @@ -1162,8 +1160,7 @@ class ProgressIndicatorPercent: self.logger.info(self.msg % percent) def finish(self): - if self.same_line: - self.logger.info(" " * len(self.msg % 100.0)) + self.logger.info(" " * len(self.msg % 100.0)) class ProgressIndicatorEndless: diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index c398ee28..5f5a3806 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -870,24 +870,9 @@ def test_yes_env_output(capfd, monkeypatch): assert 'yes' in err -def test_progress_percentage_multiline(capfd): - pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=False, msg="%3.0f%%") - pi.show(0) - out, err = capfd.readouterr() - assert err == ' 0%\n' - pi.show(420) - out, err = capfd.readouterr() - assert err == ' 42%\n' - pi.show(1000) - out, err = capfd.readouterr() - assert err == '100%\n' - pi.finish() - out, err = capfd.readouterr() - assert err == '' - - def test_progress_percentage_sameline(capfd): pi = ProgressIndicatorPercent(1000, step=5, start=0, msg="%3.0f%%") + pi.logger.setLevel('INFO') pi.show(0) out, err = capfd.readouterr() assert err == ' 0%\r' @@ -904,22 +889,22 @@ def test_progress_percentage_sameline(capfd): def test_progress_percentage_step(capfd): - pi = ProgressIndicatorPercent(100, step=2, start=0, same_line=False, msg="%3.0f%%") + pi = ProgressIndicatorPercent(100, step=2, start=0, msg="%3.0f%%") + pi.logger.setLevel('INFO') pi.show() out, err = capfd.readouterr() - assert err == ' 0%\n' + assert err == ' 0%\r' pi.show() out, err = capfd.readouterr() assert err == '' # no output at 1% as we have step == 2 pi.show() out, err = capfd.readouterr() - assert err == ' 2%\n' + assert err == ' 2%\r' def test_progress_percentage_quiet(capfd): - logging.getLogger('borg.output.progress').setLevel(logging.WARN) - - pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=False, msg="%3.0f%%") + pi = ProgressIndicatorPercent(1000, step=5, start=0, msg="%3.0f%%") + pi.logger.setLevel('WARN') pi.show(0) out, err = capfd.readouterr() assert err == '' From 53d0140bd523b404890dead6f42bffa30e6d4181 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 21 Aug 2016 21:55:53 +0200 Subject: [PATCH 0155/1387] Repository: add compact_segments progress --- src/borg/repository.py | 5 +++++ src/borg/testsuite/archiver.py | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 8b5f85db..9eebd90e 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -442,14 +442,17 @@ class Repository: unused = [] logger.debug('compaction started.') + pi = ProgressIndicatorPercent(total=len(self.compact), msg='Compacting segments %3.0f%%', step=1) for segment, freeable_space in sorted(self.compact.items()): if not self.io.segment_exists(segment): logger.warning('segment %d not found, but listed in compaction data', segment) del self.compact[segment] + pi.show() continue segment_size = self.io.segment_size(segment) if segment_size > 0.2 * self.max_segment_size and freeable_space < 0.15 * segment_size: logger.debug('not compacting segment %d (only %d bytes are sparse)', segment, freeable_space) + pi.show() continue segments.setdefault(segment, 0) logger.debug('compacting segment %d with usage count %d and %d freeable bytes', @@ -526,6 +529,8 @@ class Repository: segments.setdefault(new_segment, 0) assert segments[segment] == 0 unused.append(segment) + pi.show() + pi.finish() complete_xfer(intermediate=False) logger.debug('compaction completed.') diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 45daceda..d45d603f 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -228,6 +228,8 @@ class ArchiverTestCaseBase(BaseTestCase): os.chdir(self._old_wd) # note: ignore_errors=True as workaround for issue #862 shutil.rmtree(self.tmpdir, ignore_errors=True) + # destroy logging configuration + logging.Logger.manager.loggerDict.clear() def cmd(self, *args, **kw): exit_code = kw.pop('exit_code', 0) @@ -1044,13 +1046,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): manifest, key = Manifest.load(repository) self.assert_equal(len(manifest.archives), 0) - def test_progress(self): + def test_progress_on(self): self.create_regular_file('file1', size=1024 * 80) self.cmd('init', self.repository_location) - # progress forced on output = self.cmd('create', '--progress', self.repository_location + '::test4', 'input') self.assert_in("\r", output) - # progress forced off + + def test_progress_off(self): + self.create_regular_file('file1', size=1024 * 80) + self.cmd('init', self.repository_location) output = self.cmd('create', self.repository_location + '::test5', 'input') self.assert_not_in("\r", output) From 7e80f6821db1b29441d540f2d83d919a203d787b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 21 Aug 2016 20:17:49 +0200 Subject: [PATCH 0156/1387] use trusty for testing, to have a recent FUSE --- .travis.yml | 3 +++ .travis/install.sh | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0ec266ed..c46fc6f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,12 +10,15 @@ matrix: include: - python: 3.4 os: linux + dist: trusty env: TOXENV=py34 - python: 3.5 os: linux + dist: trusty env: TOXENV=py35 - python: 3.5 os: linux + dist: trusty env: TOXENV=flake8 - language: generic os: osx diff --git a/.travis/install.sh b/.travis/install.sh index 73e292dd..6ebbbfed 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -31,7 +31,6 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then python -m pip install --user 'virtualenv<14.0' else pip install 'virtualenv<14.0' - sudo add-apt-repository -y ppa:gezakovacs/lz4 sudo apt-get update sudo apt-get install -y liblz4-dev sudo apt-get install -y libacl1-dev From 32bd29548bcbb9267546cf93ffba76871b25cca9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 21 Aug 2016 19:27:23 +0200 Subject: [PATCH 0157/1387] travis: test fuse-enabled borg --- .travis/install.sh | 5 ++++- tox.ini | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis/install.sh b/.travis/install.sh index 6ebbbfed..64ccd5a2 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -16,6 +16,8 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then brew install lz4 brew outdated pyenv || brew upgrade pyenv + brew install pkg-config + brew install Caskroom/versions/osxfuse-beta case "${TOXENV}" in py34) @@ -34,10 +36,11 @@ else sudo apt-get update sudo apt-get install -y liblz4-dev sudo apt-get install -y libacl1-dev + sudo apt-get install -y libfuse-dev fuse pkg-config # optional, for FUSE support fi python -m virtualenv ~/.venv source ~/.venv/bin/activate pip install -r requirements.d/development.txt pip install codecov -pip install -e . +pip install -e .[fuse] diff --git a/tox.ini b/tox.ini index bc6195d9..699ef251 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,8 @@ changedir = {toxworkdir} deps = -rrequirements.d/development.txt -rrequirements.d/attic.txt -commands = py.test --cov=borg --cov-config=../.coveragerc --benchmark-skip --pyargs {posargs:borg.testsuite} + -rrequirements.d/fuse.txt +commands = py.test -rs --cov=borg --cov-config=../.coveragerc --benchmark-skip --pyargs {posargs:borg.testsuite} # fakeroot -u needs some env vars: passenv = * From a7c370b5ed7fb6a692685eb94bc271f9b5308ee3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 21 Aug 2016 23:37:07 +0200 Subject: [PATCH 0158/1387] add debug-info usage help file --- docs/usage/debug-info.rst.inc | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/usage/debug-info.rst.inc diff --git a/docs/usage/debug-info.rst.inc b/docs/usage/debug-info.rst.inc new file mode 100644 index 00000000..2f4f7237 --- /dev/null +++ b/docs/usage/debug-info.rst.inc @@ -0,0 +1,35 @@ +.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! + +.. _borg_debug-info: + +borg debug-info +--------------- +:: + + usage: borg debug-info [-h] [--critical] [--error] [--warning] [--info] + [--debug] [--lock-wait N] [--show-rc] + [--no-files-cache] [--umask M] [--remote-path PATH] + + display system information for debugging / bug reports + + optional arguments: + -h, --help show this help message and exit + --critical work on log level CRITICAL + --error work on log level ERROR + --warning work on log level WARNING (default) + --info, -v, --verbose + work on log level INFO + --debug work on log level DEBUG + --lock-wait N wait for the lock, but max. N seconds (default: 1). + --show-rc show/log the return code (rc) + --no-files-cache do not load/update the file metadata cache used to + detect unchanged files + --umask M set umask to M (local and remote, default: 0077) + --remote-path PATH set remote path to executable (default: "borg") + +Description +~~~~~~~~~~~ + +This command displays some system information that might be useful for bug +reports and debugging problems. If a traceback happens, this information is +already appended at the end of the traceback. From 3f30649a8528db2806c607dd6f1d1b08c6004b33 Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Sat, 25 Jun 2016 22:38:47 +0100 Subject: [PATCH 0159/1387] internals.rst: fix typos --- docs/internals.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/internals.rst b/docs/internals.rst index 82be188b..798ce856 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -48,7 +48,7 @@ Lock files the repository. The locking system is based on creating a directory `lock.exclusive` (for -exclusive locks). Inside the lock directory, there is a file indication +exclusive locks). Inside the lock directory, there is a file indicating hostname, process id and thread id of the lock holder. There is also a json file `lock.roster` that keeps a directory of all shared @@ -338,7 +338,7 @@ more chunks than estimated above, because 1 file is at least 1 chunk). If a remote repository is used the repo index will be allocated on the remote side. -E.g. backing up a total count of 1 Mi (IEC binary prefix e.g. 2^20) files with a total size of 1TiB. +E.g. backing up a total count of 1 Mi (IEC binary prefix i.e. 2^20) files with a total size of 1TiB. a) with ``create --chunker-params 10,23,16,4095`` (custom, like borg < 1.0 or attic): From 2a41569fecf40ce07b464d29d9c6a68a3f8a187a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 21 Aug 2016 20:17:49 +0200 Subject: [PATCH 0160/1387] use trusty for testing, to have a recent FUSE --- .travis/install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis/install.sh b/.travis/install.sh index 1f86ee38..9799b59f 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -32,6 +32,7 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then python -m pip install --user 'virtualenv<14.0' else pip install 'virtualenv<14.0' + sudo apt-get update sudo apt-get install -y liblz4-dev sudo apt-get install -y libacl1-dev fi From ebe11435706205c46f4e003c97d38fc68a1c21c3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 21 Aug 2016 19:27:23 +0200 Subject: [PATCH 0161/1387] travis: test fuse-enabled borg --- .travis/install.sh | 5 ++++- tox.ini | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis/install.sh b/.travis/install.sh index 9799b59f..3faec02d 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -17,6 +17,8 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then brew install lz4 brew install xz # required for python lzma module brew outdated pyenv || brew upgrade pyenv + brew install pkg-config + brew install Caskroom/versions/osxfuse-beta case "${TOXENV}" in py34) @@ -35,10 +37,11 @@ else sudo apt-get update sudo apt-get install -y liblz4-dev sudo apt-get install -y libacl1-dev + sudo apt-get install -y libfuse-dev fuse pkg-config # optional, for FUSE support fi python -m virtualenv ~/.venv source ~/.venv/bin/activate pip install -r requirements.d/development.txt pip install codecov -pip install -e . +pip install -e .[fuse] diff --git a/tox.ini b/tox.ini index 12434988..bcd560c5 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,8 @@ envlist = py{34,35,36},flake8 deps = -rrequirements.d/development.txt -rrequirements.d/attic.txt -commands = py.test --cov=borg --cov-config=.coveragerc --benchmark-skip --pyargs {posargs:borg.testsuite} + -rrequirements.d/fuse.txt +commands = py.test -rs --cov=borg --cov-config=.coveragerc --benchmark-skip --pyargs {posargs:borg.testsuite} # fakeroot -u needs some env vars: passenv = * From d1d2738381e645fcd22a6dac22199e21e0c259ba Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 21 Aug 2016 18:13:23 +0200 Subject: [PATCH 0162/1387] re-enable fuse tests for RemoteArchiver at some time they had deadlock issues, but it worked for me now. --- src/borg/testsuite/archiver.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index d45d603f..9d68e3ee 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1964,15 +1964,6 @@ class RemoteArchiverTestCase(ArchiverTestCase): with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]): self.cmd('init', self.repository_location + '_3') - # skip fuse tests here, they deadlock since this change in exec_cmd: - # -output = subprocess.check_output(borg + args, stderr=None) - # +output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT) - # this was introduced because some tests expect stderr contents to show up - # in "output" also. Also, the non-forking exec_cmd catches both, too. - @unittest.skip('deadlock issues') - def test_fuse(self): - pass - @unittest.skip('only works locally') def test_debug_put_get_delete_obj(self): pass From 6da34fcc5a04888ae889c5b94f11dfa5bbfdc7ec Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 21 Aug 2016 23:37:07 +0200 Subject: [PATCH 0163/1387] add debug-info usage help file --- docs/usage/debug-info.rst.inc | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/usage/debug-info.rst.inc diff --git a/docs/usage/debug-info.rst.inc b/docs/usage/debug-info.rst.inc new file mode 100644 index 00000000..2f4f7237 --- /dev/null +++ b/docs/usage/debug-info.rst.inc @@ -0,0 +1,35 @@ +.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! + +.. _borg_debug-info: + +borg debug-info +--------------- +:: + + usage: borg debug-info [-h] [--critical] [--error] [--warning] [--info] + [--debug] [--lock-wait N] [--show-rc] + [--no-files-cache] [--umask M] [--remote-path PATH] + + display system information for debugging / bug reports + + optional arguments: + -h, --help show this help message and exit + --critical work on log level CRITICAL + --error work on log level ERROR + --warning work on log level WARNING (default) + --info, -v, --verbose + work on log level INFO + --debug work on log level DEBUG + --lock-wait N wait for the lock, but max. N seconds (default: 1). + --show-rc show/log the return code (rc) + --no-files-cache do not load/update the file metadata cache used to + detect unchanged files + --umask M set umask to M (local and remote, default: 0077) + --remote-path PATH set remote path to executable (default: "borg") + +Description +~~~~~~~~~~~ + +This command displays some system information that might be useful for bug +reports and debugging problems. If a traceback happens, this information is +already appended at the end of the traceback. From 85311f0116af83a6e4b26fe5a2ee8e1f084bcbb3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 22 Aug 2016 01:15:03 +0200 Subject: [PATCH 0164/1387] fix .coverage processing --- .travis/upload_coverage.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis/upload_coverage.sh b/.travis/upload_coverage.sh index 4cb8273c..03475fc9 100755 --- a/.travis/upload_coverage.sh +++ b/.travis/upload_coverage.sh @@ -6,7 +6,6 @@ set -x NO_COVERAGE_TOXENVS=(pep8) if ! [[ "${NO_COVERAGE_TOXENVS[*]}" =~ "${TOXENV}" ]]; then source ~/.venv/bin/activate - ln .tox/.coverage .coverage # on osx, tests run as root, need access to .coverage sudo chmod 666 .coverage codecov -e TRAVIS_OS_NAME TOXENV From 2cfd9053472987bd4da0e642ebec9265a511f13a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 21 Aug 2016 01:11:46 +0200 Subject: [PATCH 0165/1387] Vagrant: pyinstaller: use pre-built linux bootloaders, fixes #1506 --- Vagrantfile | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 539ed381..c489e707 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -236,7 +236,8 @@ def install_pyinstaller(boxname) . borg-env/bin/activate git clone https://github.com/pyinstaller/pyinstaller.git cd pyinstaller - git checkout v3.1.1 + # develop branch, with rebuilt bootloaders, with ThomasWaldmann/do-not-overwrite-LD_LP + git checkout fd3df7796afa367e511c881dac983cad0697b9a3 pip install -e . EOF end @@ -248,13 +249,11 @@ def install_pyinstaller_bootloader(boxname) . borg-env/bin/activate git clone https://github.com/pyinstaller/pyinstaller.git cd pyinstaller - # develop branch, merge commit of ThomasWaldmann/do-not-overwrite-LD_LP - git checkout 639fcec992d753db2058314b843bccc37b815265 + # develop branch, with rebuilt bootloaders, with ThomasWaldmann/do-not-overwrite-LD_LP + git checkout fd3df7796afa367e511c881dac983cad0697b9a3 # build bootloader, if it is not included cd bootloader - # XXX temporarily use --no-lsb as we have no LSB environment - # XXX https://github.com/borgbackup/borg/issues/1506 - python ./waf --no-lsb all + python ./waf all cd .. pip install -e . EOF @@ -395,8 +394,7 @@ Vagrant.configure(2) do |config| b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("wheezy32") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("wheezy32") b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("wheezy32") - # XXX https://github.com/borgbackup/borg/issues/1506 - b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller_bootloader("wheezy32") + b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller("wheezy32") b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("wheezy32") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("wheezy32") end @@ -409,8 +407,7 @@ Vagrant.configure(2) do |config| b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("wheezy64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("wheezy64") b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("wheezy64") - # XXX https://github.com/borgbackup/borg/issues/1506 - b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller_bootloader("wheezy64") + b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller("wheezy64") b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("wheezy64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("wheezy64") end From 484c091c622511d190a155a96bd2b1a8836a8642 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Mon, 22 Aug 2016 19:48:39 +0200 Subject: [PATCH 0166/1387] =?UTF-8?q?RepositoryServer:=20Don=E2=80=98t=20t?= =?UTF-8?q?ry=20to=20close=20the=20repository=20if=20it=20was=20not=20yet?= =?UTF-8?q?=20opened.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- borg/remote.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/borg/remote.py b/borg/remote.py index 472d1ac3..5c42b58a 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -79,12 +79,14 @@ class RepositoryServer: # pragma: no cover if r: data = os.read(stdin_fd, BUFSIZE) if not data: - self.repository.close() + if self.repository is not None: + self.repository.close() return unpacker.feed(data) for unpacked in unpacker: if not (isinstance(unpacked, tuple) and len(unpacked) == 4): - self.repository.close() + if self.repository is not None: + self.repository.close() raise Exception("Unexpected RPC data format.") type, msgid, method, args = unpacked method = method.decode('ascii') From 972392e290e3e38beef32ce455154312452e7869 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Mon, 22 Aug 2016 22:58:54 +0200 Subject: [PATCH 0167/1387] extract: When doing a partial restore don't leak prefetched chunks. The filter function passed to iter_items (with preload=True) may never return True for items that are not really extracted later because that would leak prefetched items. For restoring hard linked files the item containing the actual chunks might not be matched or implicitly removed from the restore by strip_components. For this reason the chunk list or all items that can potentially be used as hardlink target needs to be stored. To achive both requirements at the same time the filter function needs to store the needed information for the hardlinks while not returning True just because it could be a hardlink target. Known problems: When using progress indication the calculated extracted_size now can be smaller than the actual extracted size in presence of hard links (master is not restored) instead of bigger (potential master not used in restore). --- src/borg/archive.py | 4 ++-- src/borg/archiver.py | 23 ++++++++++------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index b1768542..f4acf874 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -161,11 +161,11 @@ class DownloadPipeline: for _, data in self.fetch_many(ids): unpacker.feed(data) items = [Item(internal_dict=item) for item in unpacker] - if filter: - items = [item for item in items if filter(item)] for item in items: if 'chunks' in item: item.chunks = [ChunkListEntry(*e) for e in item.chunks] + if filter: + items = [item for item in items if filter(item)] if preload: for item in items: if 'chunks' in item: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 619dbd7e..b712d2a8 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -417,15 +417,15 @@ class Archiver: self.print_file_status(status, path) @staticmethod - def build_filter(matcher, is_hardlink_master, strip_components=0): + def build_filter(matcher, peek_and_store_hardlink_masters, strip_components=0): if strip_components: def item_filter(item): - return (is_hardlink_master(item) or - matcher.match(item.path) and os.sep.join(item.path.split(os.sep)[strip_components:])) + peek_and_store_hardlink_masters(item) + return matcher.match(item.path) and os.sep.join(item.path.split(os.sep)[strip_components:]) else: def item_filter(item): - return (is_hardlink_master(item) or - matcher.match(item.path)) + peek_and_store_hardlink_masters(item) + return matcher.match(item.path) return item_filter @with_repository() @@ -450,11 +450,12 @@ class Archiver: partial_extract = not matcher.empty() or strip_components hardlink_masters = {} if partial_extract else None - def item_is_hardlink_master(item): - return (partial_extract and stat.S_ISREG(item.mode) and - item.get('hardlink_master', True) and 'source' not in item) + def peek_and_store_hardlink_masters(item): + if (partial_extract and stat.S_ISREG(item.mode) and + item.get('hardlink_master', True) and 'source' not in item): + hardlink_masters[item.get('path')] = (item.get('chunks'), None) - filter = self.build_filter(matcher, item_is_hardlink_master, strip_components) + filter = self.build_filter(matcher, peek_and_store_hardlink_masters, strip_components) if progress: progress_logger = logging.getLogger(ProgressIndicatorPercent.LOGGER) progress_logger.info('Calculating size') @@ -465,10 +466,6 @@ class Archiver: for item in archive.iter_items(filter, preload=True): orig_path = item.path - if item_is_hardlink_master(item): - hardlink_masters[orig_path] = (item.get('chunks'), None) - if not matcher.match(item.path): - continue if strip_components: item.path = os.sep.join(orig_path.split(os.sep)[strip_components:]) if not args.dry_run: From a026febdb03e8beaa8c267de9cfbea46aec28a45 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Mon, 22 Aug 2016 23:07:38 +0200 Subject: [PATCH 0168/1387] Archiver.build_filter: strip_components is no longer a optional parameter. --- src/borg/archiver.py | 2 +- src/borg/testsuite/archiver.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index b712d2a8..d3a8f76d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -417,7 +417,7 @@ class Archiver: self.print_file_status(status, path) @staticmethod - def build_filter(matcher, peek_and_store_hardlink_masters, strip_components=0): + def build_filter(matcher, peek_and_store_hardlink_masters, strip_components): if strip_components: def item_filter(item): peek_and_store_hardlink_masters(item) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 9d68e3ee..34a8acb3 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2210,14 +2210,14 @@ class TestBuildFilter: def test_basic(self): matcher = PatternMatcher() matcher.add([parse_pattern('included')], True) - filter = Archiver.build_filter(matcher, self.item_is_hardlink_master) + filter = Archiver.build_filter(matcher, self.item_is_hardlink_master, 0) assert filter(Item(path='included')) assert filter(Item(path='included/file')) assert not filter(Item(path='something else')) def test_empty(self): matcher = PatternMatcher(fallback=True) - filter = Archiver.build_filter(matcher, self.item_is_hardlink_master) + filter = Archiver.build_filter(matcher, self.item_is_hardlink_master, 0) assert filter(Item(path='anything')) def test_strip_components(self): From b845a074cb8135b9c4bb782a81ec5616350d834d Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Mon, 22 Aug 2016 23:36:43 +0200 Subject: [PATCH 0169/1387] tests: TestBuildFilter: Adjust from item_is_hardlink_master to peek_and_store_hardlink_masters. --- src/borg/testsuite/archiver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 34a8acb3..aa661431 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2204,25 +2204,25 @@ def test_compare_chunk_contents(): class TestBuildFilter: @staticmethod - def item_is_hardlink_master(item): + def peek_and_store_hardlink_masters(item): return False def test_basic(self): matcher = PatternMatcher() matcher.add([parse_pattern('included')], True) - filter = Archiver.build_filter(matcher, self.item_is_hardlink_master, 0) + filter = Archiver.build_filter(matcher, self.peek_and_store_hardlink_masters, 0) assert filter(Item(path='included')) assert filter(Item(path='included/file')) assert not filter(Item(path='something else')) def test_empty(self): matcher = PatternMatcher(fallback=True) - filter = Archiver.build_filter(matcher, self.item_is_hardlink_master, 0) + filter = Archiver.build_filter(matcher, self.peek_and_store_hardlink_masters, 0) assert filter(Item(path='anything')) def test_strip_components(self): matcher = PatternMatcher(fallback=True) - filter = Archiver.build_filter(matcher, self.item_is_hardlink_master, strip_components=1) + filter = Archiver.build_filter(matcher, self.peek_and_store_hardlink_masters, strip_components=1) assert not filter(Item(path='shallow')) assert not filter(Item(path='shallow/')) # can this even happen? paths are normalized... assert filter(Item(path='deep enough/file')) From ff04c059b78c4070d36a1db9056962e380090ce0 Mon Sep 17 00:00:00 2001 From: sven Date: Tue, 23 Aug 2016 14:27:26 +0200 Subject: [PATCH 0170/1387] Addjust border color This changes the border-color to match with the background-color --- docs/borg_theme/css/borg.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index c4d8688f..fd80000d 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -9,6 +9,11 @@ background-color: #000000 !important; } +.wy-side-nav-search input[type="text"] { + border-color: #000000; +} + + .wy-side-nav-search > a { color: rgba(255, 255, 255, 0.5); } From 248ccf0149ce638a83382299965d58cc7a557e82 Mon Sep 17 00:00:00 2001 From: sven Date: Tue, 23 Aug 2016 15:01:39 +0200 Subject: [PATCH 0171/1387] Update borg.css --- docs/borg_theme/css/borg.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 7c5ec811..612c0aa4 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -9,6 +9,10 @@ background-color: #000000 !important; } +.wy-side-nav-search input[type="text"] { + border-color: #000000; +} + .wy-side-nav-search > a { color: rgba(255, 255, 255, 0.5); } From ddb1c60964209117483e4bc6771b1c612aba5817 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 22 Aug 2016 04:42:42 +0200 Subject: [PATCH 0172/1387] repo tests: use H(x) instead of byte literals --- borg/testsuite/repository.py | 72 ++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/borg/testsuite/repository.py b/borg/testsuite/repository.py index 9c5a5a46..56e95578 100644 --- a/borg/testsuite/repository.py +++ b/borg/testsuite/repository.py @@ -61,59 +61,59 @@ class RepositoryTestCase(RepositoryTestCaseBase): def test2(self): """Test multiple sequential transactions """ - self.repository.put(b'00000000000000000000000000000000', b'foo') - self.repository.put(b'00000000000000000000000000000001', b'foo') + self.repository.put(H(0), b'foo') + self.repository.put(H(1), b'foo') self.repository.commit() - self.repository.delete(b'00000000000000000000000000000000') - self.repository.put(b'00000000000000000000000000000001', b'bar') + self.repository.delete(H(0)) + self.repository.put(H(1), b'bar') self.repository.commit() - self.assert_equal(self.repository.get(b'00000000000000000000000000000001'), b'bar') + self.assert_equal(self.repository.get(H(1)), b'bar') def test_consistency(self): """Test cache consistency """ - self.repository.put(b'00000000000000000000000000000000', b'foo') - self.assert_equal(self.repository.get(b'00000000000000000000000000000000'), b'foo') - self.repository.put(b'00000000000000000000000000000000', b'foo2') - self.assert_equal(self.repository.get(b'00000000000000000000000000000000'), b'foo2') - self.repository.put(b'00000000000000000000000000000000', b'bar') - self.assert_equal(self.repository.get(b'00000000000000000000000000000000'), b'bar') - self.repository.delete(b'00000000000000000000000000000000') - self.assert_raises(Repository.ObjectNotFound, lambda: self.repository.get(b'00000000000000000000000000000000')) + self.repository.put(H(0), b'foo') + self.assert_equal(self.repository.get(H(0)), b'foo') + self.repository.put(H(0), b'foo2') + self.assert_equal(self.repository.get(H(0)), b'foo2') + self.repository.put(H(0), b'bar') + self.assert_equal(self.repository.get(H(0)), b'bar') + self.repository.delete(H(0)) + self.assert_raises(Repository.ObjectNotFound, lambda: self.repository.get(H(0))) def test_consistency2(self): """Test cache consistency2 """ - self.repository.put(b'00000000000000000000000000000000', b'foo') - self.assert_equal(self.repository.get(b'00000000000000000000000000000000'), b'foo') + self.repository.put(H(0), b'foo') + self.assert_equal(self.repository.get(H(0)), b'foo') self.repository.commit() - self.repository.put(b'00000000000000000000000000000000', b'foo2') - self.assert_equal(self.repository.get(b'00000000000000000000000000000000'), b'foo2') + self.repository.put(H(0), b'foo2') + self.assert_equal(self.repository.get(H(0)), b'foo2') self.repository.rollback() - self.assert_equal(self.repository.get(b'00000000000000000000000000000000'), b'foo') + self.assert_equal(self.repository.get(H(0)), b'foo') def test_overwrite_in_same_transaction(self): """Test cache consistency2 """ - self.repository.put(b'00000000000000000000000000000000', b'foo') - self.repository.put(b'00000000000000000000000000000000', b'foo2') + self.repository.put(H(0), b'foo') + self.repository.put(H(0), b'foo2') self.repository.commit() - self.assert_equal(self.repository.get(b'00000000000000000000000000000000'), b'foo2') + self.assert_equal(self.repository.get(H(0)), b'foo2') def test_single_kind_transactions(self): # put - self.repository.put(b'00000000000000000000000000000000', b'foo') + self.repository.put(H(0), b'foo') self.repository.commit() self.repository.close() # replace self.repository = self.open() with self.repository: - self.repository.put(b'00000000000000000000000000000000', b'bar') + self.repository.put(H(0), b'bar') self.repository.commit() # delete self.repository = self.open() with self.repository: - self.repository.delete(b'00000000000000000000000000000000') + self.repository.delete(H(0)) self.repository.commit() def test_list(self): @@ -131,22 +131,22 @@ class RepositoryTestCase(RepositoryTestCaseBase): def test_max_data_size(self): max_data = b'x' * MAX_DATA_SIZE - self.repository.put(b'00000000000000000000000000000000', max_data) - self.assert_equal(self.repository.get(b'00000000000000000000000000000000'), max_data) + self.repository.put(H(0), max_data) + self.assert_equal(self.repository.get(H(0)), max_data) self.assert_raises(IntegrityError, - lambda: self.repository.put(b'00000000000000000000000000000001', max_data + b'x')) + lambda: self.repository.put(H(1), max_data + b'x')) class RepositoryCommitTestCase(RepositoryTestCaseBase): def add_keys(self): - self.repository.put(b'00000000000000000000000000000000', b'foo') - self.repository.put(b'00000000000000000000000000000001', b'bar') - self.repository.put(b'00000000000000000000000000000003', b'bar') + self.repository.put(H(0), b'foo') + self.repository.put(H(1), b'bar') + self.repository.put(H(3), b'bar') self.repository.commit() - self.repository.put(b'00000000000000000000000000000001', b'bar2') - self.repository.put(b'00000000000000000000000000000002', b'boo') - self.repository.delete(b'00000000000000000000000000000003') + self.repository.put(H(1), b'bar2') + self.repository.put(H(2), b'boo') + self.repository.delete(H(3)) def test_replay_of_missing_index(self): self.add_keys() @@ -264,19 +264,19 @@ class RepositoryAppendOnlyTestCase(RepositoryTestCaseBase): def test_append_only(self): def segments_in_repository(): return len(list(self.repository.io.segment_iterator())) - self.repository.put(b'00000000000000000000000000000000', b'foo') + self.repository.put(H(0), b'foo') self.repository.commit() self.repository.append_only = False assert segments_in_repository() == 1 - self.repository.put(b'00000000000000000000000000000000', b'foo') + self.repository.put(H(0), b'foo') self.repository.commit() # normal: compact squashes the data together, only one segment assert segments_in_repository() == 1 self.repository.append_only = True assert segments_in_repository() == 1 - self.repository.put(b'00000000000000000000000000000000', b'foo') + self.repository.put(H(0), b'foo') self.repository.commit() # append only: does not compact, only new segments written assert segments_in_repository() == 2 From 0da0914955ee51aaa721617775f24813d01a4661 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 22 Aug 2016 04:52:18 +0200 Subject: [PATCH 0173/1387] repo tests: use H(x) instead of some similar constructs --- borg/testsuite/repository.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/borg/testsuite/repository.py b/borg/testsuite/repository.py index 56e95578..b95093e4 100644 --- a/borg/testsuite/repository.py +++ b/borg/testsuite/repository.py @@ -44,8 +44,8 @@ class RepositoryTestCase(RepositoryTestCaseBase): def test1(self): for x in range(100): - self.repository.put(('%-32d' % x).encode('ascii'), b'SOMEDATA') - key50 = ('%-32d' % 50).encode('ascii') + self.repository.put(H(x), b'SOMEDATA') + key50 = H(50) self.assert_equal(self.repository.get(key50), b'SOMEDATA') self.repository.delete(key50) self.assert_raises(Repository.ObjectNotFound, lambda: self.repository.get(key50)) @@ -56,7 +56,7 @@ class RepositoryTestCase(RepositoryTestCaseBase): for x in range(100): if x == 50: continue - self.assert_equal(repository2.get(('%-32d' % x).encode('ascii')), b'SOMEDATA') + self.assert_equal(repository2.get(H(x)), b'SOMEDATA') def test2(self): """Test multiple sequential transactions @@ -118,7 +118,7 @@ class RepositoryTestCase(RepositoryTestCaseBase): def test_list(self): for x in range(100): - self.repository.put(('%-32d' % x).encode('ascii'), b'SOMEDATA') + self.repository.put(H(x), b'SOMEDATA') all = self.repository.list() self.assert_equal(len(all), 100) first_half = self.repository.list(limit=50) @@ -222,7 +222,7 @@ class RepositoryCommitTestCase(RepositoryTestCaseBase): self.assert_equal(len(self.repository), 3) def test_ignores_commit_tag_in_data(self): - self.repository.put(b'0' * 32, LoggedIO.COMMIT) + self.repository.put(H(0), LoggedIO.COMMIT) self.reopen() with self.repository: io = self.repository.io @@ -294,12 +294,12 @@ class RepositoryCheckTestCase(RepositoryTestCaseBase): def get_objects(self, *ids): for id_ in ids: - self.repository.get(('%032d' % id_).encode('ascii')) + self.repository.get(H(id_)) def add_objects(self, segments): for ids in segments: for id_ in ids: - self.repository.put(('%032d' % id_).encode('ascii'), b'data') + self.repository.put(H(id_), b'data') self.repository.commit() def get_head(self): @@ -310,7 +310,7 @@ class RepositoryCheckTestCase(RepositoryTestCaseBase): def corrupt_object(self, id_): idx = self.open_index() - segment, offset = idx[('%032d' % id_).encode('ascii')] + segment, offset = idx[H(id_)] with open(os.path.join(self.tmppath, 'repository', 'data', '0', str(segment)), 'r+b') as fd: fd.seek(offset) fd.write(b'BOOM') @@ -401,8 +401,8 @@ class RepositoryCheckTestCase(RepositoryTestCaseBase): self.assert_equal(set([1, 2, 3, 4, 5, 6]), self.list_objects()) def test_crash_before_compact(self): - self.repository.put(bytes(32), b'data') - self.repository.put(bytes(32), b'data2') + self.repository.put(H(0), b'data') + self.repository.put(H(0), b'data2') # Simulate a crash before compact with patch.object(Repository, 'compact_segments') as compact: self.repository.commit() @@ -410,7 +410,7 @@ class RepositoryCheckTestCase(RepositoryTestCaseBase): self.reopen() with self.repository: self.check(repair=True) - self.assert_equal(self.repository.get(bytes(32)), b'data2') + self.assert_equal(self.repository.get(H(0)), b'data2') class RemoteRepositoryTestCase(RepositoryTestCase): From 93517ca30ef56f9f5e7d1caccf8d06e12242e64b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 22 Aug 2016 05:10:48 +0200 Subject: [PATCH 0174/1387] hashindex tests: use H(x) instead of some similar constructs note: hash values needed updating because H(x) formats differently. --- borg/testsuite/hashindex.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/borg/testsuite/hashindex.py b/borg/testsuite/hashindex.py index fba14695..75cd8022 100644 --- a/borg/testsuite/hashindex.py +++ b/borg/testsuite/hashindex.py @@ -23,19 +23,19 @@ class HashIndexTestCase(BaseTestCase): self.assert_equal(len(idx), 0) # Test set for x in range(100): - idx[bytes('%-32d' % x, 'ascii')] = make_value(x) + idx[H(x)] = make_value(x) self.assert_equal(len(idx), 100) for x in range(100): - self.assert_equal(idx[bytes('%-32d' % x, 'ascii')], make_value(x)) + self.assert_equal(idx[H(x)], make_value(x)) # Test update for x in range(100): - idx[bytes('%-32d' % x, 'ascii')] = make_value(x * 2) + idx[H(x)] = make_value(x * 2) self.assert_equal(len(idx), 100) for x in range(100): - self.assert_equal(idx[bytes('%-32d' % x, 'ascii')], make_value(x * 2)) + self.assert_equal(idx[H(x)], make_value(x * 2)) # Test delete for x in range(50): - del idx[bytes('%-32d' % x, 'ascii')] + del idx[H(x)] self.assert_equal(len(idx), 50) idx_name = tempfile.NamedTemporaryFile() idx.write(idx_name.name) @@ -47,7 +47,7 @@ class HashIndexTestCase(BaseTestCase): idx = cls.read(idx_name.name) self.assert_equal(len(idx), 50) for x in range(50, 100): - self.assert_equal(idx[bytes('%-32d' % x, 'ascii')], make_value(x * 2)) + self.assert_equal(idx[H(x)], make_value(x * 2)) idx.clear() self.assert_equal(len(idx), 0) idx.write(idx_name.name) @@ -56,11 +56,11 @@ class HashIndexTestCase(BaseTestCase): def test_nsindex(self): self._generic_test(NSIndex, lambda x: (x, x), - '80fba5b40f8cf12f1486f1ba33c9d852fb2b41a5b5961d3b9d1228cf2aa9c4c9') + 'b96ec1ddabb4278cc92261ee171f7efc979dc19397cc5e89b778f05fa25bf93f') def test_chunkindex(self): self._generic_test(ChunkIndex, lambda x: (x, x, x), - '1d71865e72e3c3af18d3c7216b6fa7b014695eaa3ed7f14cf9cd02fba75d1c95') + '9d437a1e145beccc790c69e66ba94fc17bd982d83a401c9c6e524609405529d8') def test_resize(self): n = 2000 # Must be >= MIN_BUCKETS @@ -70,11 +70,11 @@ class HashIndexTestCase(BaseTestCase): initial_size = os.path.getsize(idx_name.name) self.assert_equal(len(idx), 0) for x in range(n): - idx[bytes('%-32d' % x, 'ascii')] = x, x + idx[H(x)] = x, x idx.write(idx_name.name) self.assert_true(initial_size < os.path.getsize(idx_name.name)) for x in range(n): - del idx[bytes('%-32d' % x, 'ascii')] + del idx[H(x)] self.assert_equal(len(idx), 0) idx.write(idx_name.name) self.assert_equal(initial_size, os.path.getsize(idx_name.name)) @@ -82,7 +82,7 @@ class HashIndexTestCase(BaseTestCase): def test_iteritems(self): idx = NSIndex() for x in range(100): - idx[bytes('%-0.32d' % x, 'ascii')] = x, x + idx[H(x)] = x, x all = list(idx.iteritems()) self.assert_equal(len(all), 100) second_half = list(idx.iteritems(marker=all[49][0])) From 79de73685bc6b25607833b04aa8f619ebb381945 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Mon, 22 Aug 2016 19:50:53 +0200 Subject: [PATCH 0175/1387] remote: Change exception message for unexpected RPC data format to indicate dataflow direction don't print stacktraces to clean up error messages when sshing into a forces command to borg serve. --- borg/remote.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/borg/remote.py b/borg/remote.py index 5c42b58a..debb003f 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -37,6 +37,14 @@ class InvalidRPCMethod(Error): """RPC method {} is not valid""" +class UnexpectedRPCDataFormatFromClient(Error): + """Borg {}: Got unexpected RPC data format from client.""" + + +class UnexpectedRPCDataFormatFromServer(Error): + """Got unexpected RPC data format from server.""" + + class RepositoryServer: # pragma: no cover rpc_methods = ( '__len__', @@ -87,7 +95,7 @@ class RepositoryServer: # pragma: no cover if not (isinstance(unpacked, tuple) and len(unpacked) == 4): if self.repository is not None: self.repository.close() - raise Exception("Unexpected RPC data format.") + raise UnexpectedRPCDataFormatFromClient(__version__) type, msgid, method, args = unpacked method = method.decode('ascii') try: @@ -328,7 +336,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. self.unpacker.feed(data) for unpacked in self.unpacker: if not (isinstance(unpacked, tuple) and len(unpacked) == 4): - raise Exception("Unexpected RPC data format.") + raise UnexpectedRPCDataFormatFromServer() type, msgid, error, res = unpacked if msgid in self.ignore_responses: self.ignore_responses.remove(msgid) From 549be2129a8ac207df79abda6d0a21cdefc570dc Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Mon, 22 Aug 2016 20:22:02 +0200 Subject: [PATCH 0176/1387] RepositoryServer: Add error message when connection was closed before opening repo. --- borg/remote.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/borg/remote.py b/borg/remote.py index debb003f..20d0c185 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -89,6 +89,9 @@ class RepositoryServer: # pragma: no cover if not data: if self.repository is not None: self.repository.close() + else: + os.write(stderr_fd, "Borg {}: Got connection close before repository was opened.\n" + .format(__version__).encode()) return unpacker.feed(data) for unpacked in unpacker: From b5d7f1df26391b71ac789bf13d2a81af4b00d3a2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 Aug 2016 01:12:30 +0200 Subject: [PATCH 0177/1387] extract: fix incorrect progress output for hard links this produces correct output if any (non proper) subset of hardlinks are extracted. --- src/borg/archiver.py | 2 +- src/borg/item.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index d3a8f76d..cf2f233f 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -459,7 +459,7 @@ class Archiver: if progress: progress_logger = logging.getLogger(ProgressIndicatorPercent.LOGGER) progress_logger.info('Calculating size') - extracted_size = sum(item.file_size() for item in archive.iter_items(filter)) + extracted_size = sum(item.file_size(hardlink_masters) for item in archive.iter_items(filter)) pi = ProgressIndicatorPercent(total=extracted_size, msg='Extracting files %5.1f%%', step=0.1) else: pi = None diff --git a/src/borg/item.py b/src/borg/item.py index 0bc33623..05247870 100644 --- a/src/borg/item.py +++ b/src/borg/item.py @@ -157,10 +157,13 @@ class Item(PropDict): part = PropDict._make_property('part', int) - def file_size(self): - if 'chunks' not in self: + def file_size(self, hardlink_masters=None): + hardlink_masters = hardlink_masters or {} + chunks, _ = hardlink_masters.get(self.get('source'), (None, None)) + chunks = self.get('chunks', chunks) + if chunks is None: return 0 - return sum(chunk.size for chunk in self.chunks) + return sum(chunk.size for chunk in chunks) class EncryptedKey(PropDict): From c12bcff30fe3428cbca70d310a5adc9d4c807558 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 23 Aug 2016 22:40:01 +0200 Subject: [PATCH 0178/1387] work around fuse xattr test issue with recent fakeroot fakeroot >= 1.20.2 "supports" xattrs, but this support somehow leads to the fuse tests not seeing the xattrs in fuse, because the file visible in the fuse mount was not created via fakeroot. --- borg/testsuite/archiver.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index d67cb879..f563ea42 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -276,8 +276,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.path.join(self.input_path, 'hardlink')) # Symlink os.symlink('somewhere', os.path.join(self.input_path, 'link1')) - if xattr.is_enabled(self.input_path): - xattr.setxattr(os.path.join(self.input_path, 'file1'), 'user.foo', b'bar') + self.create_regular_file('fusexattr', size=1) + if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path): + # ironically, due to the way how fakeroot works, comparing fuse file xattrs to orig file xattrs + # will FAIL if fakeroot supports xattrs, thus we only set the xattr if XATTR_FAKEROOT is False. + # This is because fakeroot with xattr-support does not propagate xattrs of the underlying file + # into "fakeroot space". Because the xattrs exposed by borgfs are these of an underlying file + # (from fakeroots point of view) they are invisible to the test process inside the fakeroot. + xattr.setxattr(os.path.join(self.input_path, 'fusexattr'), 'user.foo', b'bar') # XXX this always fails for me # ubuntu 14.04, on a TMP dir filesystem with user_xattr, using fakeroot # same for newer ubuntu and centos. @@ -342,7 +348,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in(name, list_output) self.assert_dirs_equal('input', 'output/input') info_output = self.cmd('info', self.repository_location + '::test') - item_count = 3 if has_lchflags else 4 # one file is UF_NODUMP + item_count = 4 if has_lchflags else 5 # one file is UF_NODUMP self.assert_in('Number of files: %d' % item_count, info_output) shutil.rmtree(self.cache_path) with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='yes'): @@ -1048,7 +1054,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): with open(in_fn, 'rb') as in_f, open(out_fn, 'rb') as out_f: assert in_f.read() == out_f.read() # list/read xattrs - if xattr.is_enabled(self.input_path): + in_fn = 'input/fusexattr' + out_fn = os.path.join(mountpoint, 'input', 'fusexattr') + if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path): assert xattr.listxattr(out_fn) == ['user.foo', ] assert xattr.getxattr(out_fn, 'user.foo') == b'bar' else: From cd39ccb82106089129ced9ccccabf7a4703f0207 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 Aug 2016 21:16:20 +0200 Subject: [PATCH 0179/1387] fix overeager storing of hardlink masters n.b. we only need to store them for items that we wouldn't extract. this also fixes an intersting edge case in extracting hard links with --strip-components --- src/borg/archive.py | 8 +++++--- src/borg/archiver.py | 16 +++++++++------- src/borg/testsuite/archiver.py | 4 ++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index f4acf874..c27faf67 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -422,7 +422,7 @@ Number of files: {0.stats.nfiles}'''.format( return stats def extract_item(self, item, restore_attrs=True, dry_run=False, stdout=False, sparse=False, - hardlink_masters=None, original_path=None, pi=None): + hardlink_masters=None, stripped_components=0, original_path=None, pi=None): """ Extract archive item. @@ -432,9 +432,11 @@ Number of files: {0.stats.nfiles}'''.format( :param stdout: write extracted data to stdout :param sparse: write sparse files (chunk-granularity, independent of the original being sparse) :param hardlink_masters: maps paths to (chunks, link_target) for extracting subtrees with hardlinks correctly + :param stripped_components: stripped leading path components to correct hard link extraction :param original_path: 'path' key as stored in archive :param pi: ProgressIndicatorPercent (or similar) for file extraction progress (in bytes) """ + hardlink_masters = hardlink_masters or {} has_damaged_chunks = 'chunks_healthy' in item if dry_run or stdout: if 'chunks' in item: @@ -473,11 +475,11 @@ Number of files: {0.stats.nfiles}'''.format( os.makedirs(os.path.dirname(path)) # Hard link? if 'source' in item: - source = os.path.join(dest, item.source) + source = os.path.join(dest, *item.source.split(os.sep)[stripped_components:]) with backup_io(): if os.path.exists(path): os.unlink(path) - if not hardlink_masters: + if item.source not in hardlink_masters: os.link(source, path) return item.chunks, link_target = hardlink_masters[item.source] diff --git a/src/borg/archiver.py b/src/borg/archiver.py index cf2f233f..f5ebd730 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -420,12 +420,14 @@ class Archiver: def build_filter(matcher, peek_and_store_hardlink_masters, strip_components): if strip_components: def item_filter(item): - peek_and_store_hardlink_masters(item) - return matcher.match(item.path) and os.sep.join(item.path.split(os.sep)[strip_components:]) + matched = matcher.match(item.path) and os.sep.join(item.path.split(os.sep)[strip_components:]) + peek_and_store_hardlink_masters(item, matched) + return matched else: def item_filter(item): - peek_and_store_hardlink_masters(item) - return matcher.match(item.path) + matched = matcher.match(item.path) + peek_and_store_hardlink_masters(item, matched) + return matched return item_filter @with_repository() @@ -450,8 +452,8 @@ class Archiver: partial_extract = not matcher.empty() or strip_components hardlink_masters = {} if partial_extract else None - def peek_and_store_hardlink_masters(item): - if (partial_extract and stat.S_ISREG(item.mode) and + def peek_and_store_hardlink_masters(item, matched): + if (partial_extract and not matched and stat.S_ISREG(item.mode) and item.get('hardlink_master', True) and 'source' not in item): hardlink_masters[item.get('path')] = (item.get('chunks'), None) @@ -486,7 +488,7 @@ class Archiver: archive.extract_item(item, restore_attrs=False) else: archive.extract_item(item, stdout=stdout, sparse=sparse, hardlink_masters=hardlink_masters, - original_path=orig_path, pi=pi) + stripped_components=strip_components, original_path=orig_path, pi=pi) except BackupOSError as e: self.print_warning('%s: %s', remove_surrogates(orig_path), e) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index aa661431..fd7eb5fc 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2204,8 +2204,8 @@ def test_compare_chunk_contents(): class TestBuildFilter: @staticmethod - def peek_and_store_hardlink_masters(item): - return False + def peek_and_store_hardlink_masters(item, matched): + pass def test_basic(self): matcher = PatternMatcher() From 25fa443d2b2215904090596de6c470d65db9e467 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 26 Aug 2016 20:59:22 +0200 Subject: [PATCH 0180/1387] repo tests: convert some more byte literals to H(x) --- src/borg/testsuite/repository.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 8754ba11..620bcaf3 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -163,22 +163,22 @@ class LocalRepositoryTestCase(RepositoryTestCaseBase): assert self.repository.compact[0] == 41 + 9 def test_sparse1(self): - self.repository.put(b'00000000000000000000000000000000', b'foo') - self.repository.put(b'00000000000000000000000000000001', b'123456789') + self.repository.put(H(0), b'foo') + self.repository.put(H(1), b'123456789') self.repository.commit() - self.repository.put(b'00000000000000000000000000000001', b'bar') + self.repository.put(H(1), b'bar') self._assert_sparse() def test_sparse2(self): - self.repository.put(b'00000000000000000000000000000000', b'foo') - self.repository.put(b'00000000000000000000000000000001', b'123456789') + self.repository.put(H(0), b'foo') + self.repository.put(H(1), b'123456789') self.repository.commit() - self.repository.delete(b'00000000000000000000000000000001') + self.repository.delete(H(1)) self._assert_sparse() def test_sparse_delete(self): - self.repository.put(b'00000000000000000000000000000000', b'1245') - self.repository.delete(b'00000000000000000000000000000000') + self.repository.put(H(0), b'1245') + self.repository.delete(H(0)) self.repository.io._write_fd.sync() # The on-line tracking works on a per-object basis... @@ -385,7 +385,7 @@ class RepositoryFreeSpaceTestCase(RepositoryTestCaseBase): self.reopen() with self.repository: - self.repository.put(b'00000000000000000000000000000000', b'foobar') + self.repository.put(H(0), b'foobar') with pytest.raises(Repository.InsufficientFreeSpaceError): self.repository.commit() @@ -393,13 +393,13 @@ class RepositoryFreeSpaceTestCase(RepositoryTestCaseBase): class RepositoryAuxiliaryCorruptionTestCase(RepositoryTestCaseBase): def setUp(self): super().setUp() - self.repository.put(b'00000000000000000000000000000000', b'foo') + self.repository.put(H(0), b'foo') self.repository.commit() self.repository.close() def do_commit(self): with self.repository: - self.repository.put(b'00000000000000000000000000000000', b'fox') + self.repository.put(H(0), b'fox') self.repository.commit() def test_corrupted_hints(self): From 517ccc2d5832161560f5960eaeb6bef75840133a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 26 Aug 2016 22:15:29 +0200 Subject: [PATCH 0181/1387] ProgressIndicatorPercent: output(message) to override output --- src/borg/helpers.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 3d30692f..5e6b74cd 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1107,7 +1107,7 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, class ProgressIndicatorPercent: LOGGER = 'borg.output.progress' - def __init__(self, total, step=5, start=0, msg="%3.0f%%"): + def __init__(self, total=0, step=5, start=0, msg="%3.0f%%"): """ Percentage-based progress indicator @@ -1121,6 +1121,7 @@ class ProgressIndicatorPercent: self.trigger_at = start # output next percentage value when reaching (at least) this self.step = step self.msg = msg + self.output_len = len(self.msg % 100.0) self.handler = None self.logger = logging.getLogger(self.LOGGER) @@ -1154,13 +1155,15 @@ class ProgressIndicatorPercent: def show(self, current=None, increase=1): pct = self.progress(current, increase) if pct is not None: - return self.output(pct) + return self.output(self.msg % pct) - def output(self, percent): - self.logger.info(self.msg % percent) + def output(self, message): + self.output_len = max(len(message), self.output_len) + message = message.ljust(self.output_len) + self.logger.info(message) def finish(self): - self.logger.info(" " * len(self.msg % 100.0)) + self.output('') class ProgressIndicatorEndless: From f0a32575a58843ecff31be6a5818d74d89d54e1c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 26 Aug 2016 22:15:44 +0200 Subject: [PATCH 0182/1387] extract: --progress: no extra line for 'Calculating size' --- src/borg/archiver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f5ebd730..2b988865 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -459,10 +459,10 @@ class Archiver: filter = self.build_filter(matcher, peek_and_store_hardlink_masters, strip_components) if progress: - progress_logger = logging.getLogger(ProgressIndicatorPercent.LOGGER) - progress_logger.info('Calculating size') + pi = ProgressIndicatorPercent(msg='Extracting files %5.1f%%', step=0.1) + pi.output('Calculating size') extracted_size = sum(item.file_size(hardlink_masters) for item in archive.iter_items(filter)) - pi = ProgressIndicatorPercent(total=extracted_size, msg='Extracting files %5.1f%%', step=0.1) + pi.total = extracted_size else: pi = None From e289fb353923e91d6cb90daf4cb262915aeecb65 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 27 Aug 2016 14:01:28 +0200 Subject: [PATCH 0183/1387] recreate: fix crash if archive doesn't have chunker_params --- src/borg/archive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index c27faf67..209effc4 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1572,7 +1572,7 @@ class ArchiveRecreater: if not target: target = self.create_target_archive(target_name) # If the archives use the same chunker params, then don't rechunkify - target.recreate_rechunkify = tuple(archive.metadata.get('chunker_params')) != self.chunker_params + target.recreate_rechunkify = tuple(archive.metadata.get('chunker_params', [])) != self.chunker_params return target, resume_from def try_resume(self, archive, target_name): From e4e678790610a67ef866d569143157d80c7f6c26 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 27 Aug 2016 14:31:57 +0200 Subject: [PATCH 0184/1387] recreate: fix crash if recompress and deduplication beyond autocommit_threshold ie. it means that if all the recompressed chunks were already in the repo no data would be written, so there would be no active txn, so failure ensues. --- src/borg/archive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 209effc4..9546cb0a 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1450,7 +1450,7 @@ class ArchiveRecreater: chunk_id, size, csize = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite) new_chunks.append((chunk_id, size, csize)) self.seen_chunks.add(chunk_id) - if self.recompress: + if self.recompress and self.cache.seen_chunk(chunk_id) == 1: # This tracks how many bytes are uncommitted but compactable, since we are recompressing # existing chunks. target.recreate_uncomitted_bytes += csize From 620f505a1477a75e4e82850d37e8ae3bae5b0d12 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Mon, 25 Jul 2016 20:38:31 +0200 Subject: [PATCH 0185/1387] Reserve nonce space for AES-CTR before using it. Reusing the nonce totally breaks AES-CTR confidentiality. This code uses a reservation of nonce space and stores the next nonce available for a future reservation on the client and in the repository. Local storage is needed to protect against evil repositories that try to gain access to encrypted data by not saving nonce reservations and aborting the connection or otherwise forcing a rollback. Storage in the repository is needed to protect against another client writing to the repository after a transaction was aborted and thus not seeing the last used nonce from the manifest. With a real counter mode cipher protection for the multiple client case with an actively evil repository is not possible. But this still protects against cases where the attacker can not arbitrarily change the repository but can read everything stored and abort connections or crash the server. Fixes #22 --- src/borg/helpers.py | 11 ++ src/borg/key.py | 13 +- src/borg/nonces.py | 87 +++++++++++ src/borg/remote.py | 8 + src/borg/repository.py | 21 +++ src/borg/testsuite/archiver.py | 1 - src/borg/testsuite/helpers.py | 13 +- src/borg/testsuite/key.py | 25 +++- src/borg/testsuite/nonces.py | 242 +++++++++++++++++++++++++++++++ src/borg/testsuite/repository.py | 42 ++++++ 10 files changed, 455 insertions(+), 8 deletions(-) create mode 100644 src/borg/nonces.py create mode 100644 src/borg/testsuite/nonces.py diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 5e6b74cd..e6c805bc 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -269,6 +269,17 @@ def get_keys_dir(): return keys_dir +def get_nonces_dir(): + """Determine where to store the local nonce high watermark""" + + xdg_config = os.environ.get('XDG_CONFIG_HOME', os.path.join(get_home_dir(), '.config')) + nonces_dir = os.environ.get('BORG_NONCES_DIR', os.path.join(xdg_config, 'borg', 'key-nonces')) + if not os.path.exists(nonces_dir): + os.makedirs(nonces_dir) + os.chmod(nonces_dir, stat.S_IRWXU) + return nonces_dir + + def get_cache_dir(): """Determine where to repository keys and cache""" xdg_cache = os.environ.get('XDG_CACHE_HOME', os.path.join(get_home_dir(), '.cache')) diff --git a/src/borg/key.py b/src/borg/key.py index 11a50f80..849b99e4 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -3,7 +3,7 @@ import getpass import os import sys import textwrap -from binascii import a2b_base64, b2a_base64, hexlify +from binascii import a2b_base64, b2a_base64, hexlify, unhexlify from hashlib import sha256, pbkdf2_hmac from hmac import compare_digest @@ -23,6 +23,7 @@ from .helpers import bin_to_hex from .helpers import CompressionDecider2, CompressionSpec from .item import Key, EncryptedKey from .platform import SaveFile +from .nonces import NonceManager PREFIX = b'\0' * 8 @@ -169,6 +170,7 @@ class AESKeyBase(KeyBase): def encrypt(self, chunk): chunk = self.compress(chunk) + self.nonce_manager.ensure_reservation(num_aes_blocks(len(chunk.data))) self.enc_cipher.reset() data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(chunk.data))) hmac = hmac_sha256(self.enc_hmac_key, data) @@ -207,8 +209,9 @@ class AESKeyBase(KeyBase): if self.chunk_seed & 0x80000000: self.chunk_seed = self.chunk_seed - 0xffffffff - 1 - def init_ciphers(self, enc_iv=b''): - self.enc_cipher = AES(is_encrypt=True, key=self.enc_key, iv=enc_iv) + def init_ciphers(self, manifest_nonce=0): + self.enc_cipher = AES(is_encrypt=True, key=self.enc_key, iv=manifest_nonce.to_bytes(16, byteorder='big')) + self.nonce_manager = NonceManager(self.repository, self.enc_cipher, manifest_nonce) self.dec_cipher = AES(is_encrypt=False, key=self.enc_key) @@ -299,7 +302,7 @@ class PassphraseKey(AESKeyBase): try: key.decrypt(None, manifest_data) num_blocks = num_aes_blocks(len(manifest_data) - 41) - key.init_ciphers(PREFIX + long_to_bytes(key.extract_nonce(manifest_data) + num_blocks)) + key.init_ciphers(key.extract_nonce(manifest_data) + num_blocks) return key except IntegrityError: passphrase = Passphrase.getpass(prompt) @@ -337,7 +340,7 @@ class KeyfileKeyBase(AESKeyBase): if not key.load(target, passphrase): raise PassphraseWrong num_blocks = num_aes_blocks(len(manifest_data) - 41) - key.init_ciphers(PREFIX + long_to_bytes(key.extract_nonce(manifest_data) + num_blocks)) + key.init_ciphers(key.extract_nonce(manifest_data) + num_blocks) return key def find_key(self): diff --git a/src/borg/nonces.py b/src/borg/nonces.py new file mode 100644 index 00000000..4f929958 --- /dev/null +++ b/src/borg/nonces.py @@ -0,0 +1,87 @@ +import os +import sys +from binascii import unhexlify + +from .crypto import bytes_to_long, long_to_bytes +from .helpers import get_nonces_dir +from .helpers import bin_to_hex +from .platform import SaveFile +from .remote import InvalidRPCMethod + + +MAX_REPRESENTABLE_NONCE = 2**64 - 1 +NONCE_SPACE_RESERVATION = 2**28 # This in units of AES blocksize (16 bytes) + + +class NonceManager: + def __init__(self, repository, enc_cipher, manifest_nonce): + self.repository = repository + self.enc_cipher = enc_cipher + self.end_of_nonce_reservation = None + self.manifest_nonce = manifest_nonce + self.nonce_file = os.path.join(get_nonces_dir(), self.repository.id_str) + + def get_local_free_nonce(self): + try: + with open(self.nonce_file, 'r') as fd: + return bytes_to_long(unhexlify(fd.read())) + except FileNotFoundError: + return None + + def commit_local_nonce_reservation(self, next_unreserved, start_nonce): + if self.get_local_free_nonce() != start_nonce: + raise Exception("nonce space reservation with mismatched previous state") + with SaveFile(self.nonce_file, binary=False) as fd: + fd.write(bin_to_hex(long_to_bytes(next_unreserved))) + + def get_repo_free_nonce(self): + try: + return self.repository.get_free_nonce() + except InvalidRPCMethod as error: + # old server version, suppress further calls + sys.stderr.write("Please upgrade to borg version 1.1+ on the server for safer AES-CTR nonce handling.\n") + self.get_repo_free_nonce = lambda: None + self.commit_repo_nonce_reservation = lambda next_unreserved, start_nonce: None + return None + + def commit_repo_nonce_reservation(self, next_unreserved, start_nonce): + self.repository.commit_nonce_reservation(next_unreserved, start_nonce) + + def ensure_reservation(self, nonce_space_needed): + # Nonces may never repeat, even if a transaction aborts or the system crashes. + # Therefore a part of the nonce space is reserved before any nonce is used for encryption. + # As these reservations are commited to permanent storage before any nonce is used, this protects + # against nonce reuse in crashes and transaction aborts. In that case the reservation still + # persists and the whole reserved space is never reused. + # + # Local storage on the client is used to protect against an attacker that is able to rollback the + # state of the server or can do arbitrary modifications to the repository. + # Storage on the server is used for the multi client use case where a transaction on client A is + # aborted and later client B writes to the repository. + # + # This scheme does not protect against attacker who is able to rollback the state of the server + # or can do arbitrary modifications to the repository in the multi client usecase. + + if self.end_of_nonce_reservation: + # we already got a reservation, if nonce_space_needed still fits everything is ok + next_nonce = int.from_bytes(self.enc_cipher.iv, byteorder='big') + assert next_nonce <= self.end_of_nonce_reservation + if next_nonce + nonce_space_needed <= self.end_of_nonce_reservation: + return + + repo_free_nonce = self.get_repo_free_nonce() + local_free_nonce = self.get_local_free_nonce() + free_nonce_space = max(x for x in (repo_free_nonce, local_free_nonce, self.manifest_nonce, self.end_of_nonce_reservation) if x is not None) + reservation_end = free_nonce_space + nonce_space_needed + NONCE_SPACE_RESERVATION + assert reservation_end < MAX_REPRESENTABLE_NONCE + if self.end_of_nonce_reservation is None: + # initialization, reset the encryption cipher to the start of the reservation + self.enc_cipher.reset(None, free_nonce_space.to_bytes(16, byteorder='big')) + else: + # expand existing reservation if possible + if free_nonce_space != self.end_of_nonce_reservation: + # some other client got an interleaved reservation, skip partial space in old reservation to avoid overlap + self.enc_cipher.reset(None, free_nonce_space.to_bytes(16, byteorder='big')) + self.commit_repo_nonce_reservation(reservation_end, repo_free_nonce) + self.commit_local_nonce_reservation(reservation_end, local_free_nonce) + self.end_of_nonce_reservation = reservation_end diff --git a/src/borg/remote.py b/src/borg/remote.py index 604506cd..4632a50a 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -66,6 +66,8 @@ class RepositoryServer: # pragma: no cover 'save_key', 'load_key', 'break_lock', + 'get_free_nonce', + 'commit_nonce_reservation' ) def __init__(self, restrict_to_paths, append_only): @@ -450,6 +452,12 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. def load_key(self): return self.call('load_key') + def get_free_nonce(self): + return self.call('get_free_nonce') + + def commit_nonce_reservation(self, next_unreserved, start_nonce): + return self.call('commit_nonce_reservation', next_unreserved, start_nonce) + def break_lock(self): return self.call('break_lock') diff --git a/src/borg/repository.py b/src/borg/repository.py index 9eebd90e..6c0159a7 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -189,6 +189,27 @@ class Repository: keydata = self.config.get('repository', 'key') return keydata.encode('utf-8') # remote repo: msgpack issue #99, returning bytes + def get_free_nonce(self): + if not self.lock.got_exclusive_lock(): + raise AssertionError("bug in code, exclusive lock should exist here") + + nonce_path = os.path.join(self.path, 'nonce') + try: + with open(nonce_path, 'r') as fd: + return int.from_bytes(unhexlify(fd.read()), byteorder='big') + except FileNotFoundError: + return None + + def commit_nonce_reservation(self, next_unreserved, start_nonce): + if not self.lock.got_exclusive_lock(): + raise AssertionError("bug in code, exclusive lock should exist here") + + if self.get_free_nonce() != start_nonce: + raise Exception("nonce space reservation with mismatched previous state") + nonce_path = os.path.join(self.path, 'nonce') + with SaveFile(nonce_path, binary=False) as fd: + fd.write(bin_to_hex(next_unreserved.to_bytes(8, byteorder='big'))) + def destroy(self): """Destroy the repository at `self.path` """ diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index dfa540e0..d5d6f5a5 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1493,7 +1493,6 @@ class ArchiverTestCase(ArchiverTestCaseBase): verify_uniqueness() self.cmd('delete', self.repository_location + '::test.2') verify_uniqueness() - self.assert_equal(used, set(range(len(used)))) def test_aes_counter_uniqueness_keyfile(self): self.verify_aes_counter_uniqueness('keyfile') diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 5f5a3806..6583b4ea 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -14,7 +14,7 @@ from ..helpers import Buffer from ..helpers import partial_format, format_file_size, parse_file_size, format_timedelta, format_line, PlaceholderError from ..helpers import make_path_safe, clean_lines from ..helpers import prune_within, prune_split -from ..helpers import get_cache_dir, get_keys_dir +from ..helpers import get_cache_dir, get_keys_dir, get_nonces_dir from ..helpers import is_slow_msgpack from ..helpers import yes, TRUISH, FALSISH, DEFAULTISH from ..helpers import StableDict, int_to_bigint, bigint_to_int, bin_to_hex @@ -636,6 +636,17 @@ def test_get_keys_dir(): os.environ['BORG_KEYS_DIR'] = old_env +def test_get_nonces_dir(monkeypatch): + """test that get_nonces_dir respects environment""" + monkeypatch.delenv('XDG_CONFIG_HOME', raising=False) + monkeypatch.delenv('BORG_NONCES_DIR', raising=False) + assert get_nonces_dir() == os.path.join(os.path.expanduser('~'), '.config', 'borg', 'key-nonces') + monkeypatch.setenv('XDG_CONFIG_HOME', '/var/tmp/.config') + assert get_nonces_dir() == os.path.join('/var/tmp/.config', 'borg', 'key-nonces') + monkeypatch.setenv('BORG_NONCES_DIR', '/var/tmp') + assert get_nonces_dir() == '/var/tmp' + + def test_file_size(): """test the size formatting routines""" si_size_map = { diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index b85650a4..94b45539 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -1,6 +1,7 @@ import getpass import re import tempfile +import os.path from binascii import hexlify, unhexlify import pytest @@ -9,6 +10,7 @@ from ..crypto import bytes_to_long, num_aes_blocks from ..helpers import Location from ..helpers import Chunk from ..helpers import IntegrityError +from ..helpers import get_nonces_dir from ..key import PlaintextKey, PassphraseKey, KeyfileKey, Passphrase, PasswordRetriesExceeded, bin_to_hex @@ -18,6 +20,11 @@ def clean_env(monkeypatch): monkeypatch.delenv('BORG_PASSPHRASE', False) +@pytest.fixture(autouse=True) +def nonce_dir(tmpdir_factory, monkeypatch): + monkeypatch.setenv('XDG_CONFIG_HOME', tmpdir_factory.mktemp('xdg-config-home')) + + class TestKey: class MockArgs: location = Location(tempfile.mkstemp()[1]) @@ -59,6 +66,12 @@ class TestKey: id = bytes(32) id_str = bin_to_hex(id) + def get_free_nonce(self): + return None + + def commit_nonce_reservation(self, next_unreserved, start_nonce): + pass + def test_plaintext(self): key = PlaintextKey.create(None, None) chunk = Chunk(b'foo') @@ -77,13 +90,23 @@ class TestKey: assert key.extract_nonce(manifest2) == 1 iv = key.extract_nonce(manifest) key2 = KeyfileKey.detect(self.MockRepository(), manifest) - assert bytes_to_long(key2.enc_cipher.iv, 8) == iv + num_aes_blocks(len(manifest) - KeyfileKey.PAYLOAD_OVERHEAD) + assert bytes_to_long(key2.enc_cipher.iv, 8) >= iv + num_aes_blocks(len(manifest) - KeyfileKey.PAYLOAD_OVERHEAD) # Key data sanity check assert len({key2.id_key, key2.enc_key, key2.enc_hmac_key}) == 3 assert key2.chunk_seed != 0 chunk = Chunk(b'foo') assert chunk == key2.decrypt(key.id_hash(chunk.data), key.encrypt(chunk)) + def test_keyfile_nonce_rollback_protection(self, monkeypatch, keys_dir): + monkeypatch.setenv('BORG_PASSPHRASE', 'test') + repository = self.MockRepository() + with open(os.path.join(get_nonces_dir(), repository.id_str), "w") as fd: + fd.write("0000000000002000") + key = KeyfileKey.create(repository, self.MockArgs()) + data = key.encrypt(Chunk(b'ABC')) + assert key.extract_nonce(data) == 0x2000 + assert key.decrypt(None, data).data == b'ABC' + def test_keyfile_kfenv(self, tmpdir, monkeypatch): keyfile = tmpdir.join('keyfile') monkeypatch.setenv('BORG_KEY_FILE', str(keyfile)) diff --git a/src/borg/testsuite/nonces.py b/src/borg/testsuite/nonces.py new file mode 100644 index 00000000..88405f56 --- /dev/null +++ b/src/borg/testsuite/nonces.py @@ -0,0 +1,242 @@ +import os.path + +import pytest + +from ..helpers import get_nonces_dir +from ..key import bin_to_hex +from ..nonces import NonceManager +from ..remote import InvalidRPCMethod + +from .. import nonces # for monkey patching NONCE_SPACE_RESERVATION + + +@pytest.fixture(autouse=True) +def clean_env(monkeypatch): + # Workaround for some tests (testsuite/archiver) polluting the environment + monkeypatch.delenv('BORG_PASSPHRASE', False) + + +@pytest.fixture(autouse=True) +def nonce_dir(tmpdir_factory, monkeypatch): + monkeypatch.setenv('XDG_CONFIG_HOME', tmpdir_factory.mktemp('xdg-config-home')) + + +class TestNonceManager: + + class MockRepository: + class _Location: + orig = '/some/place' + + _location = _Location() + id = bytes(32) + id_str = bin_to_hex(id) + + def get_free_nonce(self): + return self.next_free + + def commit_nonce_reservation(self, next_unreserved, start_nonce): + assert start_nonce == self.next_free + self.next_free = next_unreserved + + class MockOldRepository(MockRepository): + def get_free_nonce(self): + raise InvalidRPCMethod("") + + def commit_nonce_reservation(self, next_unreserved, start_nonce): + pytest.fail("commit_nonce_reservation should never be called on an old repository") + + class MockEncCipher: + def __init__(self, iv): + self.iv_set = False # placeholder, this is never a valid iv + self.iv = iv + + def reset(self, key, iv): + assert key is None + assert iv is not False + self.iv_set = iv + self.iv = iv + + def expect_iv_and_advance(self, expected_iv, advance): + expected_iv = expected_iv.to_bytes(16, byteorder='big') + iv_set = self.iv_set + assert iv_set == expected_iv + self.iv_set = False + self.iv = advance.to_bytes(16, byteorder='big') + + def expect_no_reset_and_advance(self, advance): + iv_set = self.iv_set + assert iv_set is False + self.iv = advance.to_bytes(16, byteorder='big') + + def setUp(self): + self.repository = None + + def cache_nonce(self): + with open(os.path.join(get_nonces_dir(), self.repository.id_str), "r") as fd: + return fd.read() + + def set_cache_nonce(self, nonce): + with open(os.path.join(get_nonces_dir(), self.repository.id_str), "w") as fd: + assert fd.write(nonce) + + def test_empty_cache_and_old_server(self, monkeypatch): + monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) + + enc_cipher = self.MockEncCipher(0x2000) + self.repository = self.MockOldRepository() + manager = NonceManager(self.repository, enc_cipher, 0x2000) + manager.ensure_reservation(19) + enc_cipher.expect_iv_and_advance(0x2000, 0x2013) + + assert self.cache_nonce() == "0000000000002033" + + def test_empty_cache(self, monkeypatch): + monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) + + enc_cipher = self.MockEncCipher(0x2000) + self.repository = self.MockRepository() + self.repository.next_free = 0x2000 + manager = NonceManager(self.repository, enc_cipher, 0x2000) + manager.ensure_reservation(19) + enc_cipher.expect_iv_and_advance(0x2000, 0x2013) + + assert self.cache_nonce() == "0000000000002033" + + def test_empty_nonce(self, monkeypatch): + monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) + + enc_cipher = self.MockEncCipher(0x2000) + self.repository = self.MockRepository() + self.repository.next_free = None + manager = NonceManager(self.repository, enc_cipher, 0x2000) + manager.ensure_reservation(19) + enc_cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + + assert self.cache_nonce() == "0000000000002033" + assert self.repository.next_free == 0x2033 + + # enough space in reservation + manager.ensure_reservation(13) + enc_cipher.expect_no_reset_and_advance(0x2000 + 19 + 13) + assert self.cache_nonce() == "0000000000002033" + assert self.repository.next_free == 0x2033 + + # just barely enough space in reservation + manager.ensure_reservation(19) + enc_cipher.expect_no_reset_and_advance(0x2000 + 19 + 13 + 19) + assert self.cache_nonce() == "0000000000002033" + assert self.repository.next_free == 0x2033 + + # no space in reservation + manager.ensure_reservation(16) + enc_cipher.expect_no_reset_and_advance(0x2000 + 19 + 13 + 19 + 16) + assert self.cache_nonce() == "0000000000002063" + assert self.repository.next_free == 0x2063 + + # spans reservation boundary + manager.ensure_reservation(64) + enc_cipher.expect_no_reset_and_advance(0x2000 + 19 + 13 + 19 + 16 + 64) + assert self.cache_nonce() == "00000000000020c3" + assert self.repository.next_free == 0x20c3 + + def test_sync_nonce(self, monkeypatch): + monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) + + enc_cipher = self.MockEncCipher(0x2000) + self.repository = self.MockRepository() + self.repository.next_free = 0x2000 + self.set_cache_nonce("0000000000002000") + + manager = NonceManager(self.repository, enc_cipher, 0x2000) + manager.ensure_reservation(19) + enc_cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + + assert self.cache_nonce() == "0000000000002033" + assert self.repository.next_free == 0x2033 + + def test_server_just_upgraded(self, monkeypatch): + monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) + + enc_cipher = self.MockEncCipher(0x2000) + self.repository = self.MockRepository() + self.repository.next_free = None + self.set_cache_nonce("0000000000002000") + + manager = NonceManager(self.repository, enc_cipher, 0x2000) + manager.ensure_reservation(19) + enc_cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + + assert self.cache_nonce() == "0000000000002033" + assert self.repository.next_free == 0x2033 + + def test_transaction_abort_no_cache(self, monkeypatch): + monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) + + enc_cipher = self.MockEncCipher(0x1000) + self.repository = self.MockRepository() + self.repository.next_free = 0x2000 + + manager = NonceManager(self.repository, enc_cipher, 0x2000) + manager.ensure_reservation(19) + enc_cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + + assert self.cache_nonce() == "0000000000002033" + assert self.repository.next_free == 0x2033 + + def test_transaction_abort_old_server(self, monkeypatch): + monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) + + enc_cipher = self.MockEncCipher(0x1000) + self.repository = self.MockOldRepository() + self.set_cache_nonce("0000000000002000") + + manager = NonceManager(self.repository, enc_cipher, 0x2000) + manager.ensure_reservation(19) + enc_cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + + assert self.cache_nonce() == "0000000000002033" + + def test_transaction_abort_on_other_client(self, monkeypatch): + monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) + + enc_cipher = self.MockEncCipher(0x1000) + self.repository = self.MockRepository() + self.repository.next_free = 0x2000 + self.set_cache_nonce("0000000000001000") + + manager = NonceManager(self.repository, enc_cipher, 0x2000) + manager.ensure_reservation(19) + enc_cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + + assert self.cache_nonce() == "0000000000002033" + assert self.repository.next_free == 0x2033 + + def test_interleaved(self, monkeypatch): + monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) + + enc_cipher = self.MockEncCipher(0x2000) + self.repository = self.MockRepository() + self.repository.next_free = 0x2000 + self.set_cache_nonce("0000000000002000") + + manager = NonceManager(self.repository, enc_cipher, 0x2000) + manager.ensure_reservation(19) + enc_cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + + assert self.cache_nonce() == "0000000000002033" + assert self.repository.next_free == 0x2033 + + # somehow the clients unlocks, another client reserves and this client relocks + self.repository.next_free = 0x4000 + + # enough space in reservation + manager.ensure_reservation(12) + enc_cipher.expect_no_reset_and_advance(0x2000 + 19 + 12) + assert self.cache_nonce() == "0000000000002033" + assert self.repository.next_free == 0x4000 + + # spans reservation boundary + manager.ensure_reservation(21) + enc_cipher.expect_iv_and_advance(0x4000, 0x4000 + 21) + assert self.cache_nonce() == "0000000000004035" + assert self.repository.next_free == 0x4035 diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 620bcaf3..90c46f4d 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -390,6 +390,48 @@ class RepositoryFreeSpaceTestCase(RepositoryTestCaseBase): self.repository.commit() +class NonceReservation(RepositoryTestCaseBase): + def test_get_free_nonce_asserts(self): + self.reopen(exclusive=False) + with pytest.raises(AssertionError): + with self.repository: + self.repository.get_free_nonce() + + def test_get_free_nonce(self): + with self.repository: + assert self.repository.get_free_nonce() is None + + with open(os.path.join(self.repository.path, "nonce"), "w") as fd: + fd.write("0000000000000000") + assert self.repository.get_free_nonce() == 0 + + with open(os.path.join(self.repository.path, "nonce"), "w") as fd: + fd.write("5000000000000000") + assert self.repository.get_free_nonce() == 0x5000000000000000 + + def test_commit_nonce_reservation_asserts(self): + self.reopen(exclusive=False) + with pytest.raises(AssertionError): + with self.repository: + self.repository.commit_nonce_reservation(0x200, 0x100) + + def test_commit_nonce_reservation(self): + with self.repository: + with pytest.raises(Exception): + self.repository.commit_nonce_reservation(0x200, 15) + + self.repository.commit_nonce_reservation(0x200, None) + with open(os.path.join(self.repository.path, "nonce"), "r") as fd: + assert fd.read() == "0000000000000200" + + with pytest.raises(Exception): + self.repository.commit_nonce_reservation(0x200, 15) + + self.repository.commit_nonce_reservation(0x400, 0x200) + with open(os.path.join(self.repository.path, "nonce"), "r") as fd: + assert fd.read() == "0000000000000400" + + class RepositoryAuxiliaryCorruptionTestCase(RepositoryTestCaseBase): def setUp(self): super().setUp() From e0b9aede2956bc4d7e607052542817ec1d89912b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 27 Aug 2016 22:43:41 +0200 Subject: [PATCH 0186/1387] update changes (1.1.0b1) --- docs/changes.rst | 101 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 78 insertions(+), 23 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 75cca673..a8e2b640 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,62 +50,115 @@ The best check that everything is ok is to run a dry-run extraction:: borg extract -v --dry-run REPO::ARCHIVE -Version 1.1.0 (not released yet) --------------------------------- +Version 1.1.0b1 (2016-08-27) +---------------------------- New features: -- borg check: will not produce the "Checking segments" output unless - new --progress option is passed, #824. -- options that imply output (--show-rc, --show-version, --list, --stats, - --progress) don't need -v/--info to have that output displayed, #865 -- borg recreate: re-create existing archives, #787 #686 #630 #70, also see - #757, #770. +- new commands: - - selectively remove files/dirs from old archives - - re-compress data - - re-chunkify data, e.g. to have upgraded Attic / Borg 0.xx archives - deduplicate with Borg 1.x archives or to experiment with chunker-params. -- create: visit files in inode order (better speed, esp. for large directories - and rotating disks) -- borg diff: show differences between archives -- borg list improved: + - borg recreate: re-create existing archives, #787 #686 #630 #70, also see + #757, #770. + + - selectively remove files/dirs from old archives + - re-compress data + - re-chunkify data, e.g. to have upgraded Attic / Borg 0.xx archives + deduplicate with Borg 1.x archives or to experiment with chunker-params. + - borg diff: show differences between archives + - borg with-lock: execute a command with the repository locked, #990 +- borg create: + + - Flexible compression with pattern matching on path/filename, + and LZ4 heuristic for deciding compressibility, #810, #1007 + - visit files in inode order (better speed, esp. for large directories and rotating disks) + - in-file checkpoints, #1217 + - increased default checkpoint interval to 30 minutes (was 5 minutes), #896 + - added uuid archive format tag, #1151 + - save mountpoint directories with --one-file-system, makes system restore easier, #1033 + - Linux: added support for some BSD flags, #1050 + - add 'x' status for excluded paths, #814 + + - also means files excluded via UF_NODUMP, #1080 +- borg check: + + - will not produce the "Checking segments" output unless new --progress option is passed, #824. + - --verify-data to verify data cryptographically on the client, #975 +- borg list, #751, #1179 - removed {formatkeys}, see "borg list --help" - --list-format is deprecated, use --format instead + - --format now also applies to listing archives, not only archive contents, #1179 - now supports the usual [PATH [PATHS…]] syntax and excludes - new keys: csize, num_chunks, unique_chunks, NUL - supports guaranteed_available hashlib hashes - (to avoid varying functionality depending on environment) -- prune: + (to avoid varying functionality depending on environment), + which includes the SHA1 and SHA2 family as well as MD5 +- borg prune: - to better visualize the "thinning out", we now list all archives in reverse time order. rephrase and reorder help text. - implement --keep-last N via --keep-secondly N, also --keep-minutely. assuming that there is not more than 1 backup archive made in 1s, --keep-last N and --keep-secondly N are equivalent, #537 -- borg comment: add archive comments, #842 + - cleanup checkpoints except the latest, #1008 +- borg extract: + + - added --progress, #1449 + - Linux: limited support for BSD flags, #1050 +- borg info: + + - output is now more similar to borg create --stats, #977 +- repository: + + - added progress information to commit/compaction phase (often takes some time when deleting/pruning), #1519 + - automatic recovery for some forms of repository inconsistency, #858 + - check free space before going forward with a commit, #1336 + - improved write performance (esp. for rotating media), #985 + + - new IO code for Linux + - raised default segment size to approx 512 MiB + - improved compaction performance, #1041 + - reduced client CPU load and improved performance for remote repositories, #940 + +- options that imply output (--show-rc, --show-version, --list, --stats, + --progress) don't need -v/--info to have that output displayed, #865 +- add archive comments (via borg (re)create --comment), #842 - provide "borgfs" wrapper for borg mount, enables usage via fstab, #743 -- create: add 'x' status for excluded paths, #814 -- --show-version: shows/logs the borg version, #725 - borg list/prune/delete: also output archive id, #731 +- --show-version: shows/logs the borg version, #725 +- added --debug-topic for granular debug logging, #1447 +- use atomic file writing/updating for configuration and key files, #1377 +- BORG_KEY_FILE environment variable, #1001 +- self-testing module, #970 + Bug fixes: +- list: fixed default output being produced if --format is given with empty parameter, #1489 +- create: fixed overflowing progress line with CJK and similar characters, #1051 +- prune: fixed crash if --prefix resulted in no matches, #1029 - init: clean up partial repo if passphrase input is aborted, #850 - info: quote cmdline arguments that have spaces in them -- failing hashindex tests on netbsd, #804 -- fix links failing for extracting subtrees, #761 +- fix hardlinks failing in some cases for extracting subtrees, #761 Other changes: - replace stdlib hmac with OpenSSL, zero-copy decrypt (10-15% increase in performance of hash-lists and extract). +- improved chunker performance, #1021 +- open repository segment files in exclusive mode (fail-safe), #1134 +- improved error logging, #1440 - Source: - pass meta-data around, #765 - move some constants to new constants module - better readability and less errors with namedtuples, #823 + - moved source tree into src/ subdirectory, #1016 + - made borg.platform a package, #1113 + - removed dead crypto code, #1032 + - improved and ported parts of the test suite to py.test, #912 + - created data classes instead of passing dictionaries around, #981, #1158, #1161 + - cleaned up imports, #1112 - Docs: - better help texts and sphinx reproduction of usage help: @@ -116,6 +169,8 @@ Other changes: - chunker: added some insights by "Voltara", #903 - clarify what "deduplicated size" means - fix / update / add package list entries + - added a SaltStack usage example, #956 + - expanded FAQ - new contributors in AUTHORS! - Tests: From 1f04820d9d109d77ebb9997c770612662ac28dd6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 4 Jun 2016 18:26:55 +0200 Subject: [PATCH 0187/1387] fuse: implement versions view all archives, all items are read to build a unified view. files are represented by a same-name directory with the versions of the file. A filename suffix computed by adler32(chunkids) is used to disambiguate the versions. also: refactor code a little, create methods for leaves, inner nodes. --- docs/changes.rst | 6 +- docs/usage.rst | 13 ++++- src/borg/archiver.py | 2 + src/borg/fuse.py | 102 +++++++++++++++++++++++---------- src/borg/testsuite/archiver.py | 15 +++++ 5 files changed, 105 insertions(+), 33 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index a8e2b640..61e8f986 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -108,6 +108,11 @@ New features: - borg info: - output is now more similar to borg create --stats, #977 +- borg mount: + + - provide "borgfs" wrapper for borg mount, enables usage via fstab, #743 + - "versions" mount option - when used with a repository mount, this gives + a merged, versioned view of the files in all archives, #729 - repository: - added progress information to commit/compaction phase (often takes some time when deleting/pruning), #1519 @@ -123,7 +128,6 @@ New features: - options that imply output (--show-rc, --show-version, --list, --stats, --progress) don't need -v/--info to have that output displayed, #865 - add archive comments (via borg (re)create --comment), #842 -- provide "borgfs" wrapper for borg mount, enables usage via fstab, #743 - borg list/prune/delete: also output archive id, #731 - --show-version: shows/logs the borg version, #725 - added --debug-topic for granular debug logging, #1447 diff --git a/docs/usage.rst b/docs/usage.rst index e9fa7179..1c496685 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -499,8 +499,8 @@ Examples Examples ~~~~~~~~ -borg mount/borgfs -+++++++++++++++++ +borg mount +++++++++++ :: $ borg mount /path/to/repo::root-2016-02-15 /tmp/mymountpoint @@ -508,6 +508,15 @@ borg mount/borgfs bin boot etc home lib lib64 lost+found media mnt opt root sbin srv tmp usr var $ fusermount -u /tmp/mymountpoint +:: + + $ borg mount -o versions /path/to/repo /tmp/mymountpoint + $ ls -l /tmp/mymountpoint/home/user/doc.txt/ + total 24 + -rw-rw-r-- 1 user group 12357 Aug 26 21:19 doc.txt.cda00bc9 + -rw-rw-r-- 1 user group 12204 Aug 26 21:04 doc.txt.fa760f28 + $ fusermount -u /tmp/mymountpoint + borgfs ++++++ :: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 2b988865..5936cdf8 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1835,6 +1835,8 @@ class Archiver: For mount options, see the fuse(8) manual page. Additional mount options supported by borg: + - versions: when used with a repository mount, this gives a merged, versioned + view of the files in the archives. EXPERIMENTAL, layout may change in future. - allow_damaged_files: by default damaged files (where missing chunks were replaced with runs of zeros by borg check --repair) are not readable and return EIO (I/O error). Set this option to read such files. diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 4e7cf10c..c81292b6 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -6,12 +6,12 @@ import tempfile import time from collections import defaultdict from distutils.version import LooseVersion +from zlib import adler32 import llfuse import msgpack from .logger import create_logger -from .lrucache import LRUCache logger = create_logger() from .archive import Archive @@ -51,14 +51,18 @@ class ItemCache: class FuseOperations(llfuse.Operations): """Export archive as a fuse filesystem """ - + # mount options allow_damaged_files = False + versions = False def __init__(self, key, repository, manifest, archive, cached_repo): super().__init__() - self._inode_count = 0 - self.key = key + self.repository_uncached = repository self.repository = cached_repo + self.archive = archive + self.manifest = manifest + self.key = key + self._inode_count = 0 self.items = {} self.parent = {} self.contents = defaultdict(dict) @@ -69,15 +73,22 @@ class FuseOperations(llfuse.Operations): data_cache_capacity = int(os.environ.get('BORG_MOUNT_DATA_CACHE_ENTRIES', os.cpu_count() or 1)) logger.debug('mount data cache capacity: %d chunks', data_cache_capacity) self.data_cache = LRUCache(capacity=data_cache_capacity, dispose=lambda _: None) + + def _create_filesystem(self): self._create_dir(parent=1) # first call, create root dir (inode == 1) - if archive: - self.process_archive(archive) + if self.archive: + self.process_archive(self.archive) else: - for name in manifest.archives: - # Create archive placeholder inode - archive_inode = self._create_dir(parent=1) - self.contents[1][os.fsencode(name)] = archive_inode - self.pending_archives[archive_inode] = Archive(repository, key, manifest, name) + for name in self.manifest.archives: + archive = Archive(self.repository_uncached, self.key, self.manifest, name) + if self.versions: + # process archives immediately + self.process_archive(archive) + else: + # lazy load archives, create archive placeholder inode + archive_inode = self._create_dir(parent=1) + self.contents[1][os.fsencode(name)] = archive_inode + self.pending_archives[archive_inode] = archive def mount(self, mountpoint, mount_options, foreground=False): """Mount filesystem on *mountpoint* with *mount_options*.""" @@ -89,6 +100,12 @@ class FuseOperations(llfuse.Operations): self.allow_damaged_files = True except ValueError: pass + try: + options.remove('versions') + self.versions = True + except ValueError: + pass + self._create_filesystem() llfuse.init(self, mountpoint, options) if not foreground: daemonize() @@ -122,11 +139,16 @@ class FuseOperations(llfuse.Operations): unpacker.feed(data) for item in unpacker: item = Item(internal_dict=item) + is_dir = stat.S_ISDIR(item.mode) try: # This can happen if an archive was created with a command line like # $ borg create ... dir1/file dir1 # In this case the code below will have created a default_dir inode for dir1 already. - inode = self._find_inode(safe_encode(item.path), prefix) + path = safe_encode(item.path) + if not is_dir: + # not a directory -> no lookup needed + raise KeyError + inode = self._find_inode(path, prefix) except KeyError: pass else: @@ -137,25 +159,46 @@ class FuseOperations(llfuse.Operations): num_segments = len(segments) parent = 1 for i, segment in enumerate(segments, 1): - # Leaf segment? if i == num_segments: - if 'source' in item and stat.S_ISREG(item.mode): - inode = self._find_inode(item.source, prefix) - item = self.cache.get(inode) - item.nlink = item.get('nlink', 1) + 1 - self.items[inode] = item - else: - inode = self.cache.add(item) - self.parent[inode] = parent - if segment: - self.contents[parent][segment] = inode - elif segment in self.contents[parent]: - parent = self.contents[parent][segment] + self.process_leaf(segment, item, parent, prefix, is_dir) else: - inode = self._create_dir(parent) - if segment: - self.contents[parent][segment] = inode - parent = inode + parent = self.process_inner(segment, parent) + + def process_leaf(self, name, item, parent, prefix, is_dir): + def version_name(name, item): + if 'chunks' in item: + ident = 0 + for chunkid, _, _ in item.chunks: + ident = adler32(chunkid, ident) + name = name + safe_encode('.%08x' % ident) + return name + + if self.versions and not is_dir: + parent = self.process_inner(name, parent) + name = version_name(name, item) + self.process_real_leaf(name, item, parent, prefix) + + def process_real_leaf(self, name, item, parent, prefix): + if 'source' in item and stat.S_ISREG(item.mode): + inode = self._find_inode(item.source, prefix) + item = self.cache.get(inode) + item.nlink = item.get('nlink', 1) + 1 + self.items[inode] = item + else: + inode = self.cache.add(item) + self.parent[inode] = parent + if name: + self.contents[parent][name] = inode + + def process_inner(self, name, parent): + if name in self.contents[parent]: + parent = self.contents[parent][name] + else: + inode = self._create_dir(parent) + if name: + self.contents[parent][name] = inode + parent = inode + return parent def allocate_inode(self): self._inode_count += 1 @@ -280,7 +323,6 @@ class FuseOperations(llfuse.Operations): # evict fully read chunk from cache del self.data_cache[id] else: - # XXX _, data = self.key.decrypt(id, self.repository.get(id)) if offset + n < len(data): # chunk was only partially read, cache it diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index d5d6f5a5..181ccb17 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1441,6 +1441,21 @@ class ArchiverTestCase(ArchiverTestCaseBase): sto = os.stat(out_fn) assert stat.S_ISFIFO(sto.st_mode) + @unittest.skipUnless(has_llfuse, 'llfuse not installed') + def test_fuse_versions_view(self): + self.cmd('init', self.repository_location) + self.create_regular_file('test', contents=b'first') + self.cmd('create', self.repository_location + '::archive1', 'input') + self.create_regular_file('test', contents=b'second') + self.cmd('create', self.repository_location + '::archive2', 'input') + mountpoint = os.path.join(self.tmpdir, 'mountpoint') + # mount the whole repository, archive contents shall show up in versioned view: + with self.fuse_mount(self.repository_location, mountpoint, 'versions'): + path = os.path.join(mountpoint, 'input', 'test') # filename shows up as directory ... + files = os.listdir(path) + assert all(f.startswith('test.') for f in files) # ... with files test.xxxxxxxx in there + assert {b'first', b'second'} == {open(os.path.join(path, f), 'rb').read() for f in files} + @unittest.skipUnless(has_llfuse, 'llfuse not installed') def test_fuse_allow_damaged_files(self): self.cmd('init', self.repository_location) From 3f159ba18a5f07ca0259e1974cf5114f8dbab02f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 28 Aug 2016 00:03:16 +0200 Subject: [PATCH 0188/1387] update CHANGES --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 61e8f986..e0cec534 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,7 +50,7 @@ The best check that everything is ok is to run a dry-run extraction:: borg extract -v --dry-run REPO::ARCHIVE -Version 1.1.0b1 (2016-08-27) +Version 1.1.0b1 (2016-08-28) ---------------------------- New features: From 6dd29cfb1e204ceaeb80bf91d94f56693bbaeac5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 28 Aug 2016 00:16:19 +0200 Subject: [PATCH 0189/1387] ran build_api, reorder as in 1.0 --- docs/api.rst | 55 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 8dfc8cce..ab634e86 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,80 +1,95 @@ -.. highlight:: python API Documentation ================= -.. automodule:: borg.archiver +.. automodule:: src.borg.archiver :members: :undoc-members: -.. automodule:: borg.archive +.. automodule:: src.borg.archive :members: :undoc-members: -.. automodule:: borg.repository +.. automodule:: src.borg.repository :members: :undoc-members: -.. automodule:: borg.remote +.. automodule:: src.borg.remote :members: :undoc-members: -.. automodule:: borg.cache +.. automodule:: src.borg.cache :members: :undoc-members: -.. automodule:: borg.key +.. automodule:: src.borg.key :members: :undoc-members: -.. automodule:: borg.logger +.. automodule:: src.borg.nonces :members: :undoc-members: -.. automodule:: borg.helpers +.. automodule:: src.borg.item :members: :undoc-members: -.. automodule:: borg.locking +.. automodule:: src.borg.constants :members: :undoc-members: -.. automodule:: borg.shellpattern +.. automodule:: src.borg.logger :members: :undoc-members: -.. automodule:: borg.lrucache +.. automodule:: src.borg.helpers :members: :undoc-members: -.. automodule:: borg.fuse +.. automodule:: src.borg.locking :members: :undoc-members: -.. automodule:: borg.xattr +.. automodule:: src.borg.shellpattern :members: :undoc-members: -.. automodule:: borg.platform +.. automodule:: src.borg.lrucache :members: :undoc-members: -.. automodule:: borg.platform_linux +.. automodule:: src.borg.fuse :members: :undoc-members: -.. automodule:: borg.hashindex +.. automodule:: src.borg.selftest :members: :undoc-members: -.. automodule:: borg.compress +.. automodule:: src.borg.xattr + :members: + :undoc-members: + +.. automodule:: src.borg.platform.base + :members: + :undoc-members: + +.. automodule:: src.borg.platform.linux + :members: + :undoc-members: + +.. automodule:: src.borg.hashindex + :members: + :undoc-members: + +.. automodule:: src.borg.compress :members: get_compressor, Compressor, CompressorBase :undoc-members: -.. automodule:: borg.chunker +.. automodule:: src.borg.chunker :members: :undoc-members: -.. automodule:: borg.crypto +.. automodule:: src.borg.crypto :members: :undoc-members: From 83c99943c3819ab2c04deaa0b778d3fbd0828c62 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 28 Aug 2016 00:17:24 +0200 Subject: [PATCH 0190/1387] ran build_usage --- docs/usage/check.rst.inc | 4 -- docs/usage/common-options.rst.inc | 6 +- docs/usage/create.rst.inc | 5 +- docs/usage/debug-dump-repo-objs.rst.inc | 35 +++-------- docs/usage/debug-info.rst.inc | 26 ++------ docs/usage/extract.rst.inc | 2 + docs/usage/help.rst.inc | 81 +++++++++++++------------ docs/usage/info.rst.inc | 16 ++--- docs/usage/init.rst.inc | 2 + docs/usage/mount.rst.inc | 2 + docs/usage/recreate.rst.inc | 9 +++ 11 files changed, 89 insertions(+), 99 deletions(-) diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index 17d6cf49..a142df98 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -81,7 +81,3 @@ which will detect (accidental) corruption. For encrypted repositories it is tamper-resistant as well, unless the attacker has access to the keys. It is also very slow. - ---verify-data only verifies data used by the archives specified with --last, ---prefix or an explicitly named archive. If none of these are passed, -all data in the repository is verified. diff --git a/docs/usage/common-options.rst.inc b/docs/usage/common-options.rst.inc index 0a3c3c3e..29bada9d 100644 --- a/docs/usage/common-options.rst.inc +++ b/docs/usage/common-options.rst.inc @@ -10,6 +10,8 @@ | work on log level INFO ``--debug`` | enable debug output, work on log level DEBUG + ``--debug-topic TOPIC`` + | enable TOPIC debugging (can be specified multiple times). The logger path is borg.debug. if TOPIC is not fully qualified. ``--lock-wait N`` | wait for the lock, but max. N seconds (default: 1). ``--show-version`` @@ -21,4 +23,6 @@ ``--umask M`` | set umask to M (local and remote, default: 0077) ``--remote-path PATH`` - | set remote path to executable (default: "borg") \ No newline at end of file + | set remote path to executable (default: "borg") + ``--consider-part-files`` + | treat part files like normal files (e.g. to list/extract them) \ No newline at end of file diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 6e176a52..7b31c18c 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -63,7 +63,8 @@ Archive options ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm (and level): | none == no compression (default), - | auto,C[,L] == built-in heuristic decides between none or C[,L] - with C[,L] + | auto,C[,L] == built-in heuristic (try with lz4 whether the data is + | compressible) decides between none or C[,L] - with C[,L] | being any valid compression algorithm (and optional level), | lz4 == lz4, | zlib == zlib (default level 6), @@ -85,7 +86,7 @@ The archive name needs to be unique. It must not end in '.checkpoint' or checkpoints and treated in special ways. In the archive name, you may use the following format tags: -{now}, {utcnow}, {fqdn}, {hostname}, {user}, {pid}, {uuid4} +{now}, {utcnow}, {fqdn}, {hostname}, {user}, {pid}, {uuid4}, {borgversion} To speed up pulling backups over sshfs and similar network file systems which do not provide correct inode information the --ignore-inode flag can be used. This diff --git a/docs/usage/debug-dump-repo-objs.rst.inc b/docs/usage/debug-dump-repo-objs.rst.inc index 4fcd45ae..3910a126 100644 --- a/docs/usage/debug-dump-repo-objs.rst.inc +++ b/docs/usage/debug-dump-repo-objs.rst.inc @@ -6,32 +6,15 @@ borg debug-dump-repo-objs ------------------------- :: - usage: borg debug-dump-repo-objs [-h] [--critical] [--error] [--warning] - [--info] [--debug] [--lock-wait N] - [--show-rc] [--no-files-cache] [--umask M] - [--remote-path PATH] - REPOSITORY - - dump (decrypted, decompressed) repo objects - - positional arguments: - REPOSITORY repo to dump - - optional arguments: - -h, --help show this help message and exit - --critical work on log level CRITICAL - --error work on log level ERROR - --warning work on log level WARNING (default) - --info, -v, --verbose - work on log level INFO - --debug work on log level DEBUG - --lock-wait N wait for the lock, but max. N seconds (default: 1). - --show-rc show/log the return code (rc) - --no-files-cache do not load/update the file metadata cache used to - detect unchanged files - --umask M set umask to M (local and remote, default: 0077) - --remote-path PATH set remote path to executable (default: "borg") - + borg debug-dump-repo-objs REPOSITORY + +positional arguments + REPOSITORY + repo to dump + +`Common options`_ + | + Description ~~~~~~~~~~~ diff --git a/docs/usage/debug-info.rst.inc b/docs/usage/debug-info.rst.inc index 2f4f7237..4812aaf4 100644 --- a/docs/usage/debug-info.rst.inc +++ b/docs/usage/debug-info.rst.inc @@ -6,27 +6,11 @@ borg debug-info --------------- :: - usage: borg debug-info [-h] [--critical] [--error] [--warning] [--info] - [--debug] [--lock-wait N] [--show-rc] - [--no-files-cache] [--umask M] [--remote-path PATH] - - display system information for debugging / bug reports - - optional arguments: - -h, --help show this help message and exit - --critical work on log level CRITICAL - --error work on log level ERROR - --warning work on log level WARNING (default) - --info, -v, --verbose - work on log level INFO - --debug work on log level DEBUG - --lock-wait N wait for the lock, but max. N seconds (default: 1). - --show-rc show/log the return code (rc) - --no-files-cache do not load/update the file metadata cache used to - detect unchanged files - --umask M set umask to M (local and remote, default: 0077) - --remote-path PATH set remote path to executable (default: "borg") - + borg debug-info + +`Common options`_ + | + Description ~~~~~~~~~~~ diff --git a/docs/usage/extract.rst.inc b/docs/usage/extract.rst.inc index c68eaa76..682eaa3a 100644 --- a/docs/usage/extract.rst.inc +++ b/docs/usage/extract.rst.inc @@ -15,6 +15,8 @@ positional arguments paths to extract; patterns are supported optional arguments + ``-p``, ``--progress`` + | show progress while extracting (may be slower) ``--list`` | output verbose list of items (files, dirs, ...) ``-n``, ``--dry-run`` diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index 4d7c776a..b079dd2d 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -1,43 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. _borg_placeholders: - -borg help placeholders -~~~~~~~~~~~~~~~~~~~~~~ - - -Repository (or Archive) URLs and --prefix values support these placeholders: - -{hostname} - - The (short) hostname of the machine. - -{fqdn} - - The full name of the machine. - -{now} - - The current local date and time. - -{utcnow} - - The current UTC date and time. - -{user} - - The user name (or UID, if no name is available) of the user running borg. - -{pid} - - The current process ID. - -Examples:: - - borg create /path/to/repo::{hostname}-{user}-{utcnow} ... - borg create /path/to/repo::{hostname}-{now:%Y-%m-%d_%H:%M:%S} ... - borg prune --prefix '{hostname}-' ... - .. _borg_patterns: borg help patterns @@ -131,3 +93,46 @@ Examples:: EOF $ borg create --exclude-from exclude.txt backup / +.. _borg_placeholders: + +borg help placeholders +~~~~~~~~~~~~~~~~~~~~~~ + + + Repository (or Archive) URLs, --prefix and --remote-path values support these + placeholders: + + {hostname} + + The (short) hostname of the machine. + + {fqdn} + + The full name of the machine. + + {now} + + The current local date and time. + + {utcnow} + + The current UTC date and time. + + {user} + + The user name (or UID, if no name is available) of the user running borg. + + {pid} + + The current process ID. + + {borgversion} + + The version of borg. + +Examples:: + + borg create /path/to/repo::{hostname}-{user}-{utcnow} ... + borg create /path/to/repo::{hostname}-{now:%Y-%m-%d_%H:%M:%S} ... + borg prune --prefix '{hostname}-' ... + diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index e9e5f893..e4a27e7b 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -6,11 +6,11 @@ borg info --------- :: - borg info ARCHIVE + borg info REPOSITORY_OR_ARCHIVE positional arguments - ARCHIVE - archive to display information about + REPOSITORY_OR_ARCHIVE + archive or repository to display information about `Common options`_ | @@ -18,8 +18,10 @@ positional arguments Description ~~~~~~~~~~~ -This command displays some detailed information about the specified archive. +This command displays detailed information about the specified archive or repository. -The "This archive" line refers exclusively to this archive: -"Deduplicated size" is the size of the unique chunks stored only for this -archive. Non-unique / common chunks show up under "All archives". +The "This archive" line refers exclusively to the given archive: +"Deduplicated size" is the size of the unique chunks stored only for the +given archive. + +The "All archives" line shows global statistics (all chunks). diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index b2c84131..7b989497 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -15,6 +15,8 @@ positional arguments optional arguments ``-e``, ``--encryption`` | select encryption key mode (default: "repokey") + ``-a``, ``--append-only`` + | create an append-only mode repository `Common options`_ | diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index cac84a43..a9f3668e 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -41,6 +41,8 @@ To allow a regular user to use fstab entries, add the ``user`` option: For mount options, see the fuse(8) manual page. Additional mount options supported by borg: +- versions: when used with a repository mount, this gives a merged, versioned + view of the files in the archives. EXPERIMENTAL, layout may change in future. - allow_damaged_files: by default damaged files (where missing chunks were replaced with runs of zeros by borg check --repair) are not readable and return EIO (I/O error). Set this option to read such files. diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index f4134b0a..56a6e07f 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -42,6 +42,8 @@ Exclusion options | keep tag files of excluded caches/directories Archive options + ``--target TARGET`` + | create a new archive with the name ARCHIVE, do not replace existing archive (only applies for a single archive) ``--comment COMMENT`` | add a comment text to the archive ``--timestamp yyyy-mm-ddThh:mm:ss`` @@ -56,6 +58,8 @@ Archive options | zlib,0 .. zlib,9 == zlib (with level 0..9), | lzma == lzma (default level 6), | lzma,0 .. lzma,9 == lzma (with level 0..9). + ``--always-recompress`` + | always recompress chunks, don't skip chunks already compressed with the samealgorithm. ``--compression-from COMPRESSIONCONFIG`` | read compression patterns from COMPRESSIONCONFIG, one per line ``--chunker-params CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE`` @@ -70,6 +74,9 @@ Recreate the contents of existing archives. as in "borg create". If PATHs are specified the resulting archive will only contain files from these PATHs. +Note that all paths in an archive are relative, therefore absolute patterns/paths +will *not* match (--exclude, --exclude-from, --compression-from, PATHs). + --compression: all chunks seen will be stored using the given method. Due to how Borg stores compressed size information this might display incorrect information for archives that were not recreated at the same time. @@ -98,6 +105,8 @@ The archive being recreated is only removed after the operation completes. The archive that is built during the operation exists at the same time at ".recreate". The new archive will have a different archive ID. +With --target the original archive is not replaced, instead a new archive is created. + When rechunking space usage can be substantial, expect at least the entire deduplicated size of the archives using the previous chunker params. When recompressing approximately 1 % of the repository size or 512 MB From 8b5372b54cabec94c6e09e837c7e1a8b02e28aa3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 1 Sep 2016 04:30:55 +0200 Subject: [PATCH 0191/1387] fuse: slightly refactor shadowing detection if it is not a directory, the old code was a NOP, so we can just check that first. --- src/borg/fuse.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index c81292b6..50bbaaff 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -140,20 +140,18 @@ class FuseOperations(llfuse.Operations): for item in unpacker: item = Item(internal_dict=item) is_dir = stat.S_ISDIR(item.mode) - try: - # This can happen if an archive was created with a command line like - # $ borg create ... dir1/file dir1 - # In this case the code below will have created a default_dir inode for dir1 already. - path = safe_encode(item.path) - if not is_dir: - # not a directory -> no lookup needed - raise KeyError - inode = self._find_inode(path, prefix) - except KeyError: - pass - else: - self.items[inode] = item - continue + if is_dir: + try: + # This can happen if an archive was created with a command line like + # $ borg create ... dir1/file dir1 + # In this case the code below will have created a default_dir inode for dir1 already. + path = safe_encode(item.path) + inode = self._find_inode(path, prefix) + except KeyError: + pass + else: + self.items[inode] = item + continue segments = prefix + os.fsencode(os.path.normpath(item.path)).split(b'/') del item.path num_segments = len(segments) From 34eb4754639c05d58a6ade8b75986d2dd6eaa495 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 1 Sep 2016 04:33:42 +0200 Subject: [PATCH 0192/1387] fuse: remove unneeded safe_encode fsencode will happen in _find_inode() --- src/borg/fuse.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 50bbaaff..bb062b1c 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -145,8 +145,7 @@ class FuseOperations(llfuse.Operations): # This can happen if an archive was created with a command line like # $ borg create ... dir1/file dir1 # In this case the code below will have created a default_dir inode for dir1 already. - path = safe_encode(item.path) - inode = self._find_inode(path, prefix) + inode = self._find_inode(item.path, prefix) except KeyError: pass else: From d64ccff1c46a1848a18239d4728d4af5eb81aa0f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 1 Sep 2016 04:57:11 +0200 Subject: [PATCH 0193/1387] fuse: remove unneeded function definition code only had 1 caller, so just inline it. --- src/borg/fuse.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index bb062b1c..255a64f1 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -173,9 +173,7 @@ class FuseOperations(llfuse.Operations): if self.versions and not is_dir: parent = self.process_inner(name, parent) name = version_name(name, item) - self.process_real_leaf(name, item, parent, prefix) - def process_real_leaf(self, name, item, parent, prefix): if 'source' in item and stat.S_ISREG(item.mode): inode = self._find_inode(item.source, prefix) item = self.cache.get(inode) From 6f50cc34132d5d18d065db966668b4fcb8fce142 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 1 Sep 2016 05:18:10 +0200 Subject: [PATCH 0194/1387] fuse: simplify path segments loop --- src/borg/fuse.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 255a64f1..5221bd3c 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -153,13 +153,10 @@ class FuseOperations(llfuse.Operations): continue segments = prefix + os.fsencode(os.path.normpath(item.path)).split(b'/') del item.path - num_segments = len(segments) parent = 1 - for i, segment in enumerate(segments, 1): - if i == num_segments: - self.process_leaf(segment, item, parent, prefix, is_dir) - else: - parent = self.process_inner(segment, parent) + for segment in segments[:-1]: + parent = self.process_inner(segment, parent) + self.process_leaf(segments[-1], item, parent, prefix, is_dir) def process_leaf(self, name, item, parent, prefix, is_dir): def version_name(name, item): From dd89181062694df46e7ed1884f9f30af4e9a5916 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 1 Sep 2016 05:44:38 +0200 Subject: [PATCH 0195/1387] fuse: refactor / optimize process_inner --- src/borg/fuse.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 5221bd3c..47b8568b 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -182,15 +182,15 @@ class FuseOperations(llfuse.Operations): if name: self.contents[parent][name] = inode - def process_inner(self, name, parent): - if name in self.contents[parent]: - parent = self.contents[parent][name] + def process_inner(self, name, parent_inode): + dir = self.contents[parent_inode] + if name in dir: + inode = dir[name] else: - inode = self._create_dir(parent) + inode = self._create_dir(parent_inode) if name: - self.contents[parent][name] = inode - parent = inode - return parent + dir[name] = inode + return inode def allocate_inode(self): self._inode_count += 1 From 2e1cf17dd5ba5a99185a7a9285bb90a29bb3523f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 3 Sep 2016 18:41:27 +0200 Subject: [PATCH 0196/1387] add release signing key / security contact to README, fixes #1560 --- README.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.rst b/README.rst index f6132773..9b5451c6 100644 --- a/README.rst +++ b/README.rst @@ -114,6 +114,22 @@ Now doing another backup, just to show off the great deduplication: For a graphical frontend refer to our complementary project `BorgWeb `_. +Checking Release Authenticity and Security Contact +================================================== + +`Releases `_ are signed with this GPG key, +please use GPG to verify their authenticity. + +In case you discover a security issue, please use this contact for reporting it privately +and please, if possible, use encrypted E-Mail: + +Thomas Waldmann + +GPG Key Fingerprint: 6D5B EF9A DD20 7580 5747 B70F 9F88 FB52 FAF7 B393 + +The public key can be fetched from any GPG keyserver, but be careful: you must +use the **full fingerprint** to check that you got the correct key. + Links ===== From 2c5b8d690bced58f2a13d7c71e3e15711716f75d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 3 Sep 2016 19:05:07 +0200 Subject: [PATCH 0197/1387] improve borg info --help, explain size infos, fixes #1532 --- borg/archiver.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/borg/archiver.py b/borg/archiver.py index 43ec093a..7605fcd9 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1344,6 +1344,15 @@ class Archiver: info_epilog = textwrap.dedent(""" This command displays some detailed information about the specified archive. + + Please note that the deduplicated sizes of the individual archives do not add + up to the deduplicated size of the repository ("all archives"), because the two + are meaning different things: + + This archive / deduplicated size = amount of data stored ONLY for this archive + = unique chunks of this archive. + All archives / deduplicated size = amount of data stored in the repo + = all chunks in the repository. """) subparser = subparsers.add_parser('info', parents=[common_parser], description=self.do_info.__doc__, From f70008238a26905fd5cb72ba62c075414fb44fa7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 3 Sep 2016 19:22:39 +0200 Subject: [PATCH 0198/1387] link reference docs and faq about BORG_FILES_CACHE_TTL, fixes #1561 --- docs/faq.rst | 2 ++ docs/usage.rst | 1 + 2 files changed, 3 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index c772f5fa..0806c483 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -356,6 +356,8 @@ those files are reported as being added when, really, chunks are already used. +.. _always_chunking: + It always chunks all my files, even unchanged ones! --------------------------------------------------- diff --git a/docs/usage.rst b/docs/usage.rst index ab92c1cb..89a9e3cc 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -89,6 +89,7 @@ General: BORG_FILES_CACHE_TTL When set to a numeric value, this determines the maximum "time to live" for the files cache entries (default: 20). The files cache is used to quickly determine whether a file is unchanged. + The FAQ explains this more detailled in: :ref:`always_chunking` TMPDIR where temporary files are stored (might need a lot of temporary space for some operations) From 45d72722af253f1a9501895cbcde8c4862966324 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 3 Sep 2016 21:11:47 +0200 Subject: [PATCH 0199/1387] add bestpractices badge --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9b5451c6..f9db78a9 100644 --- a/README.rst +++ b/README.rst @@ -185,7 +185,7 @@ THIS IS SOFTWARE IN DEVELOPMENT, DECIDE YOURSELF WHETHER IT FITS YOUR NEEDS. Borg is distributed under a 3-clause BSD license, see `License`_ for the complete license. -|doc| |build| |coverage| +|doc| |build| |coverage| |bestpractices| .. |doc| image:: https://readthedocs.org/projects/borgbackup/badge/?version=stable :alt: Documentation @@ -202,3 +202,7 @@ Borg is distributed under a 3-clause BSD license, see `License`_ for the complet .. |screencast| image:: https://asciinema.org/a/28691.png :alt: BorgBackup Installation and Basic Usage :target: https://asciinema.org/a/28691?autoplay=1&speed=2 + +.. |bestpractices| image:: https://bestpractices.coreinfrastructure.org/projects/271/badge + :alt: Best Practices Score + :target: https://bestpractices.coreinfrastructure.org/projects/271 From ac8d65cc47adc8ca3065b0a6d3c2b19e09efda79 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 6 Sep 2016 13:03:59 +0200 Subject: [PATCH 0200/1387] Fix second block in "Easy to use" section not showing on GitHub Fixes #1576 --- README.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/README.rst b/README.rst index f9db78a9..57af3957 100644 --- a/README.rst +++ b/README.rst @@ -92,7 +92,6 @@ Initialize a new backup repository and create a backup archive:: Now doing another backup, just to show off the great deduplication: .. code-block:: none - :emphasize-lines: 11 $ borg create -v --stats /path/to/repo::Saturday2 ~/Documents ----------------------------------------------------------------------------- From 9fe0140d94dcb7bc65f02cb400ea6a294b0a2ac7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 7 Sep 2016 16:08:07 +0200 Subject: [PATCH 0201/1387] hashindex: export max load factor to Python-space --- borg/hashindex.pyx | 3 +++ borg/testsuite/hashindex.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/borg/hashindex.pyx b/borg/hashindex.pyx index 59741ad6..ce1dac04 100644 --- a/borg/hashindex.pyx +++ b/borg/hashindex.pyx @@ -23,6 +23,8 @@ cdef extern from "_hashindex.c": uint32_t _htole32(uint32_t v) uint32_t _le32toh(uint32_t v) + double HASH_MAX_LOAD + cdef _NoDefault = object() @@ -54,6 +56,7 @@ cdef class IndexBase: cdef HashIndex *index cdef int key_size + MAX_LOAD_FACTOR = HASH_MAX_LOAD def __cinit__(self, capacity=0, path=None, key_size=32): self.key_size = key_size if path: diff --git a/borg/testsuite/hashindex.py b/borg/testsuite/hashindex.py index 75cd8022..4a6bd443 100644 --- a/borg/testsuite/hashindex.py +++ b/borg/testsuite/hashindex.py @@ -276,3 +276,8 @@ def test_nsindex_segment_limit(): assert H(1) not in idx idx[H(2)] = hashindex.MAX_VALUE, 0 assert H(2) in idx + + +def test_max_load_factor(): + assert NSIndex.MAX_LOAD_FACTOR < 1 + assert ChunkIndex.MAX_LOAD_FACTOR < 1 From 197552526ff52c2a0473c6a000e34597c8a90ac3 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 7 Sep 2016 16:08:35 +0200 Subject: [PATCH 0202/1387] hashindex: make MAX_VALUE a class constant --- borg/hashindex.pyx | 7 ++++--- borg/testsuite/hashindex.py | 38 ++++++++++++++++++------------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/borg/hashindex.pyx b/borg/hashindex.pyx index ce1dac04..c32c4dd1 100644 --- a/borg/hashindex.pyx +++ b/borg/hashindex.pyx @@ -47,7 +47,6 @@ assert UINT32_MAX == 2**32-1 # module-level constant because cdef's in classes can't have default values cdef uint32_t _MAX_VALUE = 2**32-1025 -MAX_VALUE = _MAX_VALUE assert _MAX_VALUE % 2 == 1 @@ -57,6 +56,8 @@ cdef class IndexBase: cdef int key_size MAX_LOAD_FACTOR = HASH_MAX_LOAD + MAX_VALUE = _MAX_VALUE + def __cinit__(self, capacity=0, path=None, key_size=32): self.key_size = key_size if path: @@ -283,7 +284,7 @@ cdef class ChunkIndex(IndexBase): unique_chunks += 1 values = (key + self.key_size) refcount = _le32toh(values[0]) - assert refcount <= MAX_VALUE, "invalid reference count" + assert refcount <= _MAX_VALUE, "invalid reference count" chunks += refcount unique_size += _le32toh(values[1]) unique_csize += _le32toh(values[2]) @@ -343,5 +344,5 @@ cdef class ChunkKeyIterator: raise StopIteration cdef uint32_t *value = (self.key + self.key_size) cdef uint32_t refcount = _le32toh(value[0]) - assert refcount <= MAX_VALUE, "invalid reference count" + assert refcount <= _MAX_VALUE, "invalid reference count" return (self.key)[:self.key_size], (refcount, _le32toh(value[1]), _le32toh(value[2])) diff --git a/borg/testsuite/hashindex.py b/borg/testsuite/hashindex.py index 4a6bd443..b81cbf47 100644 --- a/borg/testsuite/hashindex.py +++ b/borg/testsuite/hashindex.py @@ -124,16 +124,16 @@ class HashIndexTestCase(BaseTestCase): class HashIndexRefcountingTestCase(BaseTestCase): def test_chunkindex_limit(self): idx = ChunkIndex() - idx[H(1)] = hashindex.MAX_VALUE - 1, 1, 2 + idx[H(1)] = ChunkIndex.MAX_VALUE - 1, 1, 2 # 5 is arbitray, any number of incref/decrefs shouldn't move it once it's limited for i in range(5): # first incref to move it to the limit refcount, *_ = idx.incref(H(1)) - assert refcount == hashindex.MAX_VALUE + assert refcount == ChunkIndex.MAX_VALUE for i in range(5): refcount, *_ = idx.decref(H(1)) - assert refcount == hashindex.MAX_VALUE + assert refcount == ChunkIndex.MAX_VALUE def _merge(self, refcounta, refcountb): def merge(refcount1, refcount2): @@ -152,23 +152,23 @@ class HashIndexRefcountingTestCase(BaseTestCase): def test_chunkindex_merge_limit1(self): # Check that it does *not* limit at MAX_VALUE - 1 # (MAX_VALUE is odd) - half = hashindex.MAX_VALUE // 2 - assert self._merge(half, half) == hashindex.MAX_VALUE - 1 + half = ChunkIndex.MAX_VALUE // 2 + assert self._merge(half, half) == ChunkIndex.MAX_VALUE - 1 def test_chunkindex_merge_limit2(self): # 3000000000 + 2000000000 > MAX_VALUE - assert self._merge(3000000000, 2000000000) == hashindex.MAX_VALUE + assert self._merge(3000000000, 2000000000) == ChunkIndex.MAX_VALUE def test_chunkindex_merge_limit3(self): # Crossover point: both addition and limit semantics will yield the same result - half = hashindex.MAX_VALUE // 2 - assert self._merge(half + 1, half) == hashindex.MAX_VALUE + half = ChunkIndex.MAX_VALUE // 2 + assert self._merge(half + 1, half) == ChunkIndex.MAX_VALUE def test_chunkindex_merge_limit4(self): # Beyond crossover, result of addition would be 2**31 - half = hashindex.MAX_VALUE // 2 - assert self._merge(half + 2, half) == hashindex.MAX_VALUE - assert self._merge(half + 1, half + 1) == hashindex.MAX_VALUE + half = ChunkIndex.MAX_VALUE // 2 + assert self._merge(half + 2, half) == ChunkIndex.MAX_VALUE + assert self._merge(half + 1, half + 1) == ChunkIndex.MAX_VALUE def test_chunkindex_add(self): idx1 = ChunkIndex() @@ -179,17 +179,17 @@ class HashIndexRefcountingTestCase(BaseTestCase): def test_incref_limit(self): idx1 = ChunkIndex() - idx1[H(1)] = (hashindex.MAX_VALUE, 6, 7) + idx1[H(1)] = (ChunkIndex.MAX_VALUE, 6, 7) idx1.incref(H(1)) refcount, *_ = idx1[H(1)] - assert refcount == hashindex.MAX_VALUE + assert refcount == ChunkIndex.MAX_VALUE def test_decref_limit(self): idx1 = ChunkIndex() - idx1[H(1)] = hashindex.MAX_VALUE, 6, 7 + idx1[H(1)] = ChunkIndex.MAX_VALUE, 6, 7 idx1.decref(H(1)) refcount, *_ = idx1[H(1)] - assert refcount == hashindex.MAX_VALUE + assert refcount == ChunkIndex.MAX_VALUE def test_decref_zero(self): idx1 = ChunkIndex() @@ -209,7 +209,7 @@ class HashIndexRefcountingTestCase(BaseTestCase): def test_setitem_raises(self): idx1 = ChunkIndex() with pytest.raises(AssertionError): - idx1[H(1)] = hashindex.MAX_VALUE + 1, 0, 0 + idx1[H(1)] = ChunkIndex.MAX_VALUE + 1, 0, 0 def test_keyerror(self): idx = ChunkIndex() @@ -266,15 +266,15 @@ class HashIndexDataTestCase(BaseTestCase): idx2 = ChunkIndex() idx2[H(3)] = 2**32 - 123456, 6, 7 idx1.merge(idx2) - assert idx1[H(3)] == (hashindex.MAX_VALUE, 0, 0) + assert idx1[H(3)] == (ChunkIndex.MAX_VALUE, 0, 0) def test_nsindex_segment_limit(): idx = NSIndex() with pytest.raises(AssertionError): - idx[H(1)] = hashindex.MAX_VALUE + 1, 0 + idx[H(1)] = NSIndex.MAX_VALUE + 1, 0 assert H(1) not in idx - idx[H(2)] = hashindex.MAX_VALUE, 0 + idx[H(2)] = NSIndex.MAX_VALUE, 0 assert H(2) in idx From 4cb3355d9051d42046fab88bfa4b2e570e1cefc3 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 8 Sep 2016 16:39:44 +0200 Subject: [PATCH 0203/1387] create --read-special fix crash on broken symlink also correctly processes broken symlinks. before this regressed to a crash (5b45385) a broken symlink would've been skipped. --- borg/archiver.py | 9 +++++++-- borg/testsuite/archiver.py | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 7605fcd9..785a7b8d 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -306,8 +306,13 @@ class Archiver: if not read_special: status = archive.process_symlink(path, st) else: - st_target = os.stat(path) - if is_special(st_target.st_mode): + try: + st_target = os.stat(path) + except OSError: + special = False + else: + special = is_special(st_target.st_mode) + if special: status = archive.process_file(path, st_target, cache) else: status = archive.process_symlink(path, st) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index f563ea42..7b219359 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -901,6 +901,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): output = self.cmd('create', '-v', '--list', '--filter=AM', self.repository_location + '::test3', 'input') self.assert_in('file1', output) + def test_create_read_special_broken_symlink(self): + os.symlink('somewhere doesnt exist', os.path.join(self.input_path, 'link')) + self.cmd('init', self.repository_location) + archive = self.repository_location + '::test' + self.cmd('create', '--read-special', archive, 'input') + output = self.cmd('list', archive) + assert 'input/link -> somewhere doesnt exist' in output + # def test_cmdline_compatibility(self): # self.create_regular_file('file1', size=1024 * 80) # self.cmd('init', self.repository_location) From f1cf7bc322281e6511adbe30cbe36799135cc3b2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 8 Sep 2016 16:43:48 +0200 Subject: [PATCH 0204/1387] process_symlink: fix missing backup_io() Fixes a chmod/chown/chgrp/unlink/rename/... crash race between getting dirents and dispatching to process_symlink. --- borg/archive.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/borg/archive.py b/borg/archive.py index a3a13317..dfe87016 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -610,7 +610,8 @@ Number of files: {0.stats.nfiles}'''.format( return 'b' # block device def process_symlink(self, path, st): - source = os.readlink(path) + with backup_io(): + source = os.readlink(path) item = {b'path': make_path_safe(path), b'source': source} item.update(self.stat_attrs(st, path)) self.add_item(item) From b2e389e0a044fa03039fb651d5a22d58f37dc60c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 3 Sep 2016 21:05:16 +0200 Subject: [PATCH 0205/1387] docs: add contribution guidelines --- docs/development.rst | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/development.rst b/docs/development.rst index 480a1706..3e89e34c 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -10,6 +10,46 @@ This chapter will get you started with |project_name| development. |project_name| is written in Python (with a little bit of Cython and C for the performance critical parts). +Contributions +------------- + +... are welcome! + +Some guidance for contributors: + +- discuss about changes on github issue tracker, IRC or mailing list + +- choose the branch you base your changesets on wisely: + + - choose x.y-maint for stuff that should go into next x.y release + (it usually gets merged into master branch later also) + - choose master if that does not apply + +- do clean changesets: + + - focus on some topic, resist changing anything else. + - do not do style changes mixed with functional changes. + - try to avoid refactorings mixed with functional changes. + - if you need to fix something after commit/push: + + - if there are ongoing reviews: do a fixup commit you can + merge into the bad commit later. + - if there are no ongoing reviews or you did not push the + bad commit yet: edit the commit to include your fix or + merge the fixup commit before pushing. + - have a nice, clear, typo-free commit comment + - if you fixed an issue, refer to it in your commit comment + - follow the style guide (see below) + +- if you write new code, please add tests and docs for it + +- run the tests, fix anything that comes up + +- make a pull request on github + +- wait for review by other developers + + Style guide ----------- From be3616b6b391ae16709260c0b199f20be2330ef7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Sep 2016 16:11:06 +0200 Subject: [PATCH 0206/1387] ArchiveChecker: use MAX_LOAD_FACTOR constant --- borg/archive.py | 5 +++-- borg/testsuite/hashindex.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index a3a13317..e6dd3955 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -853,8 +853,9 @@ class ArchiveChecker: """Fetch a list of all object keys from repository """ # Explicitly set the initial hash table capacity to avoid performance issues - # due to hash table "resonance" - capacity = int(len(self.repository) * 1.35 + 1) # > len * 1.0 / HASH_MAX_LOAD (see _hashindex.c) + # due to hash table "resonance". + # Since reconstruction of archive items can add some new chunks, add 10 % headroom + capacity = int(len(self.repository) / ChunkIndex.MAX_LOAD_FACTOR * 1.1) self.chunks = ChunkIndex(capacity) marker = None while True: diff --git a/borg/testsuite/hashindex.py b/borg/testsuite/hashindex.py index b81cbf47..629ae4e5 100644 --- a/borg/testsuite/hashindex.py +++ b/borg/testsuite/hashindex.py @@ -279,5 +279,5 @@ def test_nsindex_segment_limit(): def test_max_load_factor(): - assert NSIndex.MAX_LOAD_FACTOR < 1 - assert ChunkIndex.MAX_LOAD_FACTOR < 1 + assert NSIndex.MAX_LOAD_FACTOR < 1.0 + assert ChunkIndex.MAX_LOAD_FACTOR < 1.0 From c8f4e9e34ca20ff3b0688f843913bf930e1fc9d7 Mon Sep 17 00:00:00 2001 From: Julian Andres Klode Date: Tue, 13 Sep 2016 21:28:16 +0200 Subject: [PATCH 0207/1387] Correctly exit with proper unlock on SIGHUP, fixes #1593 If the connections hangs up, the borg server needs to clean up, especially unlock the repository, so a later try will work again. This is especially problematic with systemd systems that have KillUserProcesses enabled (which is the default): Logind sends a SIGHUP message to the session scope when the session ends. --- borg/archiver.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/borg/archiver.py b/borg/archiver.py index 785a7b8d..bb8e33f7 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1666,6 +1666,14 @@ def sig_term_handler(signum, stack): raise SIGTERMReceived +class SIGHUPReceived(BaseException): + pass + + +def sig_hup_handler(signum, stack): + raise SIGHUPReceived + + def setup_signal_handlers(): # pragma: no cover sigs = [] if hasattr(signal, 'SIGUSR1'): @@ -1674,7 +1682,12 @@ def setup_signal_handlers(): # pragma: no cover sigs.append(signal.SIGINFO) # kill -INFO pid (or ctrl-t) for sig in sigs: signal.signal(sig, sig_info_handler) + # If we received SIGTERM or SIGHUP, catch them and raise a proper exception + # that can be handled for an orderly exit. SIGHUP is important especially + # for systemd systems, where logind sends it when a session exits, in + # addition to any traditional use. signal.signal(signal.SIGTERM, sig_term_handler) + signal.signal(signal.SIGHUP, sig_hup_handler) def main(): # pragma: no cover @@ -1713,6 +1726,9 @@ def main(): # pragma: no cover except SIGTERMReceived: msg = 'Received SIGTERM.' exit_code = EXIT_ERROR + except SIGHUPReceived: + msg = 'Received SIGHUP.' + exit_code = EXIT_ERROR if msg: logger.error(msg) if args.show_rc: From 3c3502a9a05bd7996f5218d7de2e21ecc99601c3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 14 Sep 2016 02:22:46 +0200 Subject: [PATCH 0208/1387] update wheezy vagrant box to 7.11 7.9 is not available any more. --- Vagrantfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index f2e0945f..8316ec2f 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -385,7 +385,7 @@ Vagrant.configure(2) do |config| end config.vm.define "wheezy32" do |b| - b.vm.box = "boxcutter/debian79-i386" + b.vm.box = "boxcutter/debian711-i386" b.vm.provision "packages prepare wheezy", :type => :shell, :inline => packages_prepare_wheezy b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("wheezy32") @@ -398,7 +398,7 @@ Vagrant.configure(2) do |config| end config.vm.define "wheezy64" do |b| - b.vm.box = "boxcutter/debian79" + b.vm.box = "boxcutter/debian711" b.vm.provision "packages prepare wheezy", :type => :shell, :inline => packages_prepare_wheezy b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("wheezy64") From 57a3adb6b37781432322b98ea59e0166296e1dce Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 14 Sep 2016 02:55:13 +0200 Subject: [PATCH 0209/1387] borg recreate: also catch SIGHUP --- src/borg/archiver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f8d9ea3a..a63513de 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -989,7 +989,8 @@ class Archiver: dry_run=args.dry_run) with signal_handler(signal.SIGTERM, interrupt), \ - signal_handler(signal.SIGINT, interrupt): + signal_handler(signal.SIGINT, interrupt), \ + signal_handler(signal.SIGHUP, interrupt): if args.location.archive: name = args.location.archive if recreater.is_temporary_archive(name): From 2aa06533a390b7d7afe83eb460005030035b48f1 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 14 Sep 2016 02:59:52 +0200 Subject: [PATCH 0210/1387] fixup: typo in development.rst --- docs/development.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development.rst b/docs/development.rst index ebd6f8d0..63bc82bd 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -53,7 +53,7 @@ Code and issues --------------- Code is stored on Github, in the `Borgbackup organization -https://github.com/borgbackup/borg/>`_. `Issues +`_. `Issues `_ and `pull requests `_ should be sent there as well. See also the :ref:`support` section for more details. From 322d2176770bbf06d0854cf6053dfb5ac59cf02e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 14 Sep 2016 03:11:11 +0200 Subject: [PATCH 0211/1387] Vagrantfile: use TW's fresh-bootloader branch ... until pyinstaller team catches up, merges the fixes and recompiles the bootloader. --- Vagrantfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index c489e707..63321873 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -234,10 +234,10 @@ def install_pyinstaller(boxname) . ~/.bash_profile cd /vagrant/borg . borg-env/bin/activate - git clone https://github.com/pyinstaller/pyinstaller.git + git clone https://github.com/thomaswaldmann/pyinstaller.git cd pyinstaller - # develop branch, with rebuilt bootloaders, with ThomasWaldmann/do-not-overwrite-LD_LP - git checkout fd3df7796afa367e511c881dac983cad0697b9a3 + # develop branch, with fixed / freshly rebuilt bootloaders + git checkout fresh-bootloader pip install -e . EOF end @@ -247,10 +247,10 @@ def install_pyinstaller_bootloader(boxname) . ~/.bash_profile cd /vagrant/borg . borg-env/bin/activate - git clone https://github.com/pyinstaller/pyinstaller.git + git clone https://github.com/thomaswaldmann/pyinstaller.git cd pyinstaller - # develop branch, with rebuilt bootloaders, with ThomasWaldmann/do-not-overwrite-LD_LP - git checkout fd3df7796afa367e511c881dac983cad0697b9a3 + # develop branch, with fixed / freshly rebuilt bootloaders + git checkout fresh-bootloader # build bootloader, if it is not included cd bootloader python ./waf all From b4c7cce67d6ac6b43560c9d5e693e542c7bc454f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 9 Sep 2016 01:27:27 +0200 Subject: [PATCH 0212/1387] borg check: delete chunks with integrity errors, fixes #1575 so they can be "repaired" immediately and maybe healed later. --- src/borg/archive.py | 33 +++++++++++++++++++++++++++++++++ src/borg/testsuite/archiver.py | 4 ++++ 2 files changed, 37 insertions(+) diff --git a/src/borg/archive.py b/src/borg/archive.py index 9546cb0a..be2e5408 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1035,6 +1035,7 @@ class ArchiveChecker: logger.info('Starting cryptographic data integrity verification...') count = len(self.chunks) errors = 0 + defect_chunks = [] pi = ProgressIndicatorPercent(total=count, msg="Verifying data %6.2f%%", step=0.01) for chunk_id, (refcount, *_) in self.chunks.iteritems(): pi.show() @@ -1052,7 +1053,39 @@ class ArchiveChecker: self.error_found = True errors += 1 logger.error('chunk %s, integrity error: %s', bin_to_hex(chunk_id), integrity_error) + defect_chunks.append(chunk_id) pi.finish() + if defect_chunks: + if self.repair: + # if we kill the defect chunk here, subsequent actions within this "borg check" + # run will find missing chunks and replace them with all-zero replacement + # chunks and flag the files as "repaired". + # if another backup is done later and the missing chunks get backupped again, + # a "borg check" afterwards can heal all files where this chunk was missing. + logger.warning('Found defect chunks. They will be deleted now, so affected files can ' + 'get repaired now and maybe healed later.') + for defect_chunk in defect_chunks: + # remote repo (ssh): retry might help for strange network / NIC / RAM errors + # as the chunk will be retransmitted from remote server. + # local repo (fs): as chunks.iteritems loop usually pumps a lot of data through, + # a defect chunk is likely not in the fs cache any more and really gets re-read + # from the underlying media. + encrypted_data = self.repository.get(defect_chunk) + try: + _chunk_id = None if defect_chunk == Manifest.MANIFEST_ID else defect_chunk + self.key.decrypt(_chunk_id, encrypted_data) + except IntegrityError: + # failed twice -> get rid of this chunk + del self.chunks[defect_chunk] + self.repository.delete(defect_chunk) + logger.debug('chunk %s deleted.', bin_to_hex(defect_chunk)) + else: + logger.warning('chunk %s not deleted, did not consistently fail.') + else: + logger.warning('Found defect chunks. With --repair, they would get deleted, so affected ' + 'files could get repaired then and maybe healed later.') + for defect_chunk in defect_chunks: + logger.debug('chunk %s is defect.', bin_to_hex(defect_chunk)) log = logger.error if errors else logger.info log('Finished cryptographic data integrity verification, verified %d chunks with %d integrity errors.', count, errors) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 181ccb17..18d8c764 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1956,6 +1956,10 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.cmd('check', self.repository_location, exit_code=0) output = self.cmd('check', '--verify-data', self.repository_location, exit_code=1) assert bin_to_hex(chunk.id) + ', integrity error' in output + # repair (heal is tested in another test) + output = self.cmd('check', '--repair', '--verify-data', self.repository_location, exit_code=0) + assert bin_to_hex(chunk.id) + ', integrity error' in output + assert 'testsuite/archiver.py: New missing file chunk detected' in output def test_verify_data(self): self._test_verify_data('--encryption', 'repokey') From ae5b4980f2a4403bfd22097c2c9a93b5169061bc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 15 Sep 2016 11:23:58 +0200 Subject: [PATCH 0213/1387] Repository.check: improve object count mismatch diagnostic --- src/borg/repository.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 6c0159a7..022a8d48 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -687,7 +687,23 @@ class Repository: # current_index = "as found on disk" # self.index = "as rebuilt in-memory from segments" if len(current_index) != len(self.index): - report_error('Index object count mismatch. {} != {}'.format(len(current_index), len(self.index))) + report_error('Index object count mismatch.') + logger.error('committed index: %d objects', len(current_index)) + logger.error('rebuilt index: %d objects', len(self.index)) + + line_format = '%-64s %-16s %-16s' + not_found = '' + logger.warning(line_format, 'ID', 'rebuilt index', 'committed index') + for key, value in self.index.iteritems(): + current_value = current_index.get(key, not_found) + if current_value != value: + logger.warning(line_format, bin_to_hex(key), value, current_value) + for key, current_value in current_index.iteritems(): + if key in self.index: + continue + value = self.index.get(key, not_found) + if current_value != value: + logger.warning(line_format, bin_to_hex(key), value, current_value) elif current_index: for key, value in self.index.iteritems(): if current_index.get(key, (-1, -1)) != value: From 5d22078f3592e6b3914ec037a8dc86c5572cd868 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 15 Sep 2016 14:56:11 +0200 Subject: [PATCH 0214/1387] fuse: add parameter check to ItemCache.get to make potential failures more clear --- src/borg/fuse.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 47b8568b..8e3a8e4d 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -43,7 +43,10 @@ class ItemCache: return pos + self.offset def get(self, inode): - self.fd.seek(inode - self.offset, io.SEEK_SET) + offset = inode - self.offset + if offset < 0: + raise ValueError('ItemCache.get() called with an invalid inode number') + self.fd.seek(offset, io.SEEK_SET) item = next(msgpack.Unpacker(self.fd, read_size=1024)) return Item(internal_dict=item) From 001500ab99ed68c6ed4e277956fcc54225106a0a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 15 Sep 2016 15:41:15 +0200 Subject: [PATCH 0215/1387] fuse: refactor file versioning code --- src/borg/fuse.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 8e3a8e4d..a8ad62dc 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -162,17 +162,18 @@ class FuseOperations(llfuse.Operations): self.process_leaf(segments[-1], item, parent, prefix, is_dir) def process_leaf(self, name, item, parent, prefix, is_dir): - def version_name(name, item): + def file_version(item): if 'chunks' in item: ident = 0 for chunkid, _, _ in item.chunks: ident = adler32(chunkid, ident) - name = name + safe_encode('.%08x' % ident) - return name + return ident if self.versions and not is_dir: parent = self.process_inner(name, parent) - name = version_name(name, item) + version = file_version(item) + if version is not None: + name += safe_encode('.%08x' % version) if 'source' in item and stat.S_ISREG(item.mode): inode = self._find_inode(item.source, prefix) From c021cf466a6380271fadfaae8e4dbea57d5b04b7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 15 Sep 2016 20:29:14 +0200 Subject: [PATCH 0216/1387] fuse: add test for hardlinks in versions view --- src/borg/testsuite/archiver.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index aec0cff6..79777ec5 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1453,6 +1453,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_fuse_versions_view(self): self.cmd('init', self.repository_location) self.create_regular_file('test', contents=b'first') + if are_hardlinks_supported(): + self.create_regular_file('hardlink1', contents=b'') + os.link('input/hardlink1', 'input/hardlink2') self.cmd('create', self.repository_location + '::archive1', 'input') self.create_regular_file('test', contents=b'second') self.cmd('create', self.repository_location + '::archive2', 'input') @@ -1463,6 +1466,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): files = os.listdir(path) assert all(f.startswith('test.') for f in files) # ... with files test.xxxxxxxx in there assert {b'first', b'second'} == {open(os.path.join(path, f), 'rb').read() for f in files} + if are_hardlinks_supported(): + st1 = os.stat(os.path.join(mountpoint, 'input', 'hardlink1', 'hardlink1.00000000')) + st2 = os.stat(os.path.join(mountpoint, 'input', 'hardlink2', 'hardlink2.00000000')) + assert st1.st_ino == st2.st_ino @unittest.skipUnless(has_llfuse, 'llfuse not installed') def test_fuse_allow_damaged_files(self): From 39170479aa8aff98d031224ca441cab8840c1642 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 15 Sep 2016 20:31:57 +0200 Subject: [PATCH 0217/1387] fuse: fix hardlinks in versions view, fixes #1599 --- src/borg/fuse.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index a8ad62dc..f20ae899 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -15,7 +15,7 @@ from .logger import create_logger logger = create_logger() from .archive import Archive -from .helpers import daemonize, safe_encode +from .helpers import daemonize from .item import Item from .lrucache import LRUCache @@ -136,6 +136,7 @@ class FuseOperations(llfuse.Operations): def process_archive(self, archive, prefix=[]): """Build fuse inode hierarchy from archive metadata """ + self.file_versions = {} # for versions mode: original path -> version unpacker = msgpack.Unpacker() for key, chunk in zip(archive.metadata.items, self.repository.get_many(archive.metadata.items)): _, data = self.key.decrypt(key, chunk) @@ -155,7 +156,6 @@ class FuseOperations(llfuse.Operations): self.items[inode] = item continue segments = prefix + os.fsencode(os.path.normpath(item.path)).split(b'/') - del item.path parent = 1 for segment in segments[:-1]: parent = self.process_inner(segment, parent) @@ -169,14 +169,31 @@ class FuseOperations(llfuse.Operations): ident = adler32(chunkid, ident) return ident + def make_versioned_name(name, version, add_dir=False): + if add_dir: + # add intermediate directory with same name as filename + path_fname = name.rsplit(b'/', 1) + name += b'/' + path_fname[-1] + return name + os.fsencode('.%08x' % version) + if self.versions and not is_dir: parent = self.process_inner(name, parent) version = file_version(item) if version is not None: - name += safe_encode('.%08x' % version) + # regular file, with contents - maybe a hardlink master + name = make_versioned_name(name, version) + self.file_versions[item.path] = version + del item.path # safe some space if 'source' in item and stat.S_ISREG(item.mode): - inode = self._find_inode(item.source, prefix) + # a hardlink, no contents, is the hardlink master + source = item.source + if self.versions: + # adjust source name with version + version = self.file_versions[source] + source = os.fsdecode(make_versioned_name(os.fsencode(source), version, add_dir=True)) + name = make_versioned_name(name, version) + inode = self._find_inode(source, prefix) item = self.cache.get(inode) item.nlink = item.get('nlink', 1) + 1 self.items[inode] = item From 260ef31728a48dce87cd0e0a6a5671c3686ba909 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 15 Sep 2016 21:11:23 +0200 Subject: [PATCH 0218/1387] fuse: refactor for less encoding dance --- src/borg/fuse.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index f20ae899..b822332d 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -143,19 +143,20 @@ class FuseOperations(llfuse.Operations): unpacker.feed(data) for item in unpacker: item = Item(internal_dict=item) + path = os.fsencode(os.path.normpath(item.path)) is_dir = stat.S_ISDIR(item.mode) if is_dir: try: # This can happen if an archive was created with a command line like # $ borg create ... dir1/file dir1 # In this case the code below will have created a default_dir inode for dir1 already. - inode = self._find_inode(item.path, prefix) + inode = self._find_inode(path, prefix) except KeyError: pass else: self.items[inode] = item continue - segments = prefix + os.fsencode(os.path.normpath(item.path)).split(b'/') + segments = prefix + path.split(b'/') parent = 1 for segment in segments[:-1]: parent = self.process_inner(segment, parent) @@ -182,16 +183,17 @@ class FuseOperations(llfuse.Operations): if version is not None: # regular file, with contents - maybe a hardlink master name = make_versioned_name(name, version) - self.file_versions[item.path] = version + path = os.fsencode(os.path.normpath(item.path)) + self.file_versions[path] = version del item.path # safe some space if 'source' in item and stat.S_ISREG(item.mode): # a hardlink, no contents, is the hardlink master - source = item.source + source = os.fsencode(os.path.normpath(item.source)) if self.versions: # adjust source name with version version = self.file_versions[source] - source = os.fsdecode(make_versioned_name(os.fsencode(source), version, add_dir=True)) + source = make_versioned_name(source, version, add_dir=True) name = make_versioned_name(name, version) inode = self._find_inode(source, prefix) item = self.cache.get(inode) @@ -236,7 +238,7 @@ class FuseOperations(llfuse.Operations): return self.cache.get(inode) def _find_inode(self, path, prefix=[]): - segments = prefix + os.fsencode(os.path.normpath(path)).split(b'/') + segments = prefix + path.split(b'/') inode = 1 for segment in segments: inode = self.contents[inode][segment] From deadc81729af102d653f514cc500a7f2694b9f6d Mon Sep 17 00:00:00 2001 From: Stefano Probst Date: Sat, 17 Sep 2016 09:14:01 +0200 Subject: [PATCH 0219/1387] Fix inconsistency in FAQ The script in the FAQ is named pv-wrapper. But in the variable export pv-wrapper.sh was used. --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index 0806c483..a84cf481 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -403,7 +403,7 @@ Create a wrapper script: /usr/local/bin/pv-wrapper :: Add BORG_RSH environment variable to use pipeviewer wrapper script with ssh. :: - export BORG_RSH='/usr/local/bin/pv-wrapper.sh ssh' + export BORG_RSH='/usr/local/bin/pv-wrapper ssh' Now |project_name| will be bandwidth limited. Nice thing about pv is that you can change rate-limit on the fly: :: From 34ec344e9d547995b6faee6d07b6648eb0e4e802 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 17 Sep 2016 17:19:26 +0200 Subject: [PATCH 0220/1387] trivial code optimization --- borg/archive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/borg/archive.py b/borg/archive.py index bacf3760..8619fd8f 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -642,7 +642,7 @@ Number of files: {0.stats.nfiles}'''.format( # Is it a hard link? if st.st_nlink > 1: source = self.hard_links.get((st.st_ino, st.st_dev)) - if (st.st_ino, st.st_dev) in self.hard_links: + if source is not None: item = self.stat_attrs(st, path) item.update({b'path': safe_path, b'source': source}) self.add_item(item) From 84b3295a0d0c9dab9f64d22832cc4ab68b74cb47 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Thu, 15 Sep 2016 22:13:35 +0200 Subject: [PATCH 0221/1387] Archiver,RemoteRepository: Add --remote-ratelimit The --remote-ratelimit option adds a very simple rate limit for the sending data to the remote. Currently implemented by sleeping if the transmission speed is greater than the limit. --- src/borg/archiver.py | 2 ++ src/borg/remote.py | 37 +++++++++++++++++++++- src/borg/testsuite/remote.py | 60 ++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/borg/testsuite/remote.py diff --git a/src/borg/archiver.py b/src/borg/archiver.py index a63513de..1327980a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1322,6 +1322,8 @@ class Archiver: help='set umask to M (local and remote, default: %(default)04o)') common_group.add_argument('--remote-path', dest='remote_path', metavar='PATH', help='set remote path to executable (default: "borg")') + common_group.add_argument('--remote-ratelimit', dest='remote_ratelimit', type=int, metavar='rate', + help='set remote network upload rate limit in kiByte/s (default: 0=unlimited)') common_group.add_argument('--consider-part-files', dest='consider_part_files', action='store_true', default=False, help='treat part files like normal files (e.g. to list/extract them)') diff --git a/src/borg/remote.py b/src/borg/remote.py index 4632a50a..294bd40c 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -6,6 +6,7 @@ import select import shlex import sys import tempfile +import time import traceback from subprocess import Popen, PIPE @@ -25,6 +26,8 @@ BUFSIZE = 10 * 1024 * 1024 MAX_INFLIGHT = 100 +RATELIMIT_PERIOD = 0.1 + class ConnectionClosed(Error): """Connection closed by remote host""" @@ -166,6 +169,36 @@ class RepositoryServer: # pragma: no cover return self.repository.id +class SleepingBandwidthLimiter: + def __init__(self, limit): + if limit: + self.ratelimit = int(limit * RATELIMIT_PERIOD) + self.ratelimit_last = time.monotonic() + self.ratelimit_quota = self.ratelimit + else: + self.ratelimit = None + + def write(self, fd, to_send): + if self.ratelimit: + now = time.monotonic() + if self.ratelimit_last + RATELIMIT_PERIOD <= now: + self.ratelimit_quota += self.ratelimit + if self.ratelimit_quota > 2 * self.ratelimit: + self.ratelimit_quota = 2 * self.ratelimit + self.ratelimit_last = now + if self.ratelimit_quota == 0: + tosleep = self.ratelimit_last + RATELIMIT_PERIOD - now + time.sleep(tosleep) + self.ratelimit_quota += self.ratelimit + self.ratelimit_last = time.monotonic() + if len(to_send) > self.ratelimit_quota: + to_send = to_send[:self.ratelimit_quota] + written = os.write(fd, to_send) + if self.ratelimit: + self.ratelimit_quota -= written + return written + + class RemoteRepository: extra_test_args = [] @@ -185,6 +218,8 @@ class RemoteRepository: self.cache = {} self.ignore_responses = set() self.responses = {} + self.ratelimit = SleepingBandwidthLimiter(args.remote_ratelimit * 1024 if args and args.remote_ratelimit else 0) + self.unpacker = msgpack.Unpacker(use_list=False) self.p = None testing = location.host == '__testsuite__' @@ -406,7 +441,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. if self.to_send: try: - self.to_send = self.to_send[os.write(self.stdin_fd, self.to_send):] + self.to_send = self.to_send[self.ratelimit.write(self.stdin_fd, self.to_send):] except OSError as e: # io.write might raise EAGAIN even though select indicates # that the fd should be writable diff --git a/src/borg/testsuite/remote.py b/src/borg/testsuite/remote.py new file mode 100644 index 00000000..b9eddabd --- /dev/null +++ b/src/borg/testsuite/remote.py @@ -0,0 +1,60 @@ +import os +import time + +import pytest + +from ..remote import SleepingBandwidthLimiter + + +class TestSleepingBandwidthLimiter: + def expect_write(self, fd, data): + self.expected_fd = fd + self.expected_data = data + + def check_write(self, fd, data): + assert fd == self.expected_fd + assert data == self.expected_data + return len(data) + + def test_write_unlimited(self, monkeypatch): + monkeypatch.setattr(os, "write", self.check_write) + + it = SleepingBandwidthLimiter(0) + self.expect_write(5, b"test") + it.write(5, b"test") + + def test_write(self, monkeypatch): + monkeypatch.setattr(os, "write", self.check_write) + monkeypatch.setattr(time, "monotonic", lambda: now) + monkeypatch.setattr(time, "sleep", lambda x: None) + + now = 100 + + it = SleepingBandwidthLimiter(100) + + # all fits + self.expect_write(5, b"test") + it.write(5, b"test") + + # only partial write + self.expect_write(5, b"123456") + it.write(5, b"1234567890") + + # sleeps + self.expect_write(5, b"123456") + it.write(5, b"123456") + + # long time interval between writes + now += 10 + self.expect_write(5, b"1") + it.write(5, b"1") + + # long time interval between writes, filling up quota + now += 10 + self.expect_write(5, b"1") + it.write(5, b"1") + + # long time interval between writes, filling up quota to clip to maximum + now += 10 + self.expect_write(5, b"1") + it.write(5, b"1") From 7b9d0c9739c459066caad9122eb92505d13a6c16 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 22 Sep 2016 02:43:57 +0200 Subject: [PATCH 0222/1387] yes(): abort on wrong answers, saying so except for the passphrase display as we can only display it as long as we have it in memory, here: retry, telling the user if he entered something invalid and needs to enter again. --- borg/archiver.py | 7 ++++--- borg/cache.py | 6 ++++-- borg/helpers.py | 5 ++--- borg/key.py | 5 +++-- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index bb8e33f7..f367a4a0 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -141,7 +141,8 @@ class Archiver: msg = ("'check --repair' is an experimental feature that might result in data loss." + "\n" + "Type 'YES' if you understand this and want to continue: ") - if not yes(msg, false_msg="Aborting.", truish=('YES', ), + if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", + truish=('YES', ), retry=False, env_var_override='BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'): return EXIT_ERROR if not args.archives_only: @@ -466,8 +467,8 @@ class Archiver: msg.append(format_archive(archive_info)) msg.append("Type 'YES' if you understand this and want to continue: ") msg = '\n'.join(msg) - if not yes(msg, false_msg="Aborting.", truish=('YES', ), - env_var_override='BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'): + if not yes(msg, false_msg="Aborting.", invalid_msg='Invalid answer, aborting.', truish=('YES', ), + retry=False, env_var_override='BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'): self.exit_code = EXIT_ERROR return self.exit_code repository.destroy() diff --git a/borg/cache.py b/borg/cache.py index 763a262a..b843fc49 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -63,7 +63,8 @@ class Cache: msg = ("Warning: Attempting to access a previously unknown unencrypted repository!" + "\n" + "Do you want to continue? [yN] ") - if not yes(msg, false_msg="Aborting.", env_var_override='BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK'): + if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", + retry=False, env_var_override='BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK'): raise self.CacheInitAbortedError() self.create() self.open(lock_wait=lock_wait) @@ -73,7 +74,8 @@ class Cache: msg = ("Warning: The repository at location {} was previously located at {}".format(repository._location.canonical_path(), self.previous_location) + "\n" + "Do you want to continue? [yN] ") - if not yes(msg, false_msg="Aborting.", env_var_override='BORG_RELOCATED_REPO_ACCESS_IS_OK'): + if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", + retry=False, env_var_override='BORG_RELOCATED_REPO_ACCESS_IS_OK'): raise self.RepositoryAccessAborted() if sync and self.manifest.id != self.manifest_id: diff --git a/borg/helpers.py b/borg/helpers.py index 27b3f0d3..2867d4c9 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -959,9 +959,8 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, default=False, retry=True, env_var_override=None, ofile=None, input=input): """Output (usually a question) and let user input an answer. Qualifies the answer according to falsish, truish and defaultish as True, False or . - If it didn't qualify and retry_msg is None (no retries wanted), - return the default [which defaults to False]. Otherwise let user retry - answering until answer is qualified. + If it didn't qualify and retry is False (no retries wanted), return the default [which + defaults to False]. If retry is True let user retry answering until answer is qualified. If env_var_override is given and this var is present in the environment, do not ask the user, but just use the env var contents as answer as if it was typed in. diff --git a/borg/key.py b/borg/key.py index 95178f7c..e88baf57 100644 --- a/borg/key.py +++ b/borg/key.py @@ -190,8 +190,9 @@ class Passphrase(str): @classmethod def verification(cls, passphrase): - if yes('Do you want your passphrase to be displayed for verification? [yN]: ', - env_var_override='BORG_DISPLAY_PASSPHRASE'): + msg = 'Do you want your passphrase to be displayed for verification? [yN]: ' + if yes(msg, retry_msg=msg, invalid_msg='Invalid answer, try again.', + retry=True, env_var_override='BORG_DISPLAY_PASSPHRASE'): print('Your passphrase (between double-quotes): "%s"' % passphrase, file=sys.stderr) print('Make sure the passphrase displayed above is exactly what you wanted.', From 7e7dd9688dd41b26a22786151edb748bc30d5e21 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 23 Sep 2016 00:26:04 +0200 Subject: [PATCH 0223/1387] adapt formatting to narrow screens, do not crash, fixes #1628 when screen width was too narrow, the {space} placeholder could get negative, which crashes as it is a width specification. now we simplify progress output if screen is narrow. we stop output completely if screen is ridiculously narrow. --- borg/helpers.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/borg/helpers.py b/borg/helpers.py index 27b3f0d3..24499c77 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -223,9 +223,13 @@ class Statistics: msg = '{0.osize_fmt} O {0.csize_fmt} C {0.usize_fmt} D {0.nfiles} N '.format(self) path = remove_surrogates(item[b'path']) if item else '' space = columns - len(msg) - if space < len('...') + len(path): - path = '%s...%s' % (path[:(space // 2) - len('...')], path[-space // 2:]) - msg += "{0:<{space}}".format(path, space=space) + if space < 12: + msg = '' + space = columns - len(msg) + if space >= 8: + if space < len('...') + len(path): + path = '%s...%s' % (path[:(space // 2) - len('...')], path[-space // 2:]) + msg += "{0:<{space}}".format(path, space=space) else: msg = ' ' * columns print(msg, file=stream or sys.stderr, end="\r", flush=True) From a56dc44e1f7b76b98a2ba7508fd8a9604edac0df Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Thu, 22 Sep 2016 10:36:04 +0200 Subject: [PATCH 0224/1387] Change {utcnow} and {now} to ISO-8601 format --- src/borg/archiver.py | 6 ++++-- src/borg/helpers.py | 14 ++++++++++++-- src/borg/testsuite/helpers.py | 8 +++++++- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index a63513de..d32acc9e 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1233,11 +1233,13 @@ class Archiver: {now} - The current local date and time. + The current local date and time, by default in ISO-8601 format. + You can also supply your own `format string `_, e.g. {now:%Y-%m-%d_%H:%M:%S} {utcnow} - The current UTC date and time. + The current UTC date and time, by default in ISO-8601 format. + You can also supply your own `format string `_, e.g. {utcnow:%Y-%m-%d_%H:%M:%S} {user} diff --git a/src/borg/helpers.py b/src/borg/helpers.py index e6c805bc..e4456953 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -622,6 +622,16 @@ def partial_format(format, mapping): return format +class DatetimeWrapper: + def __init__(self, dt): + self.dt = dt + + def __format__(self, format_spec): + if format_spec == '': + format_spec = '%Y-%m-%dT%H:%M:%S' + return self.dt.__format__(format_spec) + + def format_line(format, data): try: return format.format(**data) @@ -636,8 +646,8 @@ def replace_placeholders(text): 'pid': os.getpid(), 'fqdn': socket.getfqdn(), 'hostname': socket.gethostname(), - 'now': current_time.now(), - 'utcnow': current_time.utcnow(), + 'now': DatetimeWrapper(current_time.now()), + 'utcnow': DatetimeWrapper(current_time.utcnow()), 'user': uid2user(os.getuid(), os.getuid()), 'uuid4': str(uuid.uuid4()), 'borgversion': borg_version, diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 6583b4ea..d1c7c95b 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -11,7 +11,7 @@ import msgpack.fallback from ..helpers import Location from ..helpers import Buffer -from ..helpers import partial_format, format_file_size, parse_file_size, format_timedelta, format_line, PlaceholderError +from ..helpers import partial_format, format_file_size, parse_file_size, format_timedelta, format_line, PlaceholderError, replace_placeholders from ..helpers import make_path_safe, clean_lines from ..helpers import prune_within, prune_split from ..helpers import get_cache_dir, get_keys_dir, get_nonces_dir @@ -1035,3 +1035,9 @@ def test_format_line_erroneous(): assert format_line('{invalid}', data) with pytest.raises(PlaceholderError): assert format_line('{}', data) + + +def test_replace_placeholders(): + now = datetime.now() + assert " " not in replace_placeholders('{now}') + assert int(replace_placeholders('{now:%Y}')) == now.year From 55a33319424a349fe5189c22cf328c8abc42f964 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 18 Sep 2016 17:20:27 +0200 Subject: [PATCH 0225/1387] Implement key import / export We recommed that users backup their keys, this adds simple to use commands to do so. Supported formats are the keyfile format used by borg internally and a special format with by line checksums for printed backups. For this format the import is an interactive process which checks each line as soon as it is input. Fixes #1555 --- borg/archiver.py | 62 +++++++++++ borg/keymanager.py | 213 +++++++++++++++++++++++++++++++++++++ borg/testsuite/archiver.py | 104 +++++++++++++++++- 3 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 borg/keymanager.py diff --git a/borg/archiver.py b/borg/archiver.py index bb8e33f7..6ef1d0fb 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -30,6 +30,7 @@ from .upgrader import AtticRepositoryUpgrader, BorgRepositoryUpgrader from .repository import Repository from .cache import Cache from .key import key_creator, RepoKey, PassphraseKey +from .keymanager import KeyManager from .archive import backup_io, BackupOSError, Archive, ArchiveChecker, CHUNKER_PARAMS, is_special from .remote import RepositoryServer, RemoteRepository, cache_if_remote @@ -159,6 +160,39 @@ class Archiver: key.change_passphrase() return EXIT_SUCCESS + @with_repository(lock=False, exclusive=False, manifest=False, cache=False) + def do_key_export(self, args, repository): + """Export the repository key for backup""" + manager = KeyManager(repository) + manager.load_keyblob() + if args.paper: + manager.export_paperkey(args.path) + else: + if not args.path: + self.print_error("output file to export key to expected") + return EXIT_ERROR + manager.export(args.path) + return EXIT_SUCCESS + + @with_repository(lock=False, exclusive=False, manifest=False, cache=False) + def do_key_import(self, args, repository): + """Import the repository key from backup""" + manager = KeyManager(repository) + if args.paper: + if args.path: + self.print_error("with --paper import from file is not supported") + return EXIT_ERROR + manager.import_paperkey(args) + else: + if not args.path: + self.print_error("input file to import key from expected") + return EXIT_ERROR + if not os.path.exists(args.path): + self.print_error("input file does not exist: " + args.path) + return EXIT_ERROR + manager.import_keyfile(args) + return EXIT_SUCCESS + @with_repository(manifest=False) def do_migrate_to_repokey(self, args, repository): """Migrate passphrase -> repokey""" @@ -1076,6 +1110,34 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False)) + subparser = subparsers.add_parser('key-export', parents=[common_parser], + description=self.do_key_export.__doc__, + epilog="", + formatter_class=argparse.RawDescriptionHelpFormatter, + help='export repository key for backup') + subparser.set_defaults(func=self.do_key_export) + subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', + type=location_validator(archive=False)) + subparser.add_argument('path', metavar='PATH', nargs='?', type=str, + help='where to store the backup') + subparser.add_argument('--paper', dest='paper', action='store_true', + default=False, + help='Create an export suitable for printing and later type-in') + + subparser = subparsers.add_parser('key-import', parents=[common_parser], + description=self.do_key_import.__doc__, + epilog="", + formatter_class=argparse.RawDescriptionHelpFormatter, + help='import repository key from backup') + subparser.set_defaults(func=self.do_key_import) + subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', + type=location_validator(archive=False)) + subparser.add_argument('path', metavar='PATH', nargs='?', type=str, + help='path to the backup') + subparser.add_argument('--paper', dest='paper', action='store_true', + default=False, + help='interactively import from a backup done with --paper') + migrate_to_repokey_epilog = textwrap.dedent(""" This command migrates a repository from passphrase mode (not supported any more) to repokey mode. diff --git a/borg/keymanager.py b/borg/keymanager.py new file mode 100644 index 00000000..244e16c6 --- /dev/null +++ b/borg/keymanager.py @@ -0,0 +1,213 @@ +from binascii import hexlify, unhexlify, a2b_base64, b2a_base64 +import binascii +import textwrap +from hashlib import sha256 + +from .key import KeyfileKey, RepoKey, PassphraseKey, KeyfileNotFoundError, PlaintextKey +from .helpers import Manifest, NoManifestError, Error, yes +from .repository import Repository + + +class UnencryptedRepo(Error): + """Keymanagement not available for unencrypted repositories.""" + + +class UnknownKeyType(Error): + """Keytype {0} is unknown.""" + + +class RepoIdMismatch(Error): + """This key backup seems to be for a different backup repository, aborting.""" + + +class NotABorgKeyFile(Error): + """This file is not a borg key backup, aborting.""" + + +def sha256_truncated(data, num): + h = sha256() + h.update(data) + return h.hexdigest()[:num] + + +KEYBLOB_LOCAL = 'local' +KEYBLOB_REPO = 'repo' + + +class KeyManager: + def __init__(self, repository): + self.repository = repository + self.keyblob = None + self.keyblob_storage = None + + try: + cdata = self.repository.get(Manifest.MANIFEST_ID) + except Repository.ObjectNotFound: + raise NoManifestError + + key_type = cdata[0] + if key_type == KeyfileKey.TYPE: + self.keyblob_storage = KEYBLOB_LOCAL + elif key_type == RepoKey.TYPE or key_type == PassphraseKey.TYPE: + self.keyblob_storage = KEYBLOB_REPO + elif key_type == PlaintextKey.TYPE: + raise UnencryptedRepo() + else: + raise UnknownKeyType(key_type) + + def load_keyblob(self): + if self.keyblob_storage == KEYBLOB_LOCAL: + k = KeyfileKey(self.repository) + target = k.find_key() + with open(target, 'r') as fd: + self.keyblob = ''.join(fd.readlines()[1:]) + + elif self.keyblob_storage == KEYBLOB_REPO: + self.keyblob = self.repository.load_key().decode() + + def store_keyblob(self, args): + if self.keyblob_storage == KEYBLOB_LOCAL: + k = KeyfileKey(self.repository) + try: + target = k.find_key() + except KeyfileNotFoundError: + target = k.get_new_target(args) + + self.store_keyfile(target) + elif self.keyblob_storage == KEYBLOB_REPO: + self.repository.save_key(self.keyblob.encode('utf-8')) + + def store_keyfile(self, target): + with open(target, 'w') as fd: + fd.write('%s %s\n' % (KeyfileKey.FILE_ID, hexlify(self.repository.id).decode('ascii'))) + fd.write(self.keyblob) + if not self.keyblob.endswith('\n'): + fd.write('\n') + + def export(self, path): + self.store_keyfile(path) + + def export_paperkey(self, path): + def grouped(s): + ret = '' + i = 0 + for ch in s: + if i and i % 6 == 0: + ret += ' ' + ret += ch + i += 1 + return ret + + export = 'To restore key use borg key-import --paper /path/to/repo\n\n' + + binary = a2b_base64(self.keyblob) + export += 'BORG PAPER KEY v1\n' + lines = (len(binary) + 17) // 18 + repoid = hexlify(self.repository.id).decode('ascii')[:18] + complete_checksum = sha256_truncated(binary, 12) + export += 'id: {0:d} / {1} / {2} - {3}\n'.format(lines, + grouped(repoid), + grouped(complete_checksum), + sha256_truncated((str(lines) + '/' + repoid + '/' + complete_checksum).encode('ascii'), 2)) + idx = 0 + while len(binary): + idx += 1 + binline = binary[:18] + checksum = sha256_truncated(idx.to_bytes(2, byteorder='big') + binline, 2) + export += '{0:2d}: {1} - {2}\n'.format(idx, grouped(hexlify(binline).decode('ascii')), checksum) + binary = binary[18:] + + if path: + with open(path, 'w') as fd: + fd.write(export) + else: + print(export) + + def import_keyfile(self, args): + file_id = KeyfileKey.FILE_ID + first_line = file_id + ' ' + hexlify(self.repository.id).decode('ascii') + '\n' + with open(args.path, 'r') as fd: + file_first_line = fd.read(len(first_line)) + if file_first_line != first_line: + if not file_first_line.startswith(file_id): + raise NotABorgKeyFile() + else: + raise RepoIdMismatch() + self.keyblob = fd.read() + + self.store_keyblob(args) + + def import_paperkey(self, args): + # imported here because it has global side effects + import readline + + repoid = hexlify(self.repository.id).decode('ascii')[:18] + try: + while True: # used for repeating on overall checksum mismatch + # id line input + while True: + idline = input('id: ').replace(' ', '') + if idline == "": + if yes("Abort import? [yN]:"): + raise EOFError() + + try: + (data, checksum) = idline.split('-') + except ValueError: + print("each line must contain exactly one '-', try again") + continue + try: + (id_lines, id_repoid, id_complete_checksum) = data.split('/') + except ValueError: + print("the id line must contain exactly three '/', try again") + if sha256_truncated(data.lower().encode('ascii'), 2) != checksum: + print('line checksum did not match, try same line again') + continue + try: + lines = int(id_lines) + except ValueError: + print('internal error while parsing length') + + break + + if repoid != id_repoid: + raise RepoIdMismatch() + + result = b'' + idx = 1 + # body line input + while True: + inline = input('{0:2d}: '.format(idx)) + inline = inline.replace(' ', '') + if inline == "": + if yes("Abort import? [yN]:"): + raise EOFError() + try: + (data, checksum) = inline.split('-') + except ValueError: + print("each line must contain exactly one '-', try again") + continue + try: + part = unhexlify(data) + except binascii.Error: + print("only characters 0-9 and a-f and '-' are valid, try again") + continue + if sha256_truncated(idx.to_bytes(2, byteorder='big') + part, 2) != checksum: + print('line checksum did not match, try line {0} again'.format(idx)) + continue + result += part + if idx == lines: + break + idx += 1 + + if sha256_truncated(result, 12) != id_complete_checksum: + print('The overall checksum did not match, retry or enter a blank line to abort.') + continue + + self.keyblob = '\n'.join(textwrap.wrap(b2a_base64(result).decode('ascii'))) + '\n' + self.store_keyblob(args) + break + + except EOFError: + print('\n - aborted') + return diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 7b219359..d5896c03 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1,4 +1,4 @@ -from binascii import hexlify +from binascii import hexlify, unhexlify, b2a_base64 from configparser import ConfigParser import errno import os @@ -22,6 +22,8 @@ from ..archiver import Archiver from ..cache import Cache from ..crypto import bytes_to_long, num_aes_blocks from ..helpers import Manifest, PatternMatcher, parse_pattern, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR +from ..key import RepoKey, KeyfileKey, Passphrase +from ..keymanager import RepoIdMismatch, NotABorgKeyFile from ..remote import RemoteRepository, PathNotAllowed from ..repository import Repository from . import BaseTestCase, changedir, environment_variable @@ -1194,6 +1196,106 @@ class ArchiverTestCase(ArchiverTestCaseBase): output = self.cmd('debug-delete-obj', self.repository_location, 'invalid') assert "is invalid" in output + def test_key_export_keyfile(self): + export_file = self.output_path + '/exported' + self.cmd('init', self.repository_location, '--encryption', 'keyfile') + repo_id = self._extract_repository_id(self.repository_path) + self.cmd('key-export', self.repository_location, export_file) + + with open(export_file, 'r') as fd: + export_contents = fd.read() + + assert export_contents.startswith('BORG_KEY ' + hexlify(repo_id).decode() + '\n') + + key_file = self.keys_path + '/' + os.listdir(self.keys_path)[0] + + with open(key_file, 'r') as fd: + key_contents = fd.read() + + assert key_contents == export_contents + + os.unlink(key_file) + + self.cmd('key-import', self.repository_location, export_file) + + with open(key_file, 'r') as fd: + key_contents2 = fd.read() + + assert key_contents2 == key_contents + + def test_key_export_repokey(self): + export_file = self.output_path + '/exported' + self.cmd('init', self.repository_location, '--encryption', 'repokey') + repo_id = self._extract_repository_id(self.repository_path) + self.cmd('key-export', self.repository_location, export_file) + + with open(export_file, 'r') as fd: + export_contents = fd.read() + + assert export_contents.startswith('BORG_KEY ' + hexlify(repo_id).decode() + '\n') + + with Repository(self.repository_path) as repository: + repo_key = RepoKey(repository) + repo_key.load(None, Passphrase.env_passphrase()) + + backup_key = KeyfileKey(None) + backup_key.load(export_file, Passphrase.env_passphrase()) + + assert repo_key.enc_key == backup_key.enc_key + + with Repository(self.repository_path) as repository: + repository.save_key(b'') + + self.cmd('key-import', self.repository_location, export_file) + + with Repository(self.repository_path) as repository: + repo_key2 = RepoKey(repository) + repo_key2.load(None, Passphrase.env_passphrase()) + + assert repo_key2.enc_key == repo_key2.enc_key + + def test_key_import_errors(self): + export_file = self.output_path + '/exported' + self.cmd('init', self.repository_location, '--encryption', 'keyfile') + + self.cmd('key-import', self.repository_location, export_file, exit_code=EXIT_ERROR) + + with open(export_file, 'w') as fd: + fd.write('something not a key\n') + + self.assert_raises(NotABorgKeyFile, lambda: self.cmd('key-import', self.repository_location, export_file)) + + with open(export_file, 'w') as fd: + fd.write('BORG_KEY a0a0a0\n') + + self.assert_raises(RepoIdMismatch, lambda: self.cmd('key-import', self.repository_location, export_file)) + + def test_key_export_paperkey(self): + repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239' + + export_file = self.output_path + '/exported' + self.cmd('init', self.repository_location, '--encryption', 'keyfile') + self._set_repository_id(self.repository_path, unhexlify(repo_id)) + + key_file = self.keys_path + '/' + os.listdir(self.keys_path)[0] + + with open(key_file, 'w') as fd: + fd.write(KeyfileKey.FILE_ID + ' ' + repo_id + '\n') + fd.write(b2a_base64(b'abcdefghijklmnopqrstu').decode()) + + self.cmd('key-export', '--paper', self.repository_location, export_file) + + with open(export_file, 'r') as fd: + export_contents = fd.read() + + assert export_contents == """To restore key use borg key-import --paper /path/to/repo + +BORG PAPER KEY v1 +id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 + 1: 616263 646566 676869 6a6b6c 6d6e6f 707172 - 6d + 2: 737475 - 88 +""" + @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available') class ArchiverTestCaseBinary(ArchiverTestCase): From d6bfdafdefb869dd2d50cf918826bc7fadf1aa44 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 25 Sep 2016 02:32:02 +0200 Subject: [PATCH 0226/1387] borg help compression, fixes #1582 --- src/borg/archiver.py | 117 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 96 insertions(+), 21 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 2fe8ff11..ac39a063 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1258,6 +1258,92 @@ class Archiver: borg create /path/to/repo::{hostname}-{user}-{utcnow} ... borg create /path/to/repo::{hostname}-{now:%Y-%m-%d_%H:%M:%S} ... borg prune --prefix '{hostname}-' ...\n\n''') + helptext['compression'] = textwrap.dedent(''' + Compression is off by default, if you want some, you have to specify what you want. + + Valid compression specifiers are: + + none + + Do not compress. (default) + + lz4 + + Use lz4 compression. High speed, low compression. + + zlib[,L] + + Use zlib ("gz") compression. Medium speed, medium compression. + If you do not explicitely give the compression level L (ranging from 0 + to 9), it will use level 6. + Giving level 0 (means "no compression", but still has zlib protocol + overhead) is usually pointless, you better use "none" compression. + + lzma[,L] + + Use lzma ("xz") compression. Low speed, high compression. + If you do not explicitely give the compression level L (ranging from 0 + to 9), it will use level 6. + Giving levels above 6 is pointless and counterproductive because it does + not compress better due to the buffer size used by borg - but it wastes + lots of CPU cycles and RAM. + + auto,C[,L] + + Use a built-in heuristic to decide per chunk whether to compress or not. + The heuristic tries with lz4 whether the data is compressible. + For incompressible data, it will not use compression (uses "none"). + For compressible data, it uses the given C[,L] compression - with C[,L] + being any valid compression specifier. + + The decision about which compression to use is done by borg like this: + + 1. find a compression specifier (per file): + match the path/filename against all patterns in all --compression-from + files (if any). If a pattern matches, use the compression spec given for + that pattern. If no pattern matches (and also if you do not give any + --compression-from option), default to the compression spec given by + --compression. See docs/misc/compression.conf for an example config. + + 2. if the found compression spec is not "auto", the decision is taken: + use the found compression spec. + + 3. if the found compression spec is "auto", test compressibility of each + chunk using lz4. + If it is compressible, use the C,[L] compression spec given within the + "auto" specifier. If it is not compressible, use no compression. + + Examples:: + + borg create --compression lz4 REPO::ARCHIVE data + borg create --compression zlib REPO::ARCHIVE data + borg create --compression zlib,1 REPO::ARCHIVE data + borg create --compression auto,lzma,6 REPO::ARCHIVE data + borg create --compression-from compression.conf --compression auto,lzma ... + + compression.conf has entries like: + + # example config file for --compression-from option + # + # Format of non-comment / non-empty lines: + # : + # compression-spec is same format as for --compression option + # path/filename pattern is same format as for --exclude option + none:*.gz + none:*.zip + none:*.mp3 + none:*.ogg + + General remarks: + + It is no problem to mix different compression methods in one repo, + deduplication is done on the source data chunks (not on the compressed + or encrypted data). + + If some specific chunk was once compressed and stored into the repo, creating + another backup that also uses this chunk will not change the stored chunk. + So if you use different compression specs for the backups, whichever stores a + chunk first determines its compression. See also borg recreate.\n\n''') def do_help(self, parser, commands, args): if not args.topic: @@ -1624,19 +1710,13 @@ class Archiver: help='specify the chunker parameters. default: %d,%d,%d,%d' % CHUNKER_PARAMS) archive_group.add_argument('-C', '--compression', dest='compression', type=CompressionSpec, default=dict(name='none'), metavar='COMPRESSION', - help='select compression algorithm (and level):\n' - 'none == no compression (default),\n' - 'auto,C[,L] == built-in heuristic (try with lz4 whether the data is\n' - ' compressible) decides between none or C[,L] - with C[,L]\n' - ' being any valid compression algorithm (and optional level),\n' - 'lz4 == lz4,\n' - 'zlib == zlib (default level 6),\n' - 'zlib,0 .. zlib,9 == zlib (with level 0..9),\n' - 'lzma == lzma (default level 6),\n' - 'lzma,0 .. lzma,9 == lzma (with level 0..9).') + help='select compression algorithm, see the output of the ' + '"borg help compression" command for details.') archive_group.add_argument('--compression-from', dest='compression_files', type=argparse.FileType('r'), action='append', - metavar='COMPRESSIONCONFIG', help='read compression patterns from COMPRESSIONCONFIG, one per line') + metavar='COMPRESSIONCONFIG', + help='read compression patterns from COMPRESSIONCONFIG, see the output of the ' + '"borg help compression" command for details.') subparser.add_argument('location', metavar='ARCHIVE', type=location_validator(archive=True), @@ -2146,21 +2226,16 @@ class Archiver: 'alternatively, give a reference file/directory.') archive_group.add_argument('-C', '--compression', dest='compression', type=CompressionSpec, default=None, metavar='COMPRESSION', - help='select compression algorithm (and level):\n' - 'none == no compression (default),\n' - 'auto,C[,L] == built-in heuristic decides between none or C[,L] - with C[,L]\n' - ' being any valid compression algorithm (and optional level),\n' - 'lz4 == lz4,\n' - 'zlib == zlib (default level 6),\n' - 'zlib,0 .. zlib,9 == zlib (with level 0..9),\n' - 'lzma == lzma (default level 6),\n' - 'lzma,0 .. lzma,9 == lzma (with level 0..9).') + help='select compression algorithm, see the output of the ' + '"borg help compression" command for details.') archive_group.add_argument('--always-recompress', dest='always_recompress', action='store_true', help='always recompress chunks, don\'t skip chunks already compressed with the same' 'algorithm.') archive_group.add_argument('--compression-from', dest='compression_files', type=argparse.FileType('r'), action='append', - metavar='COMPRESSIONCONFIG', help='read compression patterns from COMPRESSIONCONFIG, one per line') + metavar='COMPRESSIONCONFIG', + help='read compression patterns from COMPRESSIONCONFIG, see the output of the ' + '"borg help compression" command for details.') archive_group.add_argument('--chunker-params', dest='chunker_params', type=ChunkerParams, default=None, metavar='CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE', From abace16945b9336b3ea7be43f23d98c1bbbe876b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 25 Sep 2016 10:12:42 +0200 Subject: [PATCH 0227/1387] Repository.check: log transaction IDs --- src/borg/repository.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 022a8d48..c4e27f72 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -650,18 +650,25 @@ class Repository: try: transaction_id = self.get_transaction_id() current_index = self.open_index(transaction_id) - except Exception: + logger.debug('Read committed index of transaction %d', transaction_id) + except Exception as exc: transaction_id = self.io.get_segments_transaction_id() current_index = None + logger.debug('Failed to read committed index (%s)', exc) if transaction_id is None: + logger.debug('No segments transaction found') transaction_id = self.get_index_transaction_id() if transaction_id is None: + logger.debug('No index transaction found, trying latest segment') transaction_id = self.io.get_latest_segment() if repair: self.io.cleanup(transaction_id) segments_transaction_id = self.io.get_segments_transaction_id() + logger.debug('Segment transaction is %s', segments_transaction_id) + logger.debug('Determined transaction is %s', transaction_id) self.prepare_txn(None) # self.index, self.compact, self.segments all empty now! segment_count = sum(1 for _ in self.io.segment_iterator()) + logger.debug('Found %d segments', segment_count) pi = ProgressIndicatorPercent(total=segment_count, msg="Checking segments %3.1f%%", step=0.1) for i, (segment, filename) in enumerate(self.io.segment_iterator()): pi.show(i) @@ -683,6 +690,7 @@ class Repository: report_error('Adding commit tag to segment {}'.format(transaction_id)) self.io.segment = transaction_id + 1 self.io.write_commit() + logger.info('Starting repository index check') if current_index and not repair: # current_index = "as found on disk" # self.index = "as rebuilt in-memory from segments" From 7b1f10347a766b87deabfac96d5e6d0cc9b2aa39 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Sep 2016 16:39:55 +0200 Subject: [PATCH 0228/1387] Repository: compact: fix incorrect preservation of delete tags --- src/borg/repository.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index c4e27f72..fa197ebf 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -481,8 +481,9 @@ class Repository: for tag, key, offset, data in self.io.iter_objects(segment, include_data=True): if tag == TAG_COMMIT: continue - in_index = self.index.get(key) == (segment, offset) - if tag == TAG_PUT and in_index: + in_index = self.index.get(key) + is_index_object = in_index == (segment, offset) + if tag == TAG_PUT and is_index_object: try: new_segment, offset = self.io.write_put(key, data, raise_full=True) except LoggedIO.SegmentFull: @@ -492,22 +493,23 @@ class Repository: segments.setdefault(new_segment, 0) segments[new_segment] += 1 segments[segment] -= 1 - elif tag == TAG_PUT and not in_index: + elif tag == TAG_PUT and not is_index_object: # If this is a PUT shadowed by a later tag, then it will be gone when this segment is deleted after # this loop. Therefore it is removed from the shadow index. try: self.shadow_index[key].remove(segment) except (KeyError, ValueError): pass - elif tag == TAG_DELETE: + elif tag == TAG_DELETE and not in_index: # If the shadow index doesn't contain this key, then we can't say if there's a shadowed older tag, # therefore we do not drop the delete, but write it to a current segment. shadowed_put_exists = key not in self.shadow_index or any( # If the key is in the shadow index and there is any segment with an older PUT of this # key, we have a shadowed put. shadowed < segment for shadowed in self.shadow_index[key]) + delete_is_not_stable = index_transaction_id is None or segment > index_transaction_id - if shadowed_put_exists or index_transaction_id is None or segment > index_transaction_id: + if shadowed_put_exists or delete_is_not_stable: # (introduced in 6425d16aa84be1eaaf88) # This is needed to avoid object un-deletion if we crash between the commit and the deletion # of old segments in complete_xfer(). From 66316e10b91aebf9f1ca0fae0dcd8c0d91b97e09 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 25 Sep 2016 11:53:54 +0200 Subject: [PATCH 0229/1387] Fix indentation in borg help compression --- src/borg/archiver.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index ac39a063..5d55c29a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1313,26 +1313,26 @@ class Archiver: If it is compressible, use the C,[L] compression spec given within the "auto" specifier. If it is not compressible, use no compression. - Examples:: + Examples:: - borg create --compression lz4 REPO::ARCHIVE data - borg create --compression zlib REPO::ARCHIVE data - borg create --compression zlib,1 REPO::ARCHIVE data - borg create --compression auto,lzma,6 REPO::ARCHIVE data - borg create --compression-from compression.conf --compression auto,lzma ... + borg create --compression lz4 REPO::ARCHIVE data + borg create --compression zlib REPO::ARCHIVE data + borg create --compression zlib,1 REPO::ARCHIVE data + borg create --compression auto,lzma,6 REPO::ARCHIVE data + borg create --compression-from compression.conf --compression auto,lzma ... - compression.conf has entries like: + compression.conf has entries like:: - # example config file for --compression-from option - # - # Format of non-comment / non-empty lines: - # : - # compression-spec is same format as for --compression option - # path/filename pattern is same format as for --exclude option - none:*.gz - none:*.zip - none:*.mp3 - none:*.ogg + # example config file for --compression-from option + # + # Format of non-comment / non-empty lines: + # : + # compression-spec is same format as for --compression option + # path/filename pattern is same format as for --exclude option + none:*.gz + none:*.zip + none:*.mp3 + none:*.ogg General remarks: From 29b5136da754b4f5959bc8771f44895b5195a605 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 25 Sep 2016 15:05:39 +0200 Subject: [PATCH 0230/1387] archiver: Move key management commands to new key subcommand. --- borg/archiver.py | 12 ++++++++++-- borg/keymanager.py | 2 +- borg/testsuite/archiver.py | 18 +++++++++--------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 6ef1d0fb..8a07c5c6 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1110,7 +1110,15 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False)) - subparser = subparsers.add_parser('key-export', parents=[common_parser], + subparser = subparsers.add_parser('key', + description="Manage a keyfile or repokey of a repository", + epilog="", + formatter_class=argparse.RawDescriptionHelpFormatter, + help='manage repository key') + + key_parsers = subparser.add_subparsers(title='required arguments', metavar='') + + subparser = key_parsers.add_parser('export', parents=[common_parser], description=self.do_key_export.__doc__, epilog="", formatter_class=argparse.RawDescriptionHelpFormatter, @@ -1124,7 +1132,7 @@ class Archiver: default=False, help='Create an export suitable for printing and later type-in') - subparser = subparsers.add_parser('key-import', parents=[common_parser], + subparser = key_parsers.add_parser('import', parents=[common_parser], description=self.do_key_import.__doc__, epilog="", formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/borg/keymanager.py b/borg/keymanager.py index 244e16c6..8eef581d 100644 --- a/borg/keymanager.py +++ b/borg/keymanager.py @@ -98,7 +98,7 @@ class KeyManager: i += 1 return ret - export = 'To restore key use borg key-import --paper /path/to/repo\n\n' + export = 'To restore key use borg key import --paper /path/to/repo\n\n' binary = a2b_base64(self.keyblob) export += 'BORG PAPER KEY v1\n' diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index d5896c03..8fb9eddf 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1200,7 +1200,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): export_file = self.output_path + '/exported' self.cmd('init', self.repository_location, '--encryption', 'keyfile') repo_id = self._extract_repository_id(self.repository_path) - self.cmd('key-export', self.repository_location, export_file) + self.cmd('key', 'export', self.repository_location, export_file) with open(export_file, 'r') as fd: export_contents = fd.read() @@ -1216,7 +1216,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.unlink(key_file) - self.cmd('key-import', self.repository_location, export_file) + self.cmd('key', 'import', self.repository_location, export_file) with open(key_file, 'r') as fd: key_contents2 = fd.read() @@ -1227,7 +1227,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): export_file = self.output_path + '/exported' self.cmd('init', self.repository_location, '--encryption', 'repokey') repo_id = self._extract_repository_id(self.repository_path) - self.cmd('key-export', self.repository_location, export_file) + self.cmd('key', 'export', self.repository_location, export_file) with open(export_file, 'r') as fd: export_contents = fd.read() @@ -1246,7 +1246,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with Repository(self.repository_path) as repository: repository.save_key(b'') - self.cmd('key-import', self.repository_location, export_file) + self.cmd('key', 'import', self.repository_location, export_file) with Repository(self.repository_path) as repository: repo_key2 = RepoKey(repository) @@ -1258,17 +1258,17 @@ class ArchiverTestCase(ArchiverTestCaseBase): export_file = self.output_path + '/exported' self.cmd('init', self.repository_location, '--encryption', 'keyfile') - self.cmd('key-import', self.repository_location, export_file, exit_code=EXIT_ERROR) + self.cmd('key', 'import', self.repository_location, export_file, exit_code=EXIT_ERROR) with open(export_file, 'w') as fd: fd.write('something not a key\n') - self.assert_raises(NotABorgKeyFile, lambda: self.cmd('key-import', self.repository_location, export_file)) + self.assert_raises(NotABorgKeyFile, lambda: self.cmd('key', 'import', self.repository_location, export_file)) with open(export_file, 'w') as fd: fd.write('BORG_KEY a0a0a0\n') - self.assert_raises(RepoIdMismatch, lambda: self.cmd('key-import', self.repository_location, export_file)) + self.assert_raises(RepoIdMismatch, lambda: self.cmd('key', 'import', self.repository_location, export_file)) def test_key_export_paperkey(self): repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239' @@ -1283,12 +1283,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(KeyfileKey.FILE_ID + ' ' + repo_id + '\n') fd.write(b2a_base64(b'abcdefghijklmnopqrstu').decode()) - self.cmd('key-export', '--paper', self.repository_location, export_file) + self.cmd('key', 'export', '--paper', self.repository_location, export_file) with open(export_file, 'r') as fd: export_contents = fd.read() - assert export_contents == """To restore key use borg key-import --paper /path/to/repo + assert export_contents == """To restore key use borg key import --paper /path/to/repo BORG PAPER KEY v1 id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 From 5c2424831e8a68783e6912ceb854ccb474a7689a Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 25 Sep 2016 15:25:02 +0200 Subject: [PATCH 0231/1387] archiver: Create a subcommmand debug for all debug-* commands The debug commands all should subcommands of a common debug command. This commit adds this command but keeps the old command names for 1.0.x. The plan is to remove them in 1.1.0. --- borg/archiver.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/borg/archiver.py b/borg/archiver.py index 8a07c5c6..02491060 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1585,6 +1585,22 @@ class Archiver: subparser.add_argument('topic', metavar='TOPIC', type=str, nargs='?', help='additional help on TOPIC') + debug_epilog = textwrap.dedent(""" + These commands are not intended for normal use and potentially very + dangerous if used incorrectly. + + They exist to improve debugging capabilities without direct system access, e.g. + in case you ever run into some severe malfunction. Use them only if you know + what you are doing or if a trusted developer tells you what to do.""") + + subparser = subparsers.add_parser('debug', + description='debugging command (not intended for normal use)', + epilog=debug_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='debugging command (not intended for normal use)') + + debug_parsers = subparser.add_subparsers(title='required arguments', metavar='') + debug_info_epilog = textwrap.dedent(""" This command displays some system information that might be useful for bug reports and debugging problems. If a traceback happens, this information is @@ -1597,6 +1613,13 @@ class Archiver: help='show system infos for debugging / bug reports (debug)') subparser.set_defaults(func=self.do_debug_info) + subparser = debug_parsers.add_parser('info', parents=[common_parser], + description=self.do_debug_info.__doc__, + epilog=debug_info_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='show system infos for debugging / bug reports (debug)') + subparser.set_defaults(func=self.do_debug_info) + debug_dump_archive_items_epilog = textwrap.dedent(""" This command dumps raw (but decrypted and decompressed) archive items (only metadata) to files. """) @@ -1610,6 +1633,16 @@ class Archiver: type=location_validator(archive=True), help='archive to dump') + subparser = debug_parsers.add_parser('dump-archive-items', parents=[common_parser], + description=self.do_debug_dump_archive_items.__doc__, + epilog=debug_dump_archive_items_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='dump archive items (metadata) (debug)') + subparser.set_defaults(func=self.do_debug_dump_archive_items) + subparser.add_argument('location', metavar='ARCHIVE', + type=location_validator(archive=True), + help='archive to dump') + debug_dump_repo_objs_epilog = textwrap.dedent(""" This command dumps raw (but decrypted and decompressed) repo objects to files. """) @@ -1623,6 +1656,16 @@ class Archiver: type=location_validator(archive=False), help='repo to dump') + subparser = debug_parsers.add_parser('dump-repo-objs', parents=[common_parser], + description=self.do_debug_dump_repo_objs.__doc__, + epilog=debug_dump_repo_objs_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='dump repo objects (debug)') + subparser.set_defaults(func=self.do_debug_dump_repo_objs) + subparser.add_argument('location', metavar='REPOSITORY', + type=location_validator(archive=False), + help='repo to dump') + debug_get_obj_epilog = textwrap.dedent(""" This command gets an object from the repository. """) @@ -1640,6 +1683,20 @@ class Archiver: subparser.add_argument('path', metavar='PATH', type=str, help='file to write object data into') + subparser = debug_parsers.add_parser('get-obj', parents=[common_parser], + description=self.do_debug_get_obj.__doc__, + epilog=debug_get_obj_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='get object from repository (debug)') + subparser.set_defaults(func=self.do_debug_get_obj) + subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', + type=location_validator(archive=False), + help='repository to use') + subparser.add_argument('id', metavar='ID', type=str, + help='hex object ID to get from the repo') + subparser.add_argument('path', metavar='PATH', type=str, + help='file to write object data into') + debug_put_obj_epilog = textwrap.dedent(""" This command puts objects into the repository. """) @@ -1655,6 +1712,18 @@ class Archiver: subparser.add_argument('paths', metavar='PATH', nargs='+', type=str, help='file(s) to read and create object(s) from') + subparser = debug_parsers.add_parser('put-obj', parents=[common_parser], + description=self.do_debug_put_obj.__doc__, + epilog=debug_put_obj_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='put object to repository (debug)') + subparser.set_defaults(func=self.do_debug_put_obj) + subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', + type=location_validator(archive=False), + help='repository to use') + subparser.add_argument('paths', metavar='PATH', nargs='+', type=str, + help='file(s) to read and create object(s) from') + debug_delete_obj_epilog = textwrap.dedent(""" This command deletes objects from the repository. """) @@ -1669,6 +1738,19 @@ class Archiver: help='repository to use') subparser.add_argument('ids', metavar='IDs', nargs='+', type=str, help='hex object ID(s) to delete from the repo') + + subparser = debug_parsers.add_parser('delete-obj', parents=[common_parser], + description=self.do_debug_delete_obj.__doc__, + epilog=debug_delete_obj_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='delete object from repository (debug)') + subparser.set_defaults(func=self.do_debug_delete_obj) + subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', + type=location_validator(archive=False), + help='repository to use') + subparser.add_argument('ids', metavar='IDs', nargs='+', type=str, + help='hex object ID(s) to delete from the repo') + return parser def get_args(self, argv, cmd): From a11436cfb6e67f114936b32279c27223a611eb91 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 25 Sep 2016 16:08:22 +0200 Subject: [PATCH 0232/1387] setup.py: Add subcommand support to build_usage. --- setup.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index b0103e06..e0a18a47 100644 --- a/setup.py +++ b/setup.py @@ -150,19 +150,33 @@ class build_usage(Command): def run(self): print('generating usage docs') + if not os.path.exists('docs/usage'): + os.mkdir('docs/usage') # allows us to build docs without the C modules fully loaded during help generation from borg.archiver import Archiver parser = Archiver().build_parser(prog='borg') + + self.generate_level("", parser, Archiver) + + def generate_level(self, prefix, parser, Archiver): + is_subcommand = False choices = {} for action in parser._actions: - if action.choices is not None: - choices.update(action.choices) + if action.choices is not None and 'SubParsersAction' in str(action.__class__): + is_subcommand = True + for cmd, parser in action.choices.items(): + choices[prefix + cmd] = parser + if prefix and not choices: + return print('found commands: %s' % list(choices.keys())) - if not os.path.exists('docs/usage'): - os.mkdir('docs/usage') + for command, parser in choices.items(): print('generating help for %s' % command) - with open('docs/usage/%s.rst.inc' % command, 'w') as doc: + + if self.generate_level(command + " ", parser, Archiver): + return + + with open('docs/usage/%s.rst.inc' % command.replace(" ", "_"), 'w') as doc: doc.write(".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n") if command == 'help': for topic in Archiver.helptext: @@ -173,14 +187,16 @@ class build_usage(Command): doc.write(Archiver.helptext[topic]) else: params = {"command": command, + "command_": command.replace(' ', '_'), "underline": '-' * len('borg ' + command)} - doc.write(".. _borg_{command}:\n\n".format(**params)) + doc.write(".. _borg_{command_}:\n\n".format(**params)) doc.write("borg {command}\n{underline}\n::\n\n".format(**params)) epilog = parser.epilog parser.epilog = None doc.write(re.sub("^", " ", parser.format_help(), flags=re.M)) doc.write("\nDescription\n~~~~~~~~~~~\n") doc.write(epilog) + return is_subcommand class build_api(Command): From d9b880fdf32a0ec2463db27e6b650e73d59df12b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 23 Sep 2016 23:43:16 +0200 Subject: [PATCH 0233/1387] fix signal handling, fixes #1620 use context manager for signal handler installation / restoration - this includes the special case of installing handler SIG_IGN to ignore a signal and restoring the original (non-ignoring) handler. use SIG_IGN to avoid a 2nd signal interrupts the handling of the 1st signal. --- borg/archiver.py | 180 +++++++++++++++++++++-------------------------- borg/helpers.py | 48 +++++++++++++ 2 files changed, 128 insertions(+), 100 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 6ef1d0fb..73402e98 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -23,6 +23,7 @@ from .helpers import Error, location_validator, archivename_validator, format_li Manifest, NoManifestError, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \ dir_is_tagged, bigint_to_int, ChunkerParams, CompressionSpec, PrefixSpec, is_slow_msgpack, yes, sysinfo, \ EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, log_multi, PatternMatcher, ErrorIgnoringTextIOWrapper +from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm from .logger import create_logger, setup_logging logger = create_logger() from .compress import Compressor @@ -1697,59 +1698,28 @@ class Archiver: return args.func(args) -def sig_info_handler(signum, stack): # pragma: no cover +def sig_info_handler(sig_no, stack): # pragma: no cover """search the stack for infos about the currently processed file and print them""" - for frame in inspect.getouterframes(stack): - func, loc = frame[3], frame[0].f_locals - if func in ('process_file', '_process', ): # create op - path = loc['path'] - try: - pos = loc['fd'].tell() - total = loc['st'].st_size - except Exception: - pos, total = 0, 0 - logger.info("{0} {1}/{2}".format(path, format_file_size(pos), format_file_size(total))) - break - if func in ('extract_item', ): # extract op - path = loc['item'][b'path'] - try: - pos = loc['fd'].tell() - except Exception: - pos = 0 - logger.info("{0} {1}/???".format(path, format_file_size(pos))) - break - - -class SIGTERMReceived(BaseException): - pass - - -def sig_term_handler(signum, stack): - raise SIGTERMReceived - - -class SIGHUPReceived(BaseException): - pass - - -def sig_hup_handler(signum, stack): - raise SIGHUPReceived - - -def setup_signal_handlers(): # pragma: no cover - sigs = [] - if hasattr(signal, 'SIGUSR1'): - sigs.append(signal.SIGUSR1) # kill -USR1 pid - if hasattr(signal, 'SIGINFO'): - sigs.append(signal.SIGINFO) # kill -INFO pid (or ctrl-t) - for sig in sigs: - signal.signal(sig, sig_info_handler) - # If we received SIGTERM or SIGHUP, catch them and raise a proper exception - # that can be handled for an orderly exit. SIGHUP is important especially - # for systemd systems, where logind sends it when a session exits, in - # addition to any traditional use. - signal.signal(signal.SIGTERM, sig_term_handler) - signal.signal(signal.SIGHUP, sig_hup_handler) + with signal_handler(sig_no, signal.SIG_IGN): + for frame in inspect.getouterframes(stack): + func, loc = frame[3], frame[0].f_locals + if func in ('process_file', '_process', ): # create op + path = loc['path'] + try: + pos = loc['fd'].tell() + total = loc['st'].st_size + except Exception: + pos, total = 0, 0 + logger.info("{0} {1}/{2}".format(path, format_file_size(pos), format_file_size(total))) + break + if func in ('extract_item', ): # extract op + path = loc['item'][b'path'] + try: + pos = loc['fd'].tell() + except Exception: + pos = 0 + logger.info("{0} {1}/???".format(path, format_file_size(pos))) + break def main(): # pragma: no cover @@ -1757,54 +1727,64 @@ def main(): # pragma: no cover # issues when print()-ing unicode file names sys.stdout = ErrorIgnoringTextIOWrapper(sys.stdout.buffer, sys.stdout.encoding, 'replace', line_buffering=True) sys.stderr = ErrorIgnoringTextIOWrapper(sys.stderr.buffer, sys.stderr.encoding, 'replace', line_buffering=True) - setup_signal_handlers() - archiver = Archiver() - msg = None - try: - args = archiver.get_args(sys.argv, os.environ.get('SSH_ORIGINAL_COMMAND')) - except Error as e: - msg = e.get_message() - if e.traceback: - msg += "\n%s\n%s" % (traceback.format_exc(), sysinfo()) - # we might not have logging setup yet, so get out quickly - print(msg, file=sys.stderr) - sys.exit(e.exit_code) - try: - exit_code = archiver.run(args) - except Error as e: - msg = e.get_message() - if e.traceback: - msg += "\n%s\n%s" % (traceback.format_exc(), sysinfo()) - exit_code = e.exit_code - except RemoteRepository.RPCError as e: - msg = '%s\n%s' % (str(e), sysinfo()) - exit_code = EXIT_ERROR - except Exception: - msg = 'Local Exception.\n%s\n%s' % (traceback.format_exc(), sysinfo()) - exit_code = EXIT_ERROR - except KeyboardInterrupt: - msg = 'Keyboard interrupt.\n%s\n%s' % (traceback.format_exc(), sysinfo()) - exit_code = EXIT_ERROR - except SIGTERMReceived: - msg = 'Received SIGTERM.' - exit_code = EXIT_ERROR - except SIGHUPReceived: - msg = 'Received SIGHUP.' - exit_code = EXIT_ERROR - if msg: - logger.error(msg) - if args.show_rc: - exit_msg = 'terminating with %s status, rc %d' - if exit_code == EXIT_SUCCESS: - logger.info(exit_msg % ('success', exit_code)) - elif exit_code == EXIT_WARNING: - logger.warning(exit_msg % ('warning', exit_code)) - elif exit_code == EXIT_ERROR: - logger.error(exit_msg % ('error', exit_code)) - else: - # if you see 666 in output, it usually means exit_code was None - logger.error(exit_msg % ('abnormal', exit_code or 666)) - sys.exit(exit_code) + # If we receive SIGINT (ctrl-c), SIGTERM (kill) or SIGHUP (kill -HUP), + # catch them and raise a proper exception that can be handled for an + # orderly exit. + # SIGHUP is important especially for systemd systems, where logind + # sends it when a session exits, in addition to any traditional use. + # Output some info if we receive SIGUSR1 or SIGINFO (ctrl-t). + with signal_handler('SIGINT', raising_signal_handler(KeyboardInterrupt)), \ + signal_handler('SIGHUP', raising_signal_handler(SigHup)), \ + signal_handler('SIGTERM', raising_signal_handler(SigTerm)), \ + signal_handler('SIGUSR1', sig_info_handler), \ + signal_handler('SIGINFO', sig_info_handler): + archiver = Archiver() + msg = None + try: + args = archiver.get_args(sys.argv, os.environ.get('SSH_ORIGINAL_COMMAND')) + except Error as e: + msg = e.get_message() + if e.traceback: + msg += "\n%s\n%s" % (traceback.format_exc(), sysinfo()) + # we might not have logging setup yet, so get out quickly + print(msg, file=sys.stderr) + sys.exit(e.exit_code) + try: + exit_code = archiver.run(args) + except Error as e: + msg = e.get_message() + if e.traceback: + msg += "\n%s\n%s" % (traceback.format_exc(), sysinfo()) + exit_code = e.exit_code + except RemoteRepository.RPCError as e: + msg = '%s\n%s' % (str(e), sysinfo()) + exit_code = EXIT_ERROR + except Exception: + msg = 'Local Exception.\n%s\n%s' % (traceback.format_exc(), sysinfo()) + exit_code = EXIT_ERROR + except KeyboardInterrupt: + msg = 'Keyboard interrupt.\n%s\n%s' % (traceback.format_exc(), sysinfo()) + exit_code = EXIT_ERROR + except SigTerm: + msg = 'Received SIGTERM.' + exit_code = EXIT_ERROR + except SigHup: + msg = 'Received SIGHUP.' + exit_code = EXIT_ERROR + if msg: + logger.error(msg) + if args.show_rc: + exit_msg = 'terminating with %s status, rc %d' + if exit_code == EXIT_SUCCESS: + logger.info(exit_msg % ('success', exit_code)) + elif exit_code == EXIT_WARNING: + logger.warning(exit_msg % ('warning', exit_code)) + elif exit_code == EXIT_ERROR: + logger.error(exit_msg % ('error', exit_code)) + else: + # if you see 666 in output, it usually means exit_code was None + logger.error(exit_msg % ('abnormal', exit_code or 666)) + sys.exit(exit_code) if __name__ == '__main__': diff --git a/borg/helpers.py b/borg/helpers.py index 24499c77..27555a4e 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -1,5 +1,6 @@ import argparse from collections import namedtuple +import contextlib from functools import wraps import grp import os @@ -10,6 +11,7 @@ import re from shutil import get_terminal_size import sys import platform +import signal import threading import time import unicodedata @@ -1160,3 +1162,49 @@ class ErrorIgnoringTextIOWrapper(io.TextIOWrapper): except OSError: pass return len(s) + + +class SignalException(BaseException): + """base class for all signal-based exceptions""" + + +class SigHup(SignalException): + """raised on SIGHUP signal""" + + +class SigTerm(SignalException): + """raised on SIGTERM signal""" + + +@contextlib.contextmanager +def signal_handler(sig, handler): + """ + when entering context, set up signal handler for signal . + when leaving context, restore original signal handler. + + can bei either a str when giving a signal.SIGXXX attribute name (it + won't crash if the attribute name does not exist as some names are platform + specific) or a int, when giving a signal number. + + is any handler value as accepted by the signal.signal(sig, handler). + """ + if isinstance(sig, str): + sig = getattr(signal, sig, None) + if sig is not None: + orig_handler = signal.signal(sig, handler) + try: + yield + finally: + if sig is not None: + signal.signal(sig, orig_handler) + + +def raising_signal_handler(exc_cls): + def handler(sig_no, frame): + # setting SIG_IGN avoids that an incoming second signal of this + # kind would raise a 2nd exception while we still process the + # exception handler for exc_cls for the 1st signal. + signal.signal(sig_no, signal.SIG_IGN) + raise exc_cls + + return handler From 4f9f25db026be595ea43d3dceef7397c908fd1ab Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 25 Sep 2016 16:33:30 +0200 Subject: [PATCH 0234/1387] development.rst: Add sphinx_rtd_theme to the sphinx install command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s used by default, so install it as well. --- docs/development.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development.rst b/docs/development.rst index 3e89e34c..513bb4ec 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -147,7 +147,7 @@ The documentation (in reStructuredText format, .rst) is in docs/. To build the html version of it, you need to have sphinx installed:: - pip3 install sphinx # important: this will install sphinx with Python 3 + pip3 install sphinx sphinx_rtd_theme # important: this will install sphinx with Python 3 Now run:: From 8164524d990ad13b6713a2c28e7f9e75f884c0a0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 25 Sep 2016 15:37:50 +0200 Subject: [PATCH 0235/1387] Fix broken --progress for double-cell paths --- src/borg/archive.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index b79fc5f9..6cc7aacc 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -95,7 +95,8 @@ class Statistics: if space >= 8: if space < swidth('...') + swidth(path): path = '%s...%s' % (path[:(space // 2) - swidth('...')], path[-space // 2:]) - msg += "{0:<{space}}".format(path, space=space) + space -= swidth(path) + msg += path + ' ' * space else: msg = ' ' * columns print(msg, file=stream or sys.stderr, end="\r", flush=True) From bf681e98ce3372163c2cd3c6f62ede47b0892633 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 25 Sep 2016 22:50:08 +0200 Subject: [PATCH 0236/1387] Re-Indent borg help helptexts, again. --- src/borg/archiver.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f2a5383c..8d13dbf2 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1287,7 +1287,7 @@ class Archiver: The version of borg. - Examples:: + Examples:: borg create /path/to/repo::{hostname}-{user}-{utcnow} ... borg create /path/to/repo::{hostname}-{now:%Y-%m-%d_%H:%M:%S} ... @@ -1349,24 +1349,24 @@ class Archiver: Examples:: - borg create --compression lz4 REPO::ARCHIVE data - borg create --compression zlib REPO::ARCHIVE data - borg create --compression zlib,1 REPO::ARCHIVE data - borg create --compression auto,lzma,6 REPO::ARCHIVE data - borg create --compression-from compression.conf --compression auto,lzma ... + borg create --compression lz4 REPO::ARCHIVE data + borg create --compression zlib REPO::ARCHIVE data + borg create --compression zlib,1 REPO::ARCHIVE data + borg create --compression auto,lzma,6 REPO::ARCHIVE data + borg create --compression-from compression.conf --compression auto,lzma ... compression.conf has entries like:: - # example config file for --compression-from option - # - # Format of non-comment / non-empty lines: - # : - # compression-spec is same format as for --compression option - # path/filename pattern is same format as for --exclude option - none:*.gz - none:*.zip - none:*.mp3 - none:*.ogg + # example config file for --compression-from option + # + # Format of non-comment / non-empty lines: + # : + # compression-spec is same format as for --compression option + # path/filename pattern is same format as for --exclude option + none:*.gz + none:*.zip + none:*.mp3 + none:*.ogg General remarks: From 60c5482e6ba12ac68892b89ae2ccb23db7cae534 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 26 Sep 2016 04:34:25 +0200 Subject: [PATCH 0237/1387] fix closed FD issue, fixes #1551 --- borg/repository.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/borg/repository.py b/borg/repository.py index 71e9040a..fae22119 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -705,6 +705,13 @@ class LoggedIO: else: yield tag, key, offset offset += size + # we must get the fd via get_fd() here again as we yielded to our caller and it might + # have triggered closing of the fd we had before (e.g. by calling io.read() for + # different segment(s)). + # by calling get_fd() here again we also make our fd "recently used" so it likely + # does not get kicked out of self.fds LRUcache. + fd = self.get_fd(segment) + fd.seek(offset) header = fd.read(self.header_fmt.size) def recover_segment(self, segment, filename): From 7c2025a2c0add837b7947c422de5003d5b696d81 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Mon, 26 Sep 2016 19:28:00 +0200 Subject: [PATCH 0238/1387] testsuite/archiver.py: Add a comment how to easily test ArchiverTestCaseBinary locally. --- borg/testsuite/archiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 8fb9eddf..6c74cca0 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -69,7 +69,7 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw): sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr -# check if the binary "borg.exe" is available +# check if the binary "borg.exe" is available (for local testing a symlink to virtualenv/bin/borg should do) try: exec_cmd('help', exe='borg.exe', fork=True) BORG_EXES = ['python', 'binary', ] From 15444b19d1507fabf46291f7b216d883e3743f1f Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Mon, 26 Sep 2016 19:28:28 +0200 Subject: [PATCH 0239/1387] testsuite/archiver.py: Fix key import failure with ArchiverTestCaseBinary --- borg/testsuite/archiver.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 6c74cca0..e7f805ed 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1263,12 +1263,18 @@ class ArchiverTestCase(ArchiverTestCaseBase): with open(export_file, 'w') as fd: fd.write('something not a key\n') - self.assert_raises(NotABorgKeyFile, lambda: self.cmd('key', 'import', self.repository_location, export_file)) + if self.FORK_DEFAULT: + self.cmd('key', 'import', self.repository_location, export_file, exit_code=2) + else: + self.assert_raises(NotABorgKeyFile, lambda: self.cmd('key', 'import', self.repository_location, export_file)) with open(export_file, 'w') as fd: fd.write('BORG_KEY a0a0a0\n') - self.assert_raises(RepoIdMismatch, lambda: self.cmd('key', 'import', self.repository_location, export_file)) + if self.FORK_DEFAULT: + self.cmd('key', 'import', self.repository_location, export_file, exit_code=2) + else: + self.assert_raises(RepoIdMismatch, lambda: self.cmd('key', 'import', self.repository_location, export_file)) def test_key_export_paperkey(self): repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239' From bb6c0cd2acc18c57735f81f27185eb37def90458 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 26 Sep 2016 20:08:04 +0200 Subject: [PATCH 0240/1387] vagrant: update FUSE for macOS --- Vagrantfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 8316ec2f..f00b8c14 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -61,9 +61,9 @@ def packages_darwin # install all the (security and other) updates sudo softwareupdate --install --all # get osxfuse 3.x pre-release code from github: - curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.4.1/osxfuse-3.4.1.dmg >osxfuse.dmg + curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.5.1/osxfuse-3.5.1.dmg >osxfuse.dmg MOUNTDIR=$(echo `hdiutil mount osxfuse.dmg | tail -1 | awk '{$1="" ; print $0}'` | xargs -0 echo) \ - && sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for macOS 3.4.1.pkg" -target / + && sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for macOS 3.5.1.pkg" -target / sudo chown -R vagrant /usr/local # brew must be able to create stuff here ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew update From 9cef0a9ed84c9dba0c1e4ffe528c0c1a9519d0ea Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 27 Sep 2016 11:35:45 +0200 Subject: [PATCH 0241/1387] Fix broken --progress ellipsis for double-cell paths --- src/borg/archive.py | 5 +++-- src/borg/helpers.py | 26 ++++++++++++++++++++++++++ src/borg/testsuite/helpers.py | 22 ++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 6cc7aacc..dd5eba21 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -28,7 +28,7 @@ from .helpers import Error, IntegrityError from .helpers import uid2user, user2uid, gid2group, group2gid from .helpers import parse_timestamp, to_localtime from .helpers import format_time, format_timedelta, format_file_size, file_status -from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates +from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates, swidth_slice from .helpers import decode_dict, StableDict from .helpers import int_to_bigint, bigint_to_int, bin_to_hex from .helpers import ProgressIndicatorPercent, log_multi @@ -94,7 +94,8 @@ class Statistics: space = columns - swidth(msg) if space >= 8: if space < swidth('...') + swidth(path): - path = '%s...%s' % (path[:(space // 2) - swidth('...')], path[-space // 2:]) + path = '%s...%s' % (swidth_slice(path, space // 2 - swidth('...')), + swidth_slice(path, -space // 2)) space -= swidth(path) msg += path + ' ' * space else: diff --git a/src/borg/helpers.py b/src/borg/helpers.py index e4456953..d08c6ba7 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1696,3 +1696,29 @@ class ErrorIgnoringTextIOWrapper(io.TextIOWrapper): except OSError: pass return len(s) + + +def swidth_slice(string, max_width): + """ + Return a slice of *max_width* cells from *string*. + + Negative *max_width* means from the end of string. + + *max_width* is in units of character cells (or "columns"). + Latin characters are usually one cell wide, many CJK characters are two cells wide. + """ + from .platform import swidth + reverse = max_width < 0 + max_width = abs(max_width) + if reverse: + string = reversed(string) + current_swidth = 0 + result = [] + for character in string: + current_swidth += swidth(character) + if current_swidth > max_width: + break + result.append(character) + if reverse: + result.reverse() + return ''.join(result) diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index d1c7c95b..b2b568d7 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -9,6 +9,7 @@ import pytest import msgpack import msgpack.fallback +from .. import platform from ..helpers import Location from ..helpers import Buffer from ..helpers import partial_format, format_file_size, parse_file_size, format_timedelta, format_line, PlaceholderError, replace_placeholders @@ -23,6 +24,7 @@ from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless from ..helpers import load_excludes from ..helpers import CompressionSpec, CompressionDecider1, CompressionDecider2 from ..helpers import parse_pattern, PatternMatcher, RegexPattern, PathPrefixPattern, FnmatchPattern, ShellPattern +from ..helpers import swidth_slice from . import BaseTestCase, environment_variable, FakeInputs @@ -1041,3 +1043,23 @@ def test_replace_placeholders(): now = datetime.now() assert " " not in replace_placeholders('{now}') assert int(replace_placeholders('{now:%Y}')) == now.year + + +def working_swidth(): + return platform.swidth('선') == 2 + + +@pytest.mark.skipif(not working_swidth(), reason='swidth() is not supported / active') +def test_swidth_slice(): + string = '나윤선나윤선나윤선나윤선나윤선' + assert swidth_slice(string, 1) == '' + assert swidth_slice(string, -1) == '' + assert swidth_slice(string, 4) == '나윤' + assert swidth_slice(string, -4) == '윤선' + + +@pytest.mark.skipif(not working_swidth(), reason='swidth() is not supported / active') +def test_swidth_slice_mixed_characters(): + string = '나윤a선나윤선나윤선나윤선나윤선' + assert swidth_slice(string, 5) == '나윤a' + assert swidth_slice(string, 6) == '나윤a' From b84014e7d9a7ba92224e3ffb1418e195cda0c9c5 Mon Sep 17 00:00:00 2001 From: textshell Date: Tue, 27 Sep 2016 15:24:31 +0200 Subject: [PATCH 0242/1387] archiver: Add documentation for "key export" and "key import" commands. (#1641) archiver: Add documentation for "key export" and "key import" commands --- borg/archiver.py | 31 +++++++++++++++++-- docs/quickstart.rst | 9 ++++-- docs/usage.rst | 6 ++++ docs/usage/key_export.rst.inc | 57 +++++++++++++++++++++++++++++++++++ docs/usage/key_import.rst.inc | 45 +++++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 docs/usage/key_export.rst.inc create mode 100644 docs/usage/key_import.rst.inc diff --git a/borg/archiver.py b/borg/archiver.py index 3c4682e5..ce7655fc 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1120,9 +1120,28 @@ class Archiver: key_parsers = subparser.add_subparsers(title='required arguments', metavar='') + key_export_epilog = textwrap.dedent(""" + If repository encryption is used, the repository is inaccessible + without the key. This command allows to backup this essential key. + + There are two backup formats. The normal backup format is suitable for + digital storage as a file. The ``--paper`` backup format is optimized + for printing and typing in while importing, with per line checks to + reduce problems with manual input. + + For repositories using keyfile encryption the key is saved locally + on the system that is capable of doing backups. To guard against loss + of this key, the key needs to be backed up independently of the main + data backup. + + For repositories using the repokey encryption the key is saved in the + repository in the config file. A backup is thus not strictly needed, + but guards against the repository becoming inaccessible if the file + is damaged for some reason. + """) subparser = key_parsers.add_parser('export', parents=[common_parser], description=self.do_key_export.__doc__, - epilog="", + epilog=key_export_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, help='export repository key for backup') subparser.set_defaults(func=self.do_key_export) @@ -1134,9 +1153,17 @@ class Archiver: default=False, help='Create an export suitable for printing and later type-in') + key_import_epilog = textwrap.dedent(""" + This command allows to restore a key previously backed up with the + export command. + + If the ``--paper`` option is given, the import will be an interactive + process in which each line is checked for plausibility before + proceeding to the next line. For this format PATH must not be given. + """) subparser = key_parsers.add_parser('import', parents=[common_parser], description=self.do_key_import.__doc__, - epilog="", + epilog=key_import_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, help='import repository key from backup') subparser.set_defaults(func=self.do_key_import) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 20cd32d1..f7105d6a 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -228,8 +228,13 @@ For automated backups the passphrase can be specified using the the key in case it gets corrupted or lost. Also keep your passphrase at a safe place. - The backup that is encrypted with that key/passphrase won't help you - with that, of course. + You can make backups using :ref:`borg_key_export` subcommand. + + If you want to print a backup of your key to paper use the ``--paper`` + option of this command and print the result. + + A backup inside of the backup that is encrypted with that key/passphrase + won't help you with that, of course. .. _remote_repos: diff --git a/docs/usage.rst b/docs/usage.rst index 89a9e3cc..a3015f61 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -484,6 +484,12 @@ Examples $ fusermount -u /tmp/mymountpoint +.. include:: usage/key_export.rst.inc + + +.. include:: usage/key_import.rst.inc + + .. include:: usage/change-passphrase.rst.inc Examples diff --git a/docs/usage/key_export.rst.inc b/docs/usage/key_export.rst.inc new file mode 100644 index 00000000..47e9e119 --- /dev/null +++ b/docs/usage/key_export.rst.inc @@ -0,0 +1,57 @@ +.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! + +.. _borg_key_export: + +borg key export +--------------- +:: + + usage: borg key export [-h] [--critical] [--error] [--warning] [--info] + [--debug] [--lock-wait N] [--show-rc] + [--no-files-cache] [--umask M] [--remote-path PATH] + [--paper] + [REPOSITORY] [PATH] + + Export the repository key for backup + + positional arguments: + REPOSITORY + PATH where to store the backup + + optional arguments: + -h, --help show this help message and exit + --critical work on log level CRITICAL + --error work on log level ERROR + --warning work on log level WARNING (default) + --info, -v, --verbose + work on log level INFO + --debug work on log level DEBUG + --lock-wait N wait for the lock, but max. N seconds (default: 1). + --show-rc show/log the return code (rc) + --no-files-cache do not load/update the file metadata cache used to + detect unchanged files + --umask M set umask to M (local and remote, default: 0077) + --remote-path PATH set remote path to executable (default: "borg") + --paper Create an export suitable for printing and later type- + in + +Description +~~~~~~~~~~~ + +If repository encryption is used, the repository is inaccessible +without the key. This command allows to backup this essential key. + +There are two backup formats. The normal backup format is suitable for +digital storage as a file. The ``--paper`` backup format is optimized for +print out and later type-in, with per line checks to reduce problems +with manual input. + +For repositories using keyfile encryption the key is saved locally +on the system that is capable of doing backups. To guard against loss +of this key the key needs to be backed up independent of the main data +backup. + +For repositories using the repokey encryption the key is saved in the +repository in the config file. A backup is thus not strictly needed, +but guards against the repository becoming inaccessible if the file +is damaged for some reason. diff --git a/docs/usage/key_import.rst.inc b/docs/usage/key_import.rst.inc new file mode 100644 index 00000000..71a8eed4 --- /dev/null +++ b/docs/usage/key_import.rst.inc @@ -0,0 +1,45 @@ +.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! + +.. _borg_key_import: + +borg key import +--------------- +:: + + usage: borg key import [-h] [--critical] [--error] [--warning] [--info] + [--debug] [--lock-wait N] [--show-rc] + [--no-files-cache] [--umask M] [--remote-path PATH] + [--paper] + [REPOSITORY] [PATH] + + Import the repository key from backup + + positional arguments: + REPOSITORY + PATH path to the backup + + optional arguments: + -h, --help show this help message and exit + --critical work on log level CRITICAL + --error work on log level ERROR + --warning work on log level WARNING (default) + --info, -v, --verbose + work on log level INFO + --debug work on log level DEBUG + --lock-wait N wait for the lock, but max. N seconds (default: 1). + --show-rc show/log the return code (rc) + --no-files-cache do not load/update the file metadata cache used to + detect unchanged files + --umask M set umask to M (local and remote, default: 0077) + --remote-path PATH set remote path to executable (default: "borg") + --paper interactively import from a backup done with --paper + +Description +~~~~~~~~~~~ + +This command allows to restore a key previously backed up with the +export command. + +If the ``--paper`` option is given, the import will be an interactive +process in which each line is checked for plausibility before +proceeding to the next line. For this format PATH must not be given. From fe9816d8d4db6cfd05805401f5f89ab71594a0f5 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Tue, 27 Sep 2016 22:01:36 +0200 Subject: [PATCH 0243/1387] setup.py: Fix build_usage to always process all commands. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e0a18a47..8ba33e73 100644 --- a/setup.py +++ b/setup.py @@ -174,7 +174,7 @@ class build_usage(Command): print('generating help for %s' % command) if self.generate_level(command + " ", parser, Archiver): - return + break with open('docs/usage/%s.rst.inc' % command.replace(" ", "_"), 'w') as doc: doc.write(".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n") From 4838b9e11039a849188665844d29186a1c16277c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 27 Sep 2016 22:43:11 +0200 Subject: [PATCH 0244/1387] vagrant: upgrade osxfuse to 3.5.2 --- Vagrantfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index f00b8c14..ef38aa94 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -60,10 +60,10 @@ def packages_darwin return <<-EOF # install all the (security and other) updates sudo softwareupdate --install --all - # get osxfuse 3.x pre-release code from github: - curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.5.1/osxfuse-3.5.1.dmg >osxfuse.dmg + # get osxfuse 3.x release code from github: + curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.5.2/osxfuse-3.5.2.dmg >osxfuse.dmg MOUNTDIR=$(echo `hdiutil mount osxfuse.dmg | tail -1 | awk '{$1="" ; print $0}'` | xargs -0 echo) \ - && sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for macOS 3.5.1.pkg" -target / + && sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for macOS 3.5.2.pkg" -target / sudo chown -R vagrant /usr/local # brew must be able to create stuff here ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew update From 8fd0e07a1c4990e98e9e3fb7fa98028f0dad4b5d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Sep 2016 04:41:59 +0200 Subject: [PATCH 0245/1387] hashindex: fix iterator implementation NSKeyIterator and ChunkKeyIterator raised StopIteration once only when they reached their end. But they did not raise StopIteration if one called next() again after they were exhausted, so they did not comply to the standard iterator protocol. AFAIK, this did not cause actual problems due to the way these iterators are used, but when I tried to use itertools.islice() to get n-long sequences from these iterators, it failed / went into an endless loop. --- borg/hashindex.pyx | 10 ++++++++++ borg/testsuite/hashindex.py | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/borg/hashindex.pyx b/borg/hashindex.pyx index c32c4dd1..09ec8961 100644 --- a/borg/hashindex.pyx +++ b/borg/hashindex.pyx @@ -168,17 +168,22 @@ cdef class NSKeyIterator: cdef HashIndex *index cdef const void *key cdef int key_size + cdef int exhausted def __cinit__(self, key_size): self.key = NULL self.key_size = key_size + self.exhausted = 0 def __iter__(self): return self def __next__(self): + if self.exhausted: + raise StopIteration self.key = hashindex_next_key(self.index, self.key) if not self.key: + self.exhausted = 1 raise StopIteration cdef uint32_t *value = (self.key + self.key_size) cdef uint32_t segment = _le32toh(value[0]) @@ -330,17 +335,22 @@ cdef class ChunkKeyIterator: cdef HashIndex *index cdef const void *key cdef int key_size + cdef int exhausted def __cinit__(self, key_size): self.key = NULL self.key_size = key_size + self.exhausted = 0 def __iter__(self): return self def __next__(self): + if self.exhausted: + raise StopIteration self.key = hashindex_next_key(self.index, self.key) if not self.key: + self.exhausted = 1 raise StopIteration cdef uint32_t *value = (self.key + self.key_size) cdef uint32_t refcount = _le32toh(value[0]) diff --git a/borg/testsuite/hashindex.py b/borg/testsuite/hashindex.py index 629ae4e5..5fd9e304 100644 --- a/borg/testsuite/hashindex.py +++ b/borg/testsuite/hashindex.py @@ -83,8 +83,11 @@ class HashIndexTestCase(BaseTestCase): idx = NSIndex() for x in range(100): idx[H(x)] = x, x - all = list(idx.iteritems()) + iterator = idx.iteritems() + all = list(iterator) self.assert_equal(len(all), 100) + # iterator is already exhausted by list(): + self.assert_raises(StopIteration, next, iterator) second_half = list(idx.iteritems(marker=all[49][0])) self.assert_equal(len(second_half), 50) self.assert_equal(second_half, all[50:]) From e124f3c67bbaada0c422f16ba7ca272a9f359253 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Sep 2016 17:01:32 +0200 Subject: [PATCH 0246/1387] update CHANGES --- docs/changes.rst | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index debf4feb..b1c8a1ba 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,6 +50,73 @@ The best check that everything is ok is to run a dry-run extraction:: borg extract -v --dry-run REPO::ARCHIVE +Version 1.0.8rc1 (not released yet) +----------------------------------- + +Bug fixes: + +- fix signal handling (SIGINT, SIGTERM, SIGHUP), #1620 #1593 + Fixes e.g. leftover lock files for quickly repeated signals (e.g. Ctrl-C + Ctrl-C) or lost connections or systemd sending SIGHUP. +- progress display: adapt formatting to narrow screens, do not crash, #1628 +- borg create --read-special - fix crash on broken symlink, #1584. + also correctly processes broken symlinks. before this regressed to a crash + (5b45385) a broken symlink would've been skipped. +- process_symlink: fix missing backup_io() + Fixes a chmod/chown/chgrp/unlink/rename/... crash race between getting dirents + and dispatching to process_symlink. +- yes(): abort on wrong answers, saying so +- fixed exception borg serve raised when connection was closed before reposiory + was openend. add an error message for this. +- fix read-from-closed-FD issue, #1551 + (this seems not to get triggered in 1.0.x, but was discovered in master) +- hashindex: fix iterators (always raise StopIteration when exhausted) + (this seems not to get triggered in 1.0.x, but was discovered in master) + +New features: + +- add "borg key export" / "borg key import" commands, #1555, so users are able + to backup / restore their encryption keys more easily. + + Supported formats are the keyfile format used by borg internally and a + special "paper" format with by line checksums for printed backups. For the + paper format, the import is an interactive process which checks each line as + soon as it is input. + +Other changes: + +- add "borg debug ..." subcommands + (borg debug-* still works, but will be removed in borg 1.1) +- setup.py: Add subcommand support to build_usage. +- remote: change exception message for unexpected RPC data format to indicate + dataflow direction. +- vagrant: + + - upgrade OSXfuse / FUSE for macOS to 3.5.2 + - update Debian Wheezy boxes to 7.11 +- docs: + + - add docs for "key export" and "key import" commands, #1641 + - fix inconsistency in FAQ (pv-wrapper). + - fix second block in "Easy to use" section not showing on GitHub, #1576 + - add bestpractices badge + - link reference docs and faq about BORG_FILES_CACHE_TTL, #1561 + - improve borg info --help, explain size infos, #1532 + - add release signing key / security contact to README, #1560 + - add contribution guidelines for developers + - development.rst: add sphinx_rtd_theme to the sphinx install command + - adjust border color in borg.css + - add debug-info usage help file + - internals.rst: fix typos + - setup.py: fix build_usage to always process all commands +- tests: + + - work around fuse xattr test issue with recent fakeroot + - simplify repo/hashindex tests + - travis: test fuse-enabled borg, use trusty to have a recent FUSE + - re-enable fuse tests for RemoteArchiver (no deadlocks any more) + + Version 1.0.7 (2016-08-19) -------------------------- From 19eb75984e54501f94ad4bab0a1bb3d9bf278db8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 16 Sep 2016 02:49:54 +0200 Subject: [PATCH 0247/1387] borg check --verify-data tuning --- src/borg/archive.py | 47 +++++++++++++++++++++-------------- src/borg/helpers.py | 11 ++++++++ src/borg/testsuite/helpers.py | 18 ++++++++++++++ 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index dd5eba21..16ab75e3 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -33,7 +33,7 @@ from .helpers import decode_dict, StableDict from .helpers import int_to_bigint, bigint_to_int, bin_to_hex from .helpers import ProgressIndicatorPercent, log_multi from .helpers import PathPrefixPattern, FnmatchPattern -from .helpers import consume +from .helpers import consume, chunkit from .helpers import CompressionDecider1, CompressionDecider2, CompressionSpec from .item import Item, ArchiveItem from .key import key_factory @@ -1045,23 +1045,34 @@ class ArchiveChecker: errors = 0 defect_chunks = [] pi = ProgressIndicatorPercent(total=count, msg="Verifying data %6.2f%%", step=0.01) - for chunk_id, (refcount, *_) in self.chunks.iteritems(): - pi.show() - try: - encrypted_data = self.repository.get(chunk_id) - except Repository.ObjectNotFound: - self.error_found = True - errors += 1 - logger.error('chunk %s not found', bin_to_hex(chunk_id)) - continue - try: - _chunk_id = None if chunk_id == Manifest.MANIFEST_ID else chunk_id - _, data = self.key.decrypt(_chunk_id, encrypted_data) - except IntegrityError as integrity_error: - self.error_found = True - errors += 1 - logger.error('chunk %s, integrity error: %s', bin_to_hex(chunk_id), integrity_error) - defect_chunks.append(chunk_id) + for chunk_infos in chunkit(self.chunks.iteritems(), 100): + chunk_ids = [chunk_id for chunk_id, _ in chunk_infos] + chunk_data_iter = self.repository.get_many(chunk_ids) + chunk_ids_revd = list(reversed(chunk_ids)) + while chunk_ids_revd: + pi.show() + chunk_id = chunk_ids_revd.pop(-1) # better efficiency + try: + encrypted_data = next(chunk_data_iter) + except (Repository.ObjectNotFound, IntegrityError) as err: + self.error_found = True + errors += 1 + logger.error('chunk %s: %s', bin_to_hex(chunk_id), err) + if isinstance(err, IntegrityError): + defect_chunks.append(chunk_id) + # as the exception killed our generator, make a new one for remaining chunks: + if chunk_ids_revd: + chunk_ids = list(reversed(chunk_ids_revd)) + chunk_data_iter = self.repository.get_many(chunk_ids) + else: + try: + _chunk_id = None if chunk_id == Manifest.MANIFEST_ID else chunk_id + _, data = self.key.decrypt(_chunk_id, encrypted_data) + except IntegrityError as integrity_error: + self.error_found = True + errors += 1 + logger.error('chunk %s, integrity error: %s', bin_to_hex(chunk_id), integrity_error) + defect_chunks.append(chunk_id) pi.finish() if defect_chunks: if self.repair: diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 5c4f0fef..6d6b8c7e 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1494,6 +1494,17 @@ def file_status(mode): return '?' +def chunkit(it, size): + """ + Chunk an iterator into pieces of . + + >>> list(chunker('ABCDEFG', 3)) + [['A', 'B', 'C'], ['D', 'E', 'F'], ['G']] + """ + iterable = iter(it) + return iter(lambda: list(islice(iterable, size)), []) + + def consume(iterator, n=None): """Advance the iterator n-steps ahead. If n is none, consume entirely.""" # Use functions that consume iterators at C speed. diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index b2b568d7..55569e96 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -25,6 +25,7 @@ from ..helpers import load_excludes from ..helpers import CompressionSpec, CompressionDecider1, CompressionDecider2 from ..helpers import parse_pattern, PatternMatcher, RegexPattern, PathPrefixPattern, FnmatchPattern, ShellPattern from ..helpers import swidth_slice +from ..helpers import chunkit from . import BaseTestCase, environment_variable, FakeInputs @@ -977,6 +978,23 @@ def test_chunk_file_wrapper(): assert cfw.exhausted +def test_chunkit(): + it = chunkit('abcdefg', 3) + assert next(it) == ['a', 'b', 'c'] + assert next(it) == ['d', 'e', 'f'] + assert next(it) == ['g'] + with pytest.raises(StopIteration): + next(it) + with pytest.raises(StopIteration): + next(it) + + it = chunkit('ab', 3) + assert list(it) == [['a', 'b']] + + it = chunkit('', 3) + assert list(it) == [] + + def test_clean_lines(): conf = """\ #comment From f47f7cec893512ed228deaa1ad06c31d38ec5991 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Sep 2016 19:58:13 +0200 Subject: [PATCH 0248/1387] update CHANGES (master / 1.1) --- docs/changes.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index e0cec534..17f5dc70 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,6 +50,36 @@ The best check that everything is ok is to run a dry-run extraction:: borg extract -v --dry-run REPO::ARCHIVE +Version 1.1.0b2 (not released yet) +---------------------------------- + +Bug fixes: + +- fix incorrect preservation of delete tags, leading to "object count mismatch" + on borg check, #1598. This only occurred with 1.1.0b1 (not with 1.0.x) and is + normally fixed by running another borg create/delete/prune. +- fix broken --progress for double-cell paths (e.g. CJK), #1624 +- borg recreate: also catch SIGHUP +- FUSE: + + - fix hardlinks in versions view, #1599 + - add parameter check to ItemCache.get to make potential failures more clear + +New features: + +- Archiver, RemoteRepository: add --remote-ratelimit (send data) +- borg help compression, #1582 +- borg check: delete chunks with integrity errors, #1575, so they can be + "repaired" immediately and maybe healed later. + +Other changes: + +- borg check --verify-data slightly tuned (use get_many()) +- Change {utcnow} and {now} to ISO-8601 format ("T" date/time separator) +- repo check: log transaction IDs, improve object count mismatch diagnostic +- Vagrantfile: use TW's fresh-bootloader pyinstaller branch + + Version 1.1.0b1 (2016-08-28) ---------------------------- From f6b9276de93339aed03ce14bf3b07ab103fd0352 Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Sat, 13 Aug 2016 14:29:23 +0200 Subject: [PATCH 0249/1387] Adds arguments to filter archives These are: --sort-by, --first and --last Includes a method to obtain a list of archive infos filtered by these Archives.list: - ensure reverse is always applied - always return a list --- src/borg/archiver.py | 40 +++++++++++++++++++++++++++++++++++++++- src/borg/helpers.py | 20 +++++++++++++++++--- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 3abe7a50..50540369 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -16,6 +16,7 @@ import traceback from binascii import unhexlify from datetime import datetime from itertools import zip_longest +from operator import attrgetter from .logger import create_logger, setup_logging logger = create_logger() @@ -28,7 +29,8 @@ from .cache import Cache from .constants import * # NOQA from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from .helpers import Error, NoManifestError -from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec, PrefixSpec +from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec +from .helpers import PrefixSpec, sort_by_spec, HUMAN_SORT_KEYS from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter, format_time, format_file_size, format_archive from .helpers import safe_encode, remove_surrogates, bin_to_hex from .helpers import prune_within, prune_split @@ -2549,6 +2551,23 @@ class Archiver: return parser + @staticmethod + def add_archives_filters_args(subparser): + filters_group = subparser.add_argument_group('filters', 'Archive filters can be applied to repository targets.') + filters_group.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, default='', + help='only consider archive names starting with this prefix') + + sort_by_default = 'timestamp' + filters_group.add_argument('--sort-by', dest='sort_by', type=sort_by_spec, default=sort_by_default, + help='Comma-separated list of sorting keys; valid keys are: {}; default is: {}' + .format(', '.join(HUMAN_SORT_KEYS), sort_by_default)) + + group = filters_group.add_mutually_exclusive_group() + group.add_argument('--first', dest='first', metavar='N', default=0, type=int, + help='select first N archives') + group.add_argument('--last', dest='last', metavar='N', default=0, type=int, + help='delete last N archives') + def get_args(self, argv, cmd): """usually, just returns argv, except if we deal with a ssh forced command for borg serve.""" result = self.parse_args(argv[1:]) @@ -2611,6 +2630,25 @@ class Archiver: logger.warning("Using a pure-python msgpack! This will result in lower performance.") return args.func(args) + def _get_filtered_archives(self, args, manifest): + if args.location.archive: + raise Error('The options --first, --last and --prefix can only be used on repository targets.') + + archives = manifest.archives.list() + if not archives: + logger.critical('There are no archives.') + self.exit_code = self.exit_code or EXIT_WARNING + return [] + + for sortkey in reversed(args.sort_by.split(',')): + archives.sort(key=attrgetter(sortkey)) + if args.last: + archives.reverse() + + n = args.first or args.last + + return archives[:n] + def sig_info_handler(sig_no, stack): # pragma: no cover """search the stack for infos about the currently processed file and print them""" diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 6d6b8c7e..85959e86 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -143,10 +143,14 @@ class Archives(abc.MutableMapping): del self._archives[name] def list(self, sort_by=None, reverse=False): - # inexpensive Archive.list_archives replacement if we just need .name, .id, .ts - archives = self.values() # [self[name] for name in self] + """ Inexpensive Archive.list_archives replacement if we just need .name, .id, .ts + Returns list of borg.helpers.ArchiveInfo instances + """ + archives = list(self.values()) # [self[name] for name in self] if sort_by is not None: - archives = sorted(archives, key=attrgetter(sort_by), reverse=reverse) + archives = sorted(archives, key=attrgetter(sort_by)) + if reverse: + archives.reverse() return archives def set_raw_dict(self, d): @@ -655,6 +659,16 @@ def replace_placeholders(text): return format_line(text, data) +HUMAN_SORT_KEYS = ['timestamp'] + list(ArchiveInfo._fields) +HUMAN_SORT_KEYS.remove('ts') + +def sort_by_spec(text): + for token in text.split(','): + if token not in HUMAN_SORT_KEYS: + raise ValueError('Invalid sort key: %s' % token) + return text.replace('timestamp', 'ts') + + def safe_timestamp(item_timestamp_ns): try: return datetime.fromtimestamp(bigint_to_int(item_timestamp_ns) / 1e9) From 17f23639359f3a8207b85d3508d548bd871b8dad Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Mon, 22 Aug 2016 21:30:38 +0200 Subject: [PATCH 0250/1387] Adds --prefix to the archives filters arguments - adds prefix argument to helpers.Archives.list - also renames function PrefixSpec to prefix_spec --- src/borg/archiver.py | 12 ++++++------ src/borg/helpers.py | 11 +++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 50540369..2604818d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -30,7 +30,7 @@ from .constants import * # NOQA from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from .helpers import Error, NoManifestError from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec -from .helpers import PrefixSpec, sort_by_spec, HUMAN_SORT_KEYS +from .helpers import prefix_spec, sort_by_spec, HUMAN_SORT_KEYS from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter, format_time, format_file_size, format_archive from .helpers import safe_encode, remove_surrogates, bin_to_hex from .helpers import prune_within, prune_split @@ -1605,7 +1605,7 @@ class Archiver: subparser.add_argument('--last', dest='last', type=int, default=None, metavar='N', help='only check last N archives (Default: all)') - subparser.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, + subparser.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, help='only consider archive names starting with this prefix') subparser.add_argument('-p', '--progress', dest='progress', action='store_true', default=False, @@ -1995,7 +1995,7 @@ class Archiver: subparser.add_argument('--format', '--list-format', dest='format', type=str, help="""specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")""") - subparser.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, + subparser.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, help='only consider archive names starting with this prefix') subparser.add_argument('-e', '--exclude', dest='excludes', type=parse_pattern, action='append', @@ -2161,7 +2161,7 @@ class Archiver: help='number of monthly archives to keep') subparser.add_argument('-y', '--keep-yearly', dest='yearly', type=int, default=0, help='number of yearly archives to keep') - subparser.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, + subparser.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, help='only consider archive names starting with this prefix') subparser.add_argument('--save-space', dest='save_space', action='store_true', default=False, @@ -2634,7 +2634,7 @@ class Archiver: if args.location.archive: raise Error('The options --first, --last and --prefix can only be used on repository targets.') - archives = manifest.archives.list() + archives = manifest.archives.list(prefix=args.prefix) if not archives: logger.critical('There are no archives.') self.exit_code = self.exit_code or EXIT_WARNING @@ -2645,7 +2645,7 @@ class Archiver: if args.last: archives.reverse() - n = args.first or args.last + n = args.first or args.last or len(archives) return archives[:n] diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 85959e86..27ce121b 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -142,11 +142,11 @@ class Archives(abc.MutableMapping): name = safe_encode(name) del self._archives[name] - def list(self, sort_by=None, reverse=False): + def list(self, sort_by=None, reverse=False, prefix=''): """ Inexpensive Archive.list_archives replacement if we just need .name, .id, .ts Returns list of borg.helpers.ArchiveInfo instances """ - archives = list(self.values()) # [self[name] for name in self] + archives = [x for x in self.values() if x.name.startswith(prefix)] if sort_by is not None: archives = sorted(archives, key=attrgetter(sort_by)) if reverse: @@ -572,10 +572,6 @@ def CompressionSpec(s): raise ValueError -def PrefixSpec(s): - return replace_placeholders(s) - - def dir_is_cachedir(path): """Determines whether the specified path is a cache directory (and therefore should potentially be excluded from the backup) according to @@ -658,10 +654,13 @@ def replace_placeholders(text): } return format_line(text, data) +prefix_spec = replace_placeholders + HUMAN_SORT_KEYS = ['timestamp'] + list(ArchiveInfo._fields) HUMAN_SORT_KEYS.remove('ts') + def sort_by_spec(text): for token in text.split(','): if token not in HUMAN_SORT_KEYS: From f2d4d36ceae8ebceaeecd06ebdcd9fcfa6348531 Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Sun, 24 Jul 2016 00:40:15 +0200 Subject: [PATCH 0251/1387] Allows delete to be used with archive filters --- src/borg/archiver.py | 91 ++++++++++++++++++++++------------ src/borg/testsuite/archiver.py | 14 ++++++ 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 2604818d..6efd81eb 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -770,11 +770,31 @@ class Archiver: @with_repository(exclusive=True, manifest=False) def do_delete(self, args, repository): - """Delete an existing repository or archive""" + """Delete an existing repository or archives""" + if any((args.location.archive, args.first, args.last, args.prefix)): + return self._delete_archives(args, repository) + else: + return self._delete_repository(args, repository) + + def _delete_archives(self, args, repository): + """Delete archives""" + manifest, key = Manifest.load(repository) + if args.location.archive: - manifest, key = Manifest.load(repository) - with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: - archive = Archive(repository, key, manifest, args.location.archive, cache=cache) + archive_names = (args.location.archive,) + else: + archive_names = tuple(x.name for x in self._get_filtered_archives(args, manifest)) + if not archive_names: + return self.exit_code + + stats_logger = logging.getLogger('borg.output.stats') + if args.stats: + log_multi(DASHES, STATS_HEADER, logger=stats_logger) + + with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: + for i, archive_name in enumerate(archive_names, 1): + logger.info('Deleting {} ({}/{}):'.format(archive_name, i, len(archive_names))) + archive = Archive(repository, key, manifest, archive_name, cache=cache) stats = Statistics() archive.delete(stats, progress=args.progress, forced=args.forced) manifest.write() @@ -782,33 +802,41 @@ class Archiver: cache.commit() logger.info("Archive deleted.") if args.stats: - log_multi(DASHES, - STATS_HEADER, - stats.summary.format(label='Deleted data:', stats=stats), - str(cache), - DASHES, logger=logging.getLogger('borg.output.stats')) - else: - if not args.cache_only: - msg = [] - try: - manifest, key = Manifest.load(repository) - except NoManifestError: - msg.append("You requested to completely DELETE the repository *including* all archives it may contain.") - msg.append("This repository seems to have no manifest, so we can't tell anything about its contents.") - else: - msg.append("You requested to completely DELETE the repository *including* all archives it contains:") - for archive_info in manifest.archives.list(sort_by='ts'): - msg.append(format_archive(archive_info)) - msg.append("Type 'YES' if you understand this and want to continue: ") - msg = '\n'.join(msg) - if not yes(msg, false_msg="Aborting.", invalid_msg='Invalid answer, aborting.', truish=('YES', ), - retry=False, env_var_override='BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'): - self.exit_code = EXIT_ERROR - return self.exit_code - repository.destroy() - logger.info("Repository deleted.") - Cache.destroy(repository) - logger.info("Cache deleted.") + log_multi(stats.summary.format(label='Deleted data:', stats=stats), + DASHES, logger=stats_logger) + if not args.forced and self.exit_code: + break + if args.stats: + stats_logger.info(str(cache)) + + return self.exit_code + + def _delete_repository(self, args, repository): + """Delete a repository""" + if not args.cache_only: + msg = [] + try: + manifest, key = Manifest.load(repository) + except NoManifestError: + msg.append("You requested to completely DELETE the repository *including* all archives it may " + "contain.") + msg.append("This repository seems to have no manifest, so we can't tell anything about its " + "contents.") + else: + msg.append("You requested to completely DELETE the repository *including* all archives it " + "contains:") + for archive_info in manifest.archives.list(sort_by='ts'): + msg.append(format_archive(archive_info)) + msg.append("Type 'YES' if you understand this and want to continue: ") + msg = '\n'.join(msg) + if not yes(msg, false_msg="Aborting.", invalid_msg='Invalid answer, aborting.', truish=('YES',), + retry=False, env_var_override='BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'): + self.exit_code = EXIT_ERROR + return self.exit_code + repository.destroy() + logger.info("Repository deleted.") + Cache.destroy(repository) + logger.info("Cache deleted.") return self.exit_code @with_repository() @@ -1969,6 +1997,7 @@ class Archiver: subparser.add_argument('location', metavar='TARGET', nargs='?', default='', type=location_validator(), help='archive or repository to delete') + self.add_archives_filters_args(subparser) list_epilog = textwrap.dedent(""" This command lists the contents of a repository or an archive. diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 50cb42c5..92abd018 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -59,6 +59,9 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw): except subprocess.CalledProcessError as e: output = e.output ret = e.returncode + except SystemExit as e: # possibly raised by argparse + output = '' + ret = e.code return ret, os.fsdecode(output) else: stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr @@ -987,8 +990,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') self.cmd('create', self.repository_location + '::test.2', 'input') + self.cmd('create', self.repository_location + '::test.3', 'input') + self.cmd('create', self.repository_location + '::another_test.1', 'input') + self.cmd('create', self.repository_location + '::another_test.2', 'input') self.cmd('extract', '--dry-run', self.repository_location + '::test') self.cmd('extract', '--dry-run', self.repository_location + '::test.2') + self.cmd('delete', '--prefix', 'another_', self.repository_location) + self.cmd('delete', '--last', '1', self.repository_location) self.cmd('delete', self.repository_location + '::test') self.cmd('extract', '--dry-run', self.repository_location + '::test.2') output = self.cmd('delete', '--stats', self.repository_location + '::test.2') @@ -1811,6 +1819,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_not_in("input/file1", output) self.assert_not_in("x input/file5", output) + def test_bad_filters(self): + self.cmd('init', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + self.cmd('delete', '--first', '1', '--last', '1', self.repository_location, fork=True, exit_code=2) + + def test_key_export_keyfile(self): export_file = self.output_path + '/exported' self.cmd('init', self.repository_location, '--encryption', 'keyfile') From 369d0a0881a9c457d4e77d0b9de1cf6b9c57722b Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Sat, 13 Aug 2016 14:29:23 +0200 Subject: [PATCH 0252/1387] Adds archives filters for info --- src/borg/archiver.py | 28 ++++++++++++++++++++++++---- src/borg/testsuite/archiver.py | 2 ++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 6efd81eb..b36dc28f 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -913,11 +913,24 @@ class Archiver: @with_repository(cache=True) def do_info(self, args, repository, manifest, key, cache): """Show archive details such as disk space used""" + if any((args.location.archive, args.first, args.last, args.prefix)): + return self._info_archives(args, repository, manifest, key, cache) + else: + return self._info_repository(cache) + + def _info_archives(self, args, repository, manifest, key, cache): def format_cmdline(cmdline): return remove_surrogates(' '.join(shlex.quote(x) for x in cmdline)) if args.location.archive: - archive = Archive(repository, key, manifest, args.location.archive, cache=cache, + archive_names = (args.location.archive,) + else: + archive_names = tuple(x.name for x in self._get_filtered_archives(args, manifest)) + if not archive_names: + return self.exit_code + + for i, archive_name in enumerate(archive_names, 1): + archive = Archive(repository, key, manifest, archive_name, cache=cache, consider_part_files=args.consider_part_files) stats = archive.calc_stats(cache) print('Archive name: %s' % archive.name) @@ -934,9 +947,15 @@ class Archiver: print(STATS_HEADER) print(str(stats)) print(str(cache)) - else: - print(STATS_HEADER) - print(str(cache)) + if self.exit_code: + break + if len(archive_names) - i: + print() + return self.exit_code + + def _info_repository(self, cache): + print(STATS_HEADER) + print(str(cache)) return self.exit_code @with_repository(exclusive=True) @@ -2102,6 +2121,7 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', type=location_validator(), help='archive or repository to display information about') + self.add_archives_filters_args(subparser) break_lock_epilog = textwrap.dedent(""" This command breaks the repository and cache locks. diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 92abd018..68e9e7b7 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -964,6 +964,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert 'All archives:' in info_repo info_archive = self.cmd('info', self.repository_location + '::test') assert 'Archive name: test\n' in info_archive + info_archive = self.cmd('info', '--first', '1', self.repository_location) + assert 'Archive name: test\n' in info_archive def test_comment(self): self.create_regular_file('file1', size=1024 * 80) From e0e9edfb4292f566c452dc881219c28fe840bd82 Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Sat, 13 Aug 2016 14:29:23 +0200 Subject: [PATCH 0253/1387] Adds archives filters for list --- src/borg/archiver.py | 51 +++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index b36dc28f..39bc37ce 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -879,34 +879,38 @@ class Archiver: write = sys.stdout.buffer.write if args.location.archive: - matcher, _ = self.build_matcher(args.excludes, args.paths) - with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: - archive = Archive(repository, key, manifest, args.location.archive, cache=cache, - consider_part_files=args.consider_part_files) - - if args.format is not None: - format = args.format - elif args.short: - format = "{path}{NL}" - else: - format = "{mode} {user:6} {group:6} {size:8} {isomtime} {path}{extra}{NL}" - formatter = ItemFormatter(archive, format) - - for item in archive.iter_items(lambda item: matcher.match(item.path)): - write(safe_encode(formatter.format_item(item))) + return self._list_archive(args, repository, manifest, key, write) else: + return self._list_repository(args, manifest, write) + + def _list_archive(self, args, repository, manifest, key, write): + matcher, _ = self.build_matcher(args.excludes, args.paths) + with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: + archive = Archive(repository, key, manifest, args.location.archive, cache=cache, + consider_part_files=args.consider_part_files) if args.format is not None: format = args.format elif args.short: - format = "{archive}{NL}" + format = "{path}{NL}" else: - format = "{archive:<36} {time} [{id}]{NL}" - formatter = ArchiveFormatter(format) + format = "{mode} {user:6} {group:6} {size:8} {isomtime} {path}{extra}{NL}" + formatter = ItemFormatter(archive, format) - for archive_info in manifest.archives.list(sort_by='ts'): - if args.prefix and not archive_info.name.startswith(args.prefix): - continue - write(safe_encode(formatter.format_item(archive_info))) + for item in archive.iter_items(lambda item: matcher.match(item.path)): + write(safe_encode(formatter.format_item(item))) + return self.exit_code + + def _list_repository(self, args, manifest, write): + if args.format is not None: + format = args.format + elif args.short: + format = "{archive}{NL}" + else: + format = "{archive:<36} {time} [{id}]{NL}" + formatter = ArchiveFormatter(format) + + for archive_info in self._get_filtered_archives(args, manifest): + write(safe_encode(formatter.format_item(archive_info))) return self.exit_code @@ -2043,8 +2047,6 @@ class Archiver: subparser.add_argument('--format', '--list-format', dest='format', type=str, help="""specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")""") - subparser.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, - help='only consider archive names starting with this prefix') subparser.add_argument('-e', '--exclude', dest='excludes', type=parse_pattern, action='append', metavar="PATTERN", help='exclude paths matching PATTERN') @@ -2056,6 +2058,7 @@ class Archiver: help='repository/archive to list contents of') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to list; patterns are supported') + self.add_archives_filters_args(subparser) mount_epilog = textwrap.dedent(""" This command mounts an archive as a FUSE filesystem. This can be useful for From bd7cc38d6e567c4717f9a35a2331fb57dc91b809 Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Thu, 1 Sep 2016 17:36:37 +0200 Subject: [PATCH 0254/1387] Changes arg processor names to camelcase --- src/borg/archiver.py | 18 +++++++++--------- src/borg/helpers.py | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 39bc37ce..66a2e62d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -30,7 +30,7 @@ from .constants import * # NOQA from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from .helpers import Error, NoManifestError from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec -from .helpers import prefix_spec, sort_by_spec, HUMAN_SORT_KEYS +from .helpers import PrefixSpec, SortBySpec, HUMAN_SORT_KEYS from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter, format_time, format_file_size, format_archive from .helpers import safe_encode, remove_surrogates, bin_to_hex from .helpers import prune_within, prune_split @@ -1656,7 +1656,7 @@ class Archiver: subparser.add_argument('--last', dest='last', type=int, default=None, metavar='N', help='only check last N archives (Default: all)') - subparser.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, + subparser.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, help='only consider archive names starting with this prefix') subparser.add_argument('-p', '--progress', dest='progress', action='store_true', default=False, @@ -2213,7 +2213,7 @@ class Archiver: help='number of monthly archives to keep') subparser.add_argument('-y', '--keep-yearly', dest='yearly', type=int, default=0, help='number of yearly archives to keep') - subparser.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, + subparser.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, help='only consider archive names starting with this prefix') subparser.add_argument('--save-space', dest='save_space', action='store_true', default=False, @@ -2606,19 +2606,19 @@ class Archiver: @staticmethod def add_archives_filters_args(subparser): filters_group = subparser.add_argument_group('filters', 'Archive filters can be applied to repository targets.') - filters_group.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, default='', + filters_group.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, default='', help='only consider archive names starting with this prefix') sort_by_default = 'timestamp' - filters_group.add_argument('--sort-by', dest='sort_by', type=sort_by_spec, default=sort_by_default, - help='Comma-separated list of sorting keys; valid keys are: {}; default is: {}' - .format(', '.join(HUMAN_SORT_KEYS), sort_by_default)) + filters_group.add_argument('--sort-by', dest='sort_by', type=SortBySpec, default=sort_by_default, + help='Comma-separated list of sorting keys; valid keys are: {}; default is: {}' + .format(', '.join(HUMAN_SORT_KEYS), sort_by_default)) group = filters_group.add_mutually_exclusive_group() group.add_argument('--first', dest='first', metavar='N', default=0, type=int, - help='select first N archives') + help='consider first N archives after other filter args were applied') group.add_argument('--last', dest='last', metavar='N', default=0, type=int, - help='delete last N archives') + help='consider last N archives after other filter args were applied') def get_args(self, argv, cmd): """usually, just returns argv, except if we deal with a ssh forced command for borg serve.""" diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 27ce121b..541c832e 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -654,14 +654,14 @@ def replace_placeholders(text): } return format_line(text, data) -prefix_spec = replace_placeholders +PrefixSpec = replace_placeholders HUMAN_SORT_KEYS = ['timestamp'] + list(ArchiveInfo._fields) HUMAN_SORT_KEYS.remove('ts') -def sort_by_spec(text): +def SortBySpec(text): for token in text.split(','): if token not in HUMAN_SORT_KEYS: raise ValueError('Invalid sort key: %s' % token) From 089995ef73a990e89cf3e65ca7e345dbbf52ab30 Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Fri, 30 Sep 2016 20:01:59 +0200 Subject: [PATCH 0255/1387] Changes on filters after feedback --- src/borg/archiver.py | 8 ++------ src/borg/helpers.py | 5 +++-- src/borg/testsuite/archiver.py | 1 - 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 66a2e62d..31b1f59d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2616,9 +2616,9 @@ class Archiver: group = filters_group.add_mutually_exclusive_group() group.add_argument('--first', dest='first', metavar='N', default=0, type=int, - help='consider first N archives after other filter args were applied') + help='consider first N archives after other filters were applied') group.add_argument('--last', dest='last', metavar='N', default=0, type=int, - help='consider last N archives after other filter args were applied') + help='consider last N archives after other filters were applied') def get_args(self, argv, cmd): """usually, just returns argv, except if we deal with a ssh forced command for borg serve.""" @@ -2687,10 +2687,6 @@ class Archiver: raise Error('The options --first, --last and --prefix can only be used on repository targets.') archives = manifest.archives.list(prefix=args.prefix) - if not archives: - logger.critical('There are no archives.') - self.exit_code = self.exit_code or EXIT_WARNING - return [] for sortkey in reversed(args.sort_by.split(',')): archives.sort(key=attrgetter(sortkey)) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 541c832e..4c6b939c 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -143,8 +143,9 @@ class Archives(abc.MutableMapping): del self._archives[name] def list(self, sort_by=None, reverse=False, prefix=''): - """ Inexpensive Archive.list_archives replacement if we just need .name, .id, .ts - Returns list of borg.helpers.ArchiveInfo instances + """ + Inexpensive Archive.list_archives replacement if we just need .name, .id, .ts + Returns list of borg.helpers.ArchiveInfo instances """ archives = [x for x in self.values() if x.name.startswith(prefix)] if sort_by is not None: diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 68e9e7b7..cb79d6b2 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1826,7 +1826,6 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('create', self.repository_location + '::test', 'input') self.cmd('delete', '--first', '1', '--last', '1', self.repository_location, fork=True, exit_code=2) - def test_key_export_keyfile(self): export_file = self.output_path + '/exported' self.cmd('init', self.repository_location, '--encryption', 'keyfile') From 4174291f6f97ead3780593c6295ddce4058a91af Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 30 Sep 2016 20:38:46 +0200 Subject: [PATCH 0256/1387] hashindex: bump api version API_VERSION is used to check whether the compiled binaries are up-to-date. the tests for the recent iterator fixes of course need updated (fixed) binaries, so we bump api_version, so not-up-to-date binaries will get identified. --- borg/hashindex.pyx | 2 +- borg/helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/borg/hashindex.pyx b/borg/hashindex.pyx index 09ec8961..a27d0e8f 100644 --- a/borg/hashindex.pyx +++ b/borg/hashindex.pyx @@ -4,7 +4,7 @@ import os cimport cython from libc.stdint cimport uint32_t, UINT32_MAX, uint64_t -API_VERSION = 2 +API_VERSION = 3 cdef extern from "_hashindex.c": diff --git a/borg/helpers.py b/borg/helpers.py index e7be78cb..db579d34 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -80,7 +80,7 @@ class PlaceholderError(Error): def check_extension_modules(): from . import platform - if hashindex.API_VERSION != 2: + if hashindex.API_VERSION != 3: raise ExtensionModuleError if chunker.API_VERSION != 2: raise ExtensionModuleError From a65707beb8e98b4867ae951d40fd41b24aebdcf4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 30 Sep 2016 21:09:02 +0200 Subject: [PATCH 0257/1387] add more specific warning about write-access debug commands --- docs/usage.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index a3015f61..0452c164 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -567,9 +567,20 @@ Miscellaneous Help Debug Commands -------------- + There are some more commands (all starting with "debug-") which are all **not intended for normal use** and **potentially very dangerous** if used incorrectly. +For example, ``borg debug-put-obj`` and ``borg debug-delete-obj`` will only do +what their name suggests: put objects into repo / delete objects from repo. + +Please note: + +- they will not update the chunks cache (chunks index) about the object +- they will not update the manifest (so no automatic chunks index resync is triggered) +- they will not check whether the object is in use (e.g. before delete-obj) +- they will not update any metadata which may point to the object + They exist to improve debugging capabilities without direct system access, e.g. in case you ever run into some severe malfunction. Use them only if you know what you are doing or if a trusted |project_name| developer tells you what to do. From 8df6cb8156a810c675b12bc86073979fa4593bab Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 30 Sep 2016 23:59:41 +0200 Subject: [PATCH 0258/1387] hashindex: bump api_version note: merging the respective changeset from 1.0-maint was not effective as we already had version 3, so there was no increase. --- src/borg/hashindex.pyx | 2 +- src/borg/helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 857b1936..dd224f52 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -8,7 +8,7 @@ from libc.stdint cimport uint32_t, UINT32_MAX, uint64_t from libc.errno cimport errno from cpython.exc cimport PyErr_SetFromErrnoWithFilename -API_VERSION = 3 +API_VERSION = 4 cdef extern from "_hashindex.c": diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 4c6b939c..8a3017f5 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -85,7 +85,7 @@ class PlaceholderError(Error): def check_extension_modules(): from . import platform, compress - if hashindex.API_VERSION != 3: + if hashindex.API_VERSION != 4: raise ExtensionModuleError if chunker.API_VERSION != 2: raise ExtensionModuleError From 041151275009750e977061aabe5653c1d5873fce Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 1 Oct 2016 18:23:36 +0200 Subject: [PATCH 0259/1387] ran build_usage --- docs/usage/common-options.rst.inc | 2 + docs/usage/create.rst.inc | 13 +-- docs/usage/delete.rst.inc | 10 +++ docs/usage/help.rst.inc | 132 +++++++++++++++++++++++++----- docs/usage/info.rst.inc | 21 ++++- docs/usage/key_export.rst.inc | 43 ++++++++++ docs/usage/key_import.rst.inc | 32 ++++++++ docs/usage/list.rst.inc | 12 ++- 8 files changed, 229 insertions(+), 36 deletions(-) create mode 100644 docs/usage/key_export.rst.inc create mode 100644 docs/usage/key_import.rst.inc diff --git a/docs/usage/common-options.rst.inc b/docs/usage/common-options.rst.inc index 29bada9d..fb235b52 100644 --- a/docs/usage/common-options.rst.inc +++ b/docs/usage/common-options.rst.inc @@ -24,5 +24,7 @@ | set umask to M (local and remote, default: 0077) ``--remote-path PATH`` | set remote path to executable (default: "borg") + ``--remote-ratelimit rate`` + | set remote network upload rate limit in kiByte/s (default: 0=unlimited) ``--consider-part-files`` | treat part files like normal files (e.g. to list/extract them) \ No newline at end of file diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 7b31c18c..ba2dfe21 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -61,18 +61,9 @@ Archive options ``--chunker-params CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE`` | specify the chunker parameters. default: 19,23,21,4095 ``-C COMPRESSION``, ``--compression COMPRESSION`` - | select compression algorithm (and level): - | none == no compression (default), - | auto,C[,L] == built-in heuristic (try with lz4 whether the data is - | compressible) decides between none or C[,L] - with C[,L] - | being any valid compression algorithm (and optional level), - | lz4 == lz4, - | zlib == zlib (default level 6), - | zlib,0 .. zlib,9 == zlib (with level 0..9), - | lzma == lzma (default level 6), - | lzma,0 .. lzma,9 == lzma (with level 0..9). + | select compression algorithm, see the output of the "borg help compression" command for details. ``--compression-from COMPRESSIONCONFIG`` - | read compression patterns from COMPRESSIONCONFIG, one per line + | read compression patterns from COMPRESSIONCONFIG, see the output of the "borg help compression" command for details. Description ~~~~~~~~~~~ diff --git a/docs/usage/delete.rst.inc b/docs/usage/delete.rst.inc index 87451ec5..2a685e88 100644 --- a/docs/usage/delete.rst.inc +++ b/docs/usage/delete.rst.inc @@ -27,6 +27,16 @@ optional arguments `Common options`_ | +filters + ``-P``, ``--prefix`` + | only consider archive names starting with this prefix + ``--sort-by`` + | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + ``--first N`` + | consider first N archives after other filters were applied + ``--last N`` + | consider last N archives after other filters were applied + Description ~~~~~~~~~~~ diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index b079dd2d..572b9f23 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -99,40 +99,134 @@ borg help placeholders ~~~~~~~~~~~~~~~~~~~~~~ - Repository (or Archive) URLs, --prefix and --remote-path values support these - placeholders: +Repository (or Archive) URLs, --prefix and --remote-path values support these +placeholders: - {hostname} +{hostname} - The (short) hostname of the machine. + The (short) hostname of the machine. - {fqdn} +{fqdn} - The full name of the machine. + The full name of the machine. - {now} +{now} - The current local date and time. + The current local date and time, by default in ISO-8601 format. + You can also supply your own `format string `_, e.g. {now:%Y-%m-%d_%H:%M:%S} - {utcnow} +{utcnow} - The current UTC date and time. + The current UTC date and time, by default in ISO-8601 format. + You can also supply your own `format string `_, e.g. {utcnow:%Y-%m-%d_%H:%M:%S} - {user} +{user} - The user name (or UID, if no name is available) of the user running borg. + The user name (or UID, if no name is available) of the user running borg. - {pid} +{pid} - The current process ID. + The current process ID. - {borgversion} +{borgversion} - The version of borg. + The version of borg. Examples:: - borg create /path/to/repo::{hostname}-{user}-{utcnow} ... - borg create /path/to/repo::{hostname}-{now:%Y-%m-%d_%H:%M:%S} ... - borg prune --prefix '{hostname}-' ... + borg create /path/to/repo::{hostname}-{user}-{utcnow} ... + borg create /path/to/repo::{hostname}-{now:%Y-%m-%d_%H:%M:%S} ... + borg prune --prefix '{hostname}-' ... + +.. _borg_compression: + +borg help compression +~~~~~~~~~~~~~~~~~~~~~ + + +Compression is off by default, if you want some, you have to specify what you want. + +Valid compression specifiers are: + +none + + Do not compress. (default) + +lz4 + + Use lz4 compression. High speed, low compression. + +zlib[,L] + + Use zlib ("gz") compression. Medium speed, medium compression. + If you do not explicitely give the compression level L (ranging from 0 + to 9), it will use level 6. + Giving level 0 (means "no compression", but still has zlib protocol + overhead) is usually pointless, you better use "none" compression. + +lzma[,L] + + Use lzma ("xz") compression. Low speed, high compression. + If you do not explicitely give the compression level L (ranging from 0 + to 9), it will use level 6. + Giving levels above 6 is pointless and counterproductive because it does + not compress better due to the buffer size used by borg - but it wastes + lots of CPU cycles and RAM. + +auto,C[,L] + + Use a built-in heuristic to decide per chunk whether to compress or not. + The heuristic tries with lz4 whether the data is compressible. + For incompressible data, it will not use compression (uses "none"). + For compressible data, it uses the given C[,L] compression - with C[,L] + being any valid compression specifier. + +The decision about which compression to use is done by borg like this: + +1. find a compression specifier (per file): + match the path/filename against all patterns in all --compression-from + files (if any). If a pattern matches, use the compression spec given for + that pattern. If no pattern matches (and also if you do not give any + --compression-from option), default to the compression spec given by + --compression. See docs/misc/compression.conf for an example config. + +2. if the found compression spec is not "auto", the decision is taken: + use the found compression spec. + +3. if the found compression spec is "auto", test compressibility of each + chunk using lz4. + If it is compressible, use the C,[L] compression spec given within the + "auto" specifier. If it is not compressible, use no compression. + +Examples:: + + borg create --compression lz4 REPO::ARCHIVE data + borg create --compression zlib REPO::ARCHIVE data + borg create --compression zlib,1 REPO::ARCHIVE data + borg create --compression auto,lzma,6 REPO::ARCHIVE data + borg create --compression-from compression.conf --compression auto,lzma ... + +compression.conf has entries like:: + + # example config file for --compression-from option + # + # Format of non-comment / non-empty lines: + # : + # compression-spec is same format as for --compression option + # path/filename pattern is same format as for --exclude option + none:*.gz + none:*.zip + none:*.mp3 + none:*.ogg + +General remarks: + +It is no problem to mix different compression methods in one repo, +deduplication is done on the source data chunks (not on the compressed +or encrypted data). + +If some specific chunk was once compressed and stored into the repo, creating +another backup that also uses this chunk will not change the stored chunk. +So if you use different compression specs for the backups, whichever stores a +chunk first determines its compression. See also borg recreate. diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index e4a27e7b..be61637e 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -15,13 +15,26 @@ positional arguments `Common options`_ | +filters + ``-P``, ``--prefix`` + | only consider archive names starting with this prefix + ``--sort-by`` + | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + ``--first N`` + | consider first N archives after other filters were applied + ``--last N`` + | consider last N archives after other filters were applied + Description ~~~~~~~~~~~ This command displays detailed information about the specified archive or repository. -The "This archive" line refers exclusively to the given archive: -"Deduplicated size" is the size of the unique chunks stored only for the -given archive. +Please note that the deduplicated sizes of the individual archives do not add +up to the deduplicated size of the repository ("all archives"), because the two +are meaning different things: -The "All archives" line shows global statistics (all chunks). +This archive / deduplicated size = amount of data stored ONLY for this archive + = unique chunks of this archive. +All archives / deduplicated size = amount of data stored in the repo + = all chunks in the repository. diff --git a/docs/usage/key_export.rst.inc b/docs/usage/key_export.rst.inc new file mode 100644 index 00000000..f251d430 --- /dev/null +++ b/docs/usage/key_export.rst.inc @@ -0,0 +1,43 @@ +.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! + +.. _borg_key_export: + +borg key export +--------------- +:: + + borg key export REPOSITORY PATH + +positional arguments + REPOSITORY + + PATH + where to store the backup + +optional arguments + ``--paper`` + | Create an export suitable for printing and later type-in + +`Common options`_ + | + +Description +~~~~~~~~~~~ + +If repository encryption is used, the repository is inaccessible +without the key. This command allows to backup this essential key. + +There are two backup formats. The normal backup format is suitable for +digital storage as a file. The ``--paper`` backup format is optimized +for printing and typing in while importing, with per line checks to +reduce problems with manual input. + +For repositories using keyfile encryption the key is saved locally +on the system that is capable of doing backups. To guard against loss +of this key, the key needs to be backed up independently of the main +data backup. + +For repositories using the repokey encryption the key is saved in the +repository in the config file. A backup is thus not strictly needed, +but guards against the repository becoming inaccessible if the file +is damaged for some reason. diff --git a/docs/usage/key_import.rst.inc b/docs/usage/key_import.rst.inc new file mode 100644 index 00000000..c8c2a0a8 --- /dev/null +++ b/docs/usage/key_import.rst.inc @@ -0,0 +1,32 @@ +.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! + +.. _borg_key_import: + +borg key import +--------------- +:: + + borg key import REPOSITORY PATH + +positional arguments + REPOSITORY + + PATH + path to the backup + +optional arguments + ``--paper`` + | interactively import from a backup done with --paper + +`Common options`_ + | + +Description +~~~~~~~~~~~ + +This command allows to restore a key previously backed up with the +export command. + +If the ``--paper`` option is given, the import will be an interactive +process in which each line is checked for plausibility before +proceeding to the next line. For this format PATH must not be given. diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index 8e32df6a..ce6bd804 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -20,8 +20,6 @@ optional arguments ``--format``, ``--list-format`` | specify format for file listing | (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") - ``-P``, ``--prefix`` - | only consider archive names starting with this prefix ``-e PATTERN``, ``--exclude PATTERN`` | exclude paths matching PATTERN ``--exclude-from EXCLUDEFILE`` @@ -30,6 +28,16 @@ optional arguments `Common options`_ | +filters + ``-P``, ``--prefix`` + | only consider archive names starting with this prefix + ``--sort-by`` + | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + ``--first N`` + | consider first N archives after other filters were applied + ``--last N`` + | consider last N archives after other filters were applied + Description ~~~~~~~~~~~ From f57feb121d10d18c3dbc02a9e887620f4c527138 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 1 Oct 2016 18:29:45 +0200 Subject: [PATCH 0260/1387] fix module names in api.rst --- docs/api.rst | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index ab634e86..7ce03019 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2,94 +2,94 @@ API Documentation ================= -.. automodule:: src.borg.archiver +.. automodule:: borg.archiver :members: :undoc-members: -.. automodule:: src.borg.archive +.. automodule:: borg.archive :members: :undoc-members: -.. automodule:: src.borg.repository +.. automodule:: borg.repository :members: :undoc-members: -.. automodule:: src.borg.remote +.. automodule:: borg.remote :members: :undoc-members: -.. automodule:: src.borg.cache +.. automodule:: borg.cache :members: :undoc-members: -.. automodule:: src.borg.key +.. automodule:: borg.key :members: :undoc-members: -.. automodule:: src.borg.nonces +.. automodule:: borg.nonces :members: :undoc-members: -.. automodule:: src.borg.item +.. automodule:: borg.item :members: :undoc-members: -.. automodule:: src.borg.constants +.. automodule:: borg.constants :members: :undoc-members: -.. automodule:: src.borg.logger +.. automodule:: borg.logger :members: :undoc-members: -.. automodule:: src.borg.helpers +.. automodule:: borg.helpers :members: :undoc-members: -.. automodule:: src.borg.locking +.. automodule:: borg.locking :members: :undoc-members: -.. automodule:: src.borg.shellpattern +.. automodule:: borg.shellpattern :members: :undoc-members: -.. automodule:: src.borg.lrucache +.. automodule:: borg.lrucache :members: :undoc-members: -.. automodule:: src.borg.fuse +.. automodule:: borg.fuse :members: :undoc-members: -.. automodule:: src.borg.selftest +.. automodule:: borg.selftest :members: :undoc-members: -.. automodule:: src.borg.xattr +.. automodule:: borg.xattr :members: :undoc-members: -.. automodule:: src.borg.platform.base +.. automodule:: borg.platform.base :members: :undoc-members: -.. automodule:: src.borg.platform.linux +.. automodule:: borg.platform.linux :members: :undoc-members: -.. automodule:: src.borg.hashindex +.. automodule:: borg.hashindex :members: :undoc-members: -.. automodule:: src.borg.compress +.. automodule:: borg.compress :members: get_compressor, Compressor, CompressorBase :undoc-members: -.. automodule:: src.borg.chunker +.. automodule:: borg.chunker :members: :undoc-members: -.. automodule:: src.borg.crypto +.. automodule:: borg.crypto :members: :undoc-members: From e6241cce125d977a26fa86ba7f054019fddf2117 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 1 Oct 2016 18:44:52 +0200 Subject: [PATCH 0261/1387] update CHANGES --- docs/changes.rst | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index f2510b14..dbeb110b 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,8 +50,8 @@ The best check that everything is ok is to run a dry-run extraction:: borg extract -v --dry-run REPO::ARCHIVE -Version 1.1.0b2 (not released yet) ----------------------------------- +Version 1.1.0b2 (2016-10-01) +---------------------------- Bug fixes: @@ -71,13 +71,19 @@ New features: - borg help compression, #1582 - borg check: delete chunks with integrity errors, #1575, so they can be "repaired" immediately and maybe healed later. +- archives filters concept (refactoring/unifying older code) + + - covers --first/--last/--prefix/--sort-by options + - currently used for borg list/info/delete Other changes: - borg check --verify-data slightly tuned (use get_many()) -- Change {utcnow} and {now} to ISO-8601 format ("T" date/time separator) +- change {utcnow} and {now} to ISO-8601 format ("T" date/time separator) - repo check: log transaction IDs, improve object count mismatch diagnostic - Vagrantfile: use TW's fresh-bootloader pyinstaller branch +- fix module names in api.rst +- hashindex: bump api_version Version 1.1.0b1 (2016-08-28) From 5baf749b29c79f7c3df2ef5b37bdc6f9d4086c4f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 1 Oct 2016 20:17:30 +0200 Subject: [PATCH 0262/1387] vagrant freebsd64: fix tox not finding any pythons --- Vagrantfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Vagrantfile b/Vagrantfile index c2bb4142..7980d9bd 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -277,6 +277,7 @@ def run_tests(boxname) if which pyenv > /dev/null; then # for testing, use the earliest point releases of the supported python versions: pyenv global 3.4.0 3.5.0 + pyenv local 3.4.0 3.5.0 fi # otherwise: just use the system python if which fakeroot > /dev/null; then From 864333d6869d2c93840316879f5735d5b92a1a5d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 2 Oct 2016 00:43:06 +0200 Subject: [PATCH 0263/1387] pyinstaller: use a spec file to build borg.exe binary also: exclude osxfuse dylib on Mac OS X --- Vagrantfile | 2 +- scripts/borg.exe.spec | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 scripts/borg.exe.spec diff --git a/Vagrantfile b/Vagrantfile index ef38aa94..60f1b02a 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -263,7 +263,7 @@ def build_binary_with_pyinstaller(boxname) cd /vagrant/borg . borg-env/bin/activate cd borg - pyinstaller -F -n borg.exe --distpath=/vagrant/borg --clean borg/__main__.py + pyinstaller --clean --distpath=/vagrant/borg scripts/borg.exe.spec EOF end diff --git a/scripts/borg.exe.spec b/scripts/borg.exe.spec new file mode 100644 index 00000000..9834c1b8 --- /dev/null +++ b/scripts/borg.exe.spec @@ -0,0 +1,38 @@ +# -*- mode: python -*- +# this pyinstaller spec file is used to build borg binaries on posix platforms + +import os, sys + +basepath = '/vagrant/borg/borg' + +block_cipher = None + +a = Analysis([os.path.join(basepath, 'borg/__main__.py'), ], + pathex=[basepath, ], + binaries=[], + datas=[], + hiddenimports=['borg.platform.posix'], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher) + +if sys.platform == 'darwin': + # do not bundle the osxfuse libraries, so we do not get a version + # mismatch to the installed kernel driver of osxfuse. + a.binaries = [b for b in a.binaries if 'libosxfuse' not in b[0]] + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name='borg.exe', + debug=False, + strip=False, + upx=True, + console=True ) From ce72d2482599e090850bd156032c25b93e197698 Mon Sep 17 00:00:00 2001 From: enkore Date: Sun, 2 Oct 2016 20:11:34 +0200 Subject: [PATCH 0264/1387] Clarify FAQ regarding backup of virtual machines (#1672) --- docs/faq.rst | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index a84cf481..47d0ce1a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -12,6 +12,39 @@ Yes, the `deduplication`_ technique used by |project_name| makes sure only the modified parts of the file are stored. Also, we have optional simple sparse file support for extract. +If you use non-snapshotting backup tools like Borg to back up virtual machines, +then these should be turned off for doing so. Backing up live VMs this way can (and will) +result in corrupted or inconsistent backup contents: a VM image is just a regular file to +Borg with the same issues as regular files when it comes to concurrent reading and writing from +the same file. + +For backing up live VMs use file system snapshots on the VM host, which establishes +crash-consistency for the VM images. This means that with most file systems +(that are journaling) the FS will always be fine in the backup (but may need a +journal replay to become accessible). + +Usually this does not mean that file *contents* on the VM are consistent, since file +contents are normally not journaled. Notable exceptions are ext4 in data=journal mode, +ZFS and btrfs (unless nodatacow is used). + +Applications designed with crash-consistency in mind (most relational databases +like PostgreSQL, SQLite etc. but also for example Borg repositories) should always +be able to recover to a consistent state from a backup created with +crash-consistent snapshots (even on ext4 with data=writeback or XFS). + +Hypervisor snapshots capturing most of the VM's state can also be used for backups +and can be a better alternative to pure file system based snapshots of the VM's disk, +since no state is lost. Depending on the application this can be the easiest and most +reliable way to create application-consistent backups. + +Other applications may require a lot of work to reach application-consistency: +It's a broad and complex issue that cannot be explained in entirety here. + +Borg doesn't intend to address these issues due to their huge complexity +and platform/software dependency. Combining Borg with the mechanisms provided +by the platform (snapshots, hypervisor features) will be the best approach +to start tackling them. + Can I backup from multiple servers into a single repository? ------------------------------------------------------------ From cf1c73b4f94fc8a4529d5e0c3f00ea167861fd51 Mon Sep 17 00:00:00 2001 From: Simon Heath Date: Sun, 2 Oct 2016 17:14:34 -0400 Subject: [PATCH 0265/1387] Added docs explaining multiple --restrict-to-path flags, with example (take 2) --- borg/archiver.py | 3 ++- docs/deployment.rst | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/borg/archiver.py b/borg/archiver.py index ce7655fc..785a98a6 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1011,7 +1011,8 @@ class Archiver: help='start repository server process') subparser.set_defaults(func=self.do_serve) subparser.add_argument('--restrict-to-path', dest='restrict_to_paths', action='append', - metavar='PATH', help='restrict repository access to PATH') + metavar='PATH', help='restrict repository access to PATH. ' + 'Can be specified multiple times to allow the client access to several directories.') subparser.add_argument('--append-only', dest='append_only', action='store_true', help='only allow appending to repository segment files') init_epilog = textwrap.dedent(""" diff --git a/docs/deployment.rst b/docs/deployment.rst index 3c76500f..b4794300 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -54,6 +54,11 @@ Restrictions Borg is instructed to restrict clients into their own paths: ``borg serve --restrict-to-path /home/backup/repos/`` +The client will be able to access any file or subdirectory inside of ``/home/backup/repos/`` +but no other directories. You can allow a client to access several directories by passing multiple +`--restrict-to-path` flags, for instance: ``borg serve --restrict-to-path /home/backup/repos//root --restrict-to-path /home/backup/repos//home``, +or instead simply use `--restrict-to-path` once to restrict the client to ``/home/backup/repos//*``. + There is only one ssh key per client allowed. Keys are added for ``johndoe.clnt.local``, ``web01.srv.local`` and ``app01.srv.local``. But they will access the backup under only one UNIX user account as: ``backup@backup01.srv.local``. Every key in ``$HOME/.ssh/authorized_keys`` has a From 27bc73c23e9c575c4bf89cf355844c2b123b558f Mon Sep 17 00:00:00 2001 From: enkore Date: Mon, 3 Oct 2016 14:53:16 +0200 Subject: [PATCH 0266/1387] borg info : print general repo information (#1680) --- src/borg/archiver.py | 14 ++++++++++++-- src/borg/key.py | 4 ++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 31b1f59d..e469fcc9 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -920,7 +920,7 @@ class Archiver: if any((args.location.archive, args.first, args.last, args.prefix)): return self._info_archives(args, repository, manifest, key, cache) else: - return self._info_repository(cache) + return self._info_repository(repository, key, cache) def _info_archives(self, args, repository, manifest, key, cache): def format_cmdline(cmdline): @@ -957,7 +957,17 @@ class Archiver: print() return self.exit_code - def _info_repository(self, cache): + def _info_repository(self, repository, key, cache): + print('Repository ID: %s' % bin_to_hex(repository.id)) + if key.NAME == 'plaintext': + encrypted = 'No' + else: + encrypted = 'Yes (%s)' % key.NAME + print('Encrypted: %s' % encrypted) + if key.NAME == 'key file': + print('Key file: %s' % key.find_key()) + print('Cache: %s' % cache.path) + print(DASHES) print(STATS_HEADER) print(str(cache)) return self.exit_code diff --git a/src/borg/key.py b/src/borg/key.py index a6199292..d2d43039 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -118,6 +118,7 @@ class KeyBase: class PlaintextKey(KeyBase): TYPE = 0x02 + NAME = 'plaintext' chunk_seed = 0 @@ -281,6 +282,7 @@ class PassphraseKey(AESKeyBase): # - --encryption=passphrase is an invalid argument now # This class is kept for a while to support migration from passphrase to repokey mode. TYPE = 0x01 + NAME = 'passphrase' iterations = 100000 # must not be changed ever! @classmethod @@ -432,6 +434,7 @@ class KeyfileKeyBase(AESKeyBase): class KeyfileKey(KeyfileKeyBase): TYPE = 0x00 + NAME = 'key file' FILE_ID = 'BORG_KEY' def sanity_check(self, filename, id): @@ -491,6 +494,7 @@ class KeyfileKey(KeyfileKeyBase): class RepoKey(KeyfileKeyBase): TYPE = 0x03 + NAME = 'repokey' def find_key(self): loc = self.repository._location.canonical_path() From 4c1a920ed46b4a17c465fe43b9dccfa1bb68a230 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 5 Oct 2016 12:27:26 +0200 Subject: [PATCH 0267/1387] debug delete/get obj: fix wrong reference to exception iow RemoteRepository doesn't define this type, only Repository does. --- borg/archiver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index ce7655fc..0b57e14c 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -767,7 +767,7 @@ class Archiver: else: try: data = repository.get(id) - except repository.ObjectNotFound: + except Repository.ObjectNotFound: print("object %s not found." % hex_id) else: with open(args.path, "wb") as f: @@ -801,7 +801,7 @@ class Archiver: repository.delete(id) modified = True print("object %s deleted." % hex_id) - except repository.ObjectNotFound: + except Repository.ObjectNotFound: print("object %s not found." % hex_id) if modified: repository.commit() From 90111363ba5f181da6b0f79a783fbf1ecee375de Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 23 Sep 2016 21:45:01 +0200 Subject: [PATCH 0268/1387] repo.list() yielding IDs in on-disk order --- src/borg/remote.py | 4 +++ src/borg/repository.py | 45 +++++++++++++++++++++++++++++++- src/borg/testsuite/repository.py | 18 +++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 294bd40c..d48fa949 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -62,6 +62,7 @@ class RepositoryServer: # pragma: no cover 'destroy', 'get', 'list', + 'scan', 'negotiate', 'open', 'put', @@ -467,6 +468,9 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. def list(self, limit=None, marker=None): return self.call('list', limit, marker) + def scan(self, limit=None, marker=None): + return self.call('scan', limit, marker) + def get(self, id_): for resp in self.get_many([id_]): return resp diff --git a/src/borg/repository.py b/src/borg/repository.py index 095a9ba7..e0f006c2 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -2,7 +2,7 @@ import errno import os import shutil import struct -from binascii import unhexlify +from binascii import hexlify, unhexlify from collections import defaultdict from configparser import ConfigParser from datetime import datetime @@ -750,10 +750,53 @@ class Repository: return id in self.index def list(self, limit=None, marker=None): + """ + list IDs starting from after id - in index (pseudo-random) order. + """ if not self.index: self.index = self.open_index(self.get_transaction_id()) return [id_ for id_, _ in islice(self.index.iteritems(marker=marker), limit)] + def scan(self, limit=None, marker=None): + """ + list IDs starting from after id - in on-disk order, so that a client + fetching data in this order does linear reads and reuses stuff from disk cache. + + We rely on repository.check() has run already (either now or some time before) and that: + - if we are called from a borg check command, self.index is a valid, fresh, in-sync repo index. + - if we are called from elsewhere, either self.index or the on-disk index is valid and in-sync. + - the repository segments are valid (no CRC errors). + if we encounter CRC errors in segment entry headers, rest of segment is skipped. + """ + if limit is not None and limit < 1: + raise ValueError('please use limit > 0 or limit = None') + if not self.index: + transaction_id = self.get_transaction_id() + self.index = self.open_index(transaction_id) + at_start = marker is None + # smallest valid seg is 0, smallest valid offs is 8 + marker_segment, marker_offset = (0, 0) if at_start else self.index[marker] + result = [] + for segment, filename in self.io.segment_iterator(): + if segment < marker_segment: + continue + obj_iterator = self.io.iter_objects(segment, read_data=False, include_data=False) + while True: + try: + tag, id, offset, size = next(obj_iterator) + except (StopIteration, IntegrityError): + # either end-of-segment or an error - we can not seek to objects at + # higher offsets than one that has an error in the header fields + break + if segment == marker_segment and offset <= marker_offset: + continue + if tag == TAG_PUT and (segment, offset) == self.index.get(id): + # we have found an existing and current object + result.append(id) + if len(result) == limit: + return result + return result + def get(self, id_): if not self.index: self.index = self.open_index(self.get_transaction_id()) diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 90c46f4d..dc6f656b 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -133,6 +133,7 @@ class RepositoryTestCase(RepositoryTestCaseBase): def test_list(self): for x in range(100): self.repository.put(H(x), b'SOMEDATA') + self.repository.commit() all = self.repository.list() self.assert_equal(len(all), 100) first_half = self.repository.list(limit=50) @@ -143,6 +144,23 @@ class RepositoryTestCase(RepositoryTestCaseBase): self.assert_equal(second_half, all[50:]) self.assert_equal(len(self.repository.list(limit=50)), 50) + def test_scan(self): + for x in range(100): + self.repository.put(H(x), b'SOMEDATA') + self.repository.commit() + all = self.repository.scan() + assert len(all) == 100 + first_half = self.repository.scan(limit=50) + assert len(first_half) == 50 + assert first_half == all[:50] + second_half = self.repository.scan(marker=first_half[-1]) + assert len(second_half) == 50 + assert second_half == all[50:] + assert len(self.repository.scan(limit=50)) == 50 + # check result order == on-disk order (which is hash order) + for x in range(100): + assert all[x] == H(x) + def test_max_data_size(self): max_data = b'x' * MAX_DATA_SIZE self.repository.put(H(0), max_data) From 6624ca9cdbecfa662777ffd61d6f2d13b7bdd9d7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 4 Oct 2016 04:55:10 +0200 Subject: [PATCH 0269/1387] verify_data: do a linear scan in disk-order --- src/borg/archive.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 16ab75e3..dd597a20 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1045,8 +1045,12 @@ class ArchiveChecker: errors = 0 defect_chunks = [] pi = ProgressIndicatorPercent(total=count, msg="Verifying data %6.2f%%", step=0.01) - for chunk_infos in chunkit(self.chunks.iteritems(), 100): - chunk_ids = [chunk_id for chunk_id, _ in chunk_infos] + marker = None + while True: + chunk_ids = self.repository.scan(limit=100, marker=marker) + if not chunk_ids: + break + marker = chunk_ids[-1] chunk_data_iter = self.repository.get_many(chunk_ids) chunk_ids_revd = list(reversed(chunk_ids)) while chunk_ids_revd: From cdb8d64fe2a32c3819d709b97bf75c48027de6de Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 4 Oct 2016 22:05:26 +0200 Subject: [PATCH 0270/1387] check for index vs. segment files object count mismatch --- src/borg/archive.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index dd597a20..a081261b 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1041,15 +1041,17 @@ class ArchiveChecker: def verify_data(self): logger.info('Starting cryptographic data integrity verification...') - count = len(self.chunks) + chunks_count_index = len(self.chunks) + chunks_count_segments = 0 errors = 0 defect_chunks = [] - pi = ProgressIndicatorPercent(total=count, msg="Verifying data %6.2f%%", step=0.01) + pi = ProgressIndicatorPercent(total=chunks_count_index, msg="Verifying data %6.2f%%", step=0.01) marker = None while True: chunk_ids = self.repository.scan(limit=100, marker=marker) if not chunk_ids: break + chunks_count_segments += len(chunk_ids) marker = chunk_ids[-1] chunk_data_iter = self.repository.get_many(chunk_ids) chunk_ids_revd = list(reversed(chunk_ids)) @@ -1078,6 +1080,10 @@ class ArchiveChecker: logger.error('chunk %s, integrity error: %s', bin_to_hex(chunk_id), integrity_error) defect_chunks.append(chunk_id) pi.finish() + if chunks_count_index != chunks_count_segments: + logger.error('Repo/Chunks index object count vs. segment files object count mismatch.') + logger.error('Repo/Chunks index: %d objects != segment files: %d objects', + chunks_count_index, chunks_count_segments) if defect_chunks: if self.repair: # if we kill the defect chunk here, subsequent actions within this "borg check" @@ -1110,7 +1116,8 @@ class ArchiveChecker: for defect_chunk in defect_chunks: logger.debug('chunk %s is defect.', bin_to_hex(defect_chunk)) log = logger.error if errors else logger.info - log('Finished cryptographic data integrity verification, verified %d chunks with %d integrity errors.', count, errors) + log('Finished cryptographic data integrity verification, verified %d chunks with %d integrity errors.', + chunks_count_segments, errors) def rebuild_manifest(self): """Rebuild the manifest object if it is missing From 573cb616d3979d2deae249565afddeb56457029c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 6 Oct 2016 01:00:07 +0200 Subject: [PATCH 0271/1387] deployment: synthesize alternative --restrict-to-path example --- borg/archiver.py | 3 ++- docs/deployment.rst | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 785a98a6..ab465e4f 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1012,7 +1012,8 @@ class Archiver: subparser.set_defaults(func=self.do_serve) subparser.add_argument('--restrict-to-path', dest='restrict_to_paths', action='append', metavar='PATH', help='restrict repository access to PATH. ' - 'Can be specified multiple times to allow the client access to several directories.') + 'Can be specified multiple times to allow the client access to several directories. ' + 'Access to all sub-directories is granted implicitly; PATH doesn\'t need to directly point to a repository.') subparser.add_argument('--append-only', dest='append_only', action='store_true', help='only allow appending to repository segment files') init_epilog = textwrap.dedent(""" diff --git a/docs/deployment.rst b/docs/deployment.rst index b4794300..c73c6ddb 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -55,9 +55,10 @@ Borg is instructed to restrict clients into their own paths: ``borg serve --restrict-to-path /home/backup/repos/`` The client will be able to access any file or subdirectory inside of ``/home/backup/repos/`` -but no other directories. You can allow a client to access several directories by passing multiple -`--restrict-to-path` flags, for instance: ``borg serve --restrict-to-path /home/backup/repos//root --restrict-to-path /home/backup/repos//home``, -or instead simply use `--restrict-to-path` once to restrict the client to ``/home/backup/repos//*``. +but no other directories. You can allow a client to access several separate directories by passing multiple +`--restrict-to-path` flags, for instance: ``borg serve --restrict-to-path /home/backup/repos/ --restrict-to-path /home/backup/repos/``, +which could make sense if multiple machines belong to one person which should then have access to all the +backups of their machines. There is only one ssh key per client allowed. Keys are added for ``johndoe.clnt.local``, ``web01.srv.local`` and ``app01.srv.local``. But they will access the backup under only one UNIX user account as: From 67aafec1951e900bcb6cfe6edff1bd83d465f351 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 6 Oct 2016 05:15:01 +0200 Subject: [PATCH 0272/1387] backport bin_to_hex and use it simplifies code and also porting and merging between 1.0 and 1.1/master. --- borg/archive.py | 9 ++++----- borg/archiver.py | 10 +++++----- borg/cache.py | 16 ++++++++-------- borg/helpers.py | 5 +++++ borg/key.py | 6 +++--- borg/keymanager.py | 14 +++++++------- borg/repository.py | 6 +++--- borg/testsuite/archiver.py | 10 +++++----- borg/upgrader.py | 11 +++++------ 9 files changed, 45 insertions(+), 42 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index 8619fd8f..e725857e 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -1,4 +1,3 @@ -from binascii import hexlify from contextlib import contextmanager from datetime import datetime, timezone from getpass import getuser @@ -17,7 +16,7 @@ import sys import time from io import BytesIO from . import xattr -from .helpers import Error, uid2user, user2uid, gid2group, group2gid, \ +from .helpers import Error, uid2user, user2uid, gid2group, group2gid, bin_to_hex, \ parse_timestamp, to_localtime, format_time, format_timedelta, remove_surrogates, \ Manifest, Statistics, decode_dict, make_path_safe, StableDict, int_to_bigint, bigint_to_int, \ ProgressIndicatorPercent @@ -254,7 +253,7 @@ class Archive: @property def fpr(self): - return hexlify(self.id).decode('ascii') + return bin_to_hex(self.id) @property def duration(self): @@ -522,7 +521,7 @@ Number of files: {0.stats.nfiles}'''.format( try: self.cache.chunk_decref(id, stats) except KeyError: - cid = hexlify(id).decode('ascii') + cid = bin_to_hex(id) raise ChunksIndexError(cid) except Repository.ObjectNotFound as e: # object not in repo - strange, but we wanted to delete it anyway. @@ -1010,7 +1009,7 @@ class ArchiveChecker: return _state def report(msg, chunk_id, chunk_no): - cid = hexlify(chunk_id).decode('ascii') + cid = bin_to_hex(chunk_id) msg += ' [chunk: %06d_%s]' % (chunk_no, cid) # see debug-dump-archive-items self.error_found = True logger.error(msg) diff --git a/borg/archiver.py b/borg/archiver.py index ab465e4f..fb5c14db 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1,4 +1,4 @@ -from binascii import hexlify, unhexlify +from binascii import unhexlify from datetime import datetime from hashlib import sha256 from operator import attrgetter @@ -18,7 +18,7 @@ import collections from . import __version__ from .helpers import Error, location_validator, archivename_validator, format_line, format_time, format_file_size, \ - parse_pattern, PathPrefixPattern, to_localtime, timestamp, safe_timestamp, \ + parse_pattern, PathPrefixPattern, to_localtime, timestamp, safe_timestamp, bin_to_hex, \ get_cache_dir, prune_within, prune_split, \ Manifest, NoManifestError, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \ dir_is_tagged, bigint_to_int, ChunkerParams, CompressionSpec, PrefixSpec, is_slow_msgpack, yes, sysinfo, \ @@ -631,7 +631,7 @@ class Archiver: """Show archive details such as disk space used""" stats = archive.calc_stats(cache) print('Name:', archive.name) - print('Fingerprint: %s' % hexlify(archive.id).decode('ascii')) + print('Fingerprint: %s' % bin_to_hex(archive.id)) print('Hostname:', archive.metadata[b'hostname']) print('Username:', archive.metadata[b'username']) print('Time (start): %s' % format_time(to_localtime(archive.ts))) @@ -727,7 +727,7 @@ class Archiver: archive = Archive(repository, key, manifest, args.location.archive) for i, item_id in enumerate(archive.metadata[b'items']): data = key.decrypt(item_id, repository.get(item_id)) - filename = '%06d_%s.items' % (i, hexlify(item_id).decode('ascii')) + filename = '%06d_%s.items' % (i, bin_to_hex(item_id)) print('Dumping', filename) with open(filename, 'wb') as fd: fd.write(data) @@ -748,7 +748,7 @@ class Archiver: cdata = repository.get(id) give_id = id if id != Manifest.MANIFEST_ID else None data = key.decrypt(give_id, cdata) - filename = '%06d_%s.obj' % (i, hexlify(id).decode('ascii')) + filename = '%06d_%s.obj' % (i, bin_to_hex(id)) print('Dumping', filename) with open(filename, 'wb') as fd: fd.write(data) diff --git a/borg/cache.py b/borg/cache.py index b843fc49..e293d9e1 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -3,14 +3,14 @@ from .remote import cache_if_remote from collections import namedtuple import os import stat -from binascii import hexlify, unhexlify +from binascii import unhexlify import shutil from .key import PlaintextKey from .logger import create_logger logger = create_logger() from .helpers import Error, get_cache_dir, decode_dict, int_to_bigint, \ - bigint_to_int, format_file_size, yes + bigint_to_int, format_file_size, yes, bin_to_hex from .locking import Lock from .hashindex import ChunkIndex @@ -34,13 +34,13 @@ class Cache: @staticmethod def break_lock(repository, path=None): - path = path or os.path.join(get_cache_dir(), hexlify(repository.id).decode('ascii')) + path = path or os.path.join(get_cache_dir(), bin_to_hex(repository.id)) Lock(os.path.join(path, 'lock'), exclusive=True).break_lock() @staticmethod def destroy(repository, path=None): """destroy the cache for ``repository`` or at ``path``""" - path = path or os.path.join(get_cache_dir(), hexlify(repository.id).decode('ascii')) + path = path or os.path.join(get_cache_dir(), bin_to_hex(repository.id)) config = os.path.join(path, 'config') if os.path.exists(config): os.remove(config) # kill config first @@ -55,7 +55,7 @@ class Cache: self.repository = repository self.key = key self.manifest = manifest - self.path = path or os.path.join(get_cache_dir(), hexlify(repository.id).decode('ascii')) + self.path = path or os.path.join(get_cache_dir(), bin_to_hex(repository.id)) self.do_files = do_files # Warn user before sending data to a never seen before unencrypted repository if not os.path.exists(self.path): @@ -122,7 +122,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" config = configparser.ConfigParser(interpolation=None) config.add_section('cache') config.set('cache', 'version', '1') - config.set('cache', 'repository', hexlify(self.repository.id).decode('ascii')) + config.set('cache', 'repository', bin_to_hex(self.repository.id)) config.set('cache', 'manifest', '') with open(os.path.join(self.path, 'config'), 'w') as fd: config.write(fd) @@ -208,7 +208,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if age == 0 and bigint_to_int(item[3]) < self._newest_mtime or \ age > 0 and age < ttl: msgpack.pack((path_hash, item), fd) - self.config.set('cache', 'manifest', hexlify(self.manifest.id).decode('ascii')) + self.config.set('cache', 'manifest', bin_to_hex(self.manifest.id)) self.config.set('cache', 'timestamp', self.manifest.timestamp) self.config.set('cache', 'key_type', str(self.key.TYPE)) self.config.set('cache', 'previous_location', self.repository._location.canonical_path()) @@ -251,7 +251,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" archive_path = os.path.join(self.path, 'chunks.archive.d') def mkpath(id, suffix=''): - id_hex = hexlify(id).decode('ascii') + id_hex = bin_to_hex(id) path = os.path.join(archive_path, id_hex + suffix) return path.encode('utf-8') diff --git a/borg/helpers.py b/borg/helpers.py index db579d34..a452e17a 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -1,4 +1,5 @@ import argparse +from binascii import hexlify from collections import namedtuple import contextlib from functools import wraps @@ -759,6 +760,10 @@ def safe_encode(s, coding='utf-8', errors='surrogateescape'): return s.encode(coding, errors) +def bin_to_hex(binary): + return hexlify(binary).decode('ascii') + + class Location: """Object representing a repository / archive location """ diff --git a/borg/key.py b/borg/key.py index e88baf57..648d2193 100644 --- a/borg/key.py +++ b/borg/key.py @@ -7,7 +7,7 @@ import textwrap from hmac import HMAC, compare_digest from hashlib import sha256, pbkdf2_hmac -from .helpers import IntegrityError, get_keys_dir, Error, yes +from .helpers import IntegrityError, get_keys_dir, Error, yes, bin_to_hex from .logger import create_logger logger = create_logger() @@ -201,7 +201,7 @@ class Passphrase(str): passphrase.encode('ascii') except UnicodeEncodeError: print('Your passphrase (UTF-8 encoding in hex): %s' % - hexlify(passphrase.encode('utf-8')).decode('ascii'), + bin_to_hex(passphrase.encode('utf-8')), file=sys.stderr) print('As you have a non-ASCII passphrase, it is recommended to keep the UTF-8 encoding in hex together with the passphrase at a safe place.', file=sys.stderr) @@ -427,7 +427,7 @@ class KeyfileKey(KeyfileKeyBase): def save(self, target, passphrase): key_data = self._save(passphrase) with open(target, 'w') as fd: - fd.write('%s %s\n' % (self.FILE_ID, hexlify(self.repository_id).decode('ascii'))) + fd.write('%s %s\n' % (self.FILE_ID, bin_to_hex(self.repository_id))) fd.write(key_data) fd.write('\n') self.target = target diff --git a/borg/keymanager.py b/borg/keymanager.py index 8eef581d..0b365e82 100644 --- a/borg/keymanager.py +++ b/borg/keymanager.py @@ -1,10 +1,10 @@ -from binascii import hexlify, unhexlify, a2b_base64, b2a_base64 +from binascii import unhexlify, a2b_base64, b2a_base64 import binascii import textwrap from hashlib import sha256 from .key import KeyfileKey, RepoKey, PassphraseKey, KeyfileNotFoundError, PlaintextKey -from .helpers import Manifest, NoManifestError, Error, yes +from .helpers import Manifest, NoManifestError, Error, yes, bin_to_hex from .repository import Repository @@ -79,7 +79,7 @@ class KeyManager: def store_keyfile(self, target): with open(target, 'w') as fd: - fd.write('%s %s\n' % (KeyfileKey.FILE_ID, hexlify(self.repository.id).decode('ascii'))) + fd.write('%s %s\n' % (KeyfileKey.FILE_ID, bin_to_hex(self.repository.id))) fd.write(self.keyblob) if not self.keyblob.endswith('\n'): fd.write('\n') @@ -103,7 +103,7 @@ class KeyManager: binary = a2b_base64(self.keyblob) export += 'BORG PAPER KEY v1\n' lines = (len(binary) + 17) // 18 - repoid = hexlify(self.repository.id).decode('ascii')[:18] + repoid = bin_to_hex(self.repository.id)[:18] complete_checksum = sha256_truncated(binary, 12) export += 'id: {0:d} / {1} / {2} - {3}\n'.format(lines, grouped(repoid), @@ -114,7 +114,7 @@ class KeyManager: idx += 1 binline = binary[:18] checksum = sha256_truncated(idx.to_bytes(2, byteorder='big') + binline, 2) - export += '{0:2d}: {1} - {2}\n'.format(idx, grouped(hexlify(binline).decode('ascii')), checksum) + export += '{0:2d}: {1} - {2}\n'.format(idx, grouped(bin_to_hex(binline)), checksum) binary = binary[18:] if path: @@ -125,7 +125,7 @@ class KeyManager: def import_keyfile(self, args): file_id = KeyfileKey.FILE_ID - first_line = file_id + ' ' + hexlify(self.repository.id).decode('ascii') + '\n' + first_line = file_id + ' ' + bin_to_hex(self.repository.id) + '\n' with open(args.path, 'r') as fd: file_first_line = fd.read(len(first_line)) if file_first_line != first_line: @@ -141,7 +141,7 @@ class KeyManager: # imported here because it has global side effects import readline - repoid = hexlify(self.repository.id).decode('ascii')[:18] + repoid = bin_to_hex(self.repository.id)[:18] try: while True: # used for repeating on overall checksum mismatch # id line input diff --git a/borg/repository.py b/borg/repository.py index fae22119..fa6458c6 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -1,5 +1,5 @@ from configparser import ConfigParser -from binascii import hexlify, unhexlify +from binascii import unhexlify from datetime import datetime from itertools import islice import errno @@ -12,7 +12,7 @@ import struct from zlib import crc32 import msgpack -from .helpers import Error, ErrorWithTraceback, IntegrityError, Location, ProgressIndicatorPercent +from .helpers import Error, ErrorWithTraceback, IntegrityError, Location, ProgressIndicatorPercent, bin_to_hex from .hashindex import NSIndex from .locking import Lock, LockError, LockErrorT from .lrucache import LRUCache @@ -109,7 +109,7 @@ class Repository: config.set('repository', 'segments_per_dir', str(self.DEFAULT_SEGMENTS_PER_DIR)) config.set('repository', 'max_segment_size', str(self.DEFAULT_MAX_SEGMENT_SIZE)) config.set('repository', 'append_only', str(int(self.append_only))) - config.set('repository', 'id', hexlify(os.urandom(32)).decode('ascii')) + config.set('repository', 'id', bin_to_hex(os.urandom(32))) self.save_config(path, config) def save_config(self, path, config): diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index e7f805ed..706319bd 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1,4 +1,4 @@ -from binascii import hexlify, unhexlify, b2a_base64 +from binascii import unhexlify, b2a_base64 from configparser import ConfigParser import errno import os @@ -21,7 +21,7 @@ from ..archive import Archive, ChunkBuffer, CHUNK_MAX_EXP, flags_noatime, flags_ from ..archiver import Archiver from ..cache import Cache from ..crypto import bytes_to_long, num_aes_blocks -from ..helpers import Manifest, PatternMatcher, parse_pattern, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR +from ..helpers import Manifest, PatternMatcher, parse_pattern, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, bin_to_hex from ..key import RepoKey, KeyfileKey, Passphrase from ..keymanager import RepoIdMismatch, NotABorgKeyFile from ..remote import RemoteRepository, PathNotAllowed @@ -409,7 +409,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def _set_repository_id(self, path, id): config = ConfigParser(interpolation=None) config.read(os.path.join(path, 'config')) - config.set('repository', 'id', hexlify(id).decode('ascii')) + config.set('repository', 'id', bin_to_hex(id)) with open(os.path.join(path, 'config'), 'w') as fd: config.write(fd) with Repository(self.repository_path) as repository: @@ -1205,7 +1205,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with open(export_file, 'r') as fd: export_contents = fd.read() - assert export_contents.startswith('BORG_KEY ' + hexlify(repo_id).decode() + '\n') + assert export_contents.startswith('BORG_KEY ' + bin_to_hex(repo_id) + '\n') key_file = self.keys_path + '/' + os.listdir(self.keys_path)[0] @@ -1232,7 +1232,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with open(export_file, 'r') as fd: export_contents = fd.read() - assert export_contents.startswith('BORG_KEY ' + hexlify(repo_id).decode() + '\n') + assert export_contents.startswith('BORG_KEY ' + bin_to_hex(repo_id) + '\n') with Repository(self.repository_path) as repository: repo_key = RepoKey(repository) diff --git a/borg/upgrader.py b/borg/upgrader.py index f4327e34..c6700262 100644 --- a/borg/upgrader.py +++ b/borg/upgrader.py @@ -1,4 +1,3 @@ -from binascii import hexlify import datetime import logging logger = logging.getLogger(__name__) @@ -6,7 +5,7 @@ import os import shutil import time -from .helpers import get_keys_dir, get_cache_dir, ProgressIndicatorPercent +from .helpers import get_keys_dir, get_cache_dir, ProgressIndicatorPercent, bin_to_hex from .locking import Lock from .repository import Repository, MAGIC from .key import KeyfileKey, KeyfileNotFoundError @@ -188,8 +187,8 @@ class AtticRepositoryUpgrader(Repository): attic_cache_dir = os.environ.get('ATTIC_CACHE_DIR', os.path.join(os.path.expanduser('~'), '.cache', 'attic')) - attic_cache_dir = os.path.join(attic_cache_dir, hexlify(self.id).decode('ascii')) - borg_cache_dir = os.path.join(get_cache_dir(), hexlify(self.id).decode('ascii')) + attic_cache_dir = os.path.join(attic_cache_dir, bin_to_hex(self.id)) + borg_cache_dir = os.path.join(get_cache_dir(), bin_to_hex(self.id)) def copy_cache_file(path): """copy the given attic cache path into the borg directory @@ -263,7 +262,7 @@ class AtticKeyfileKey(KeyfileKey): assume the repository has been opened by the archiver yet """ get_keys_dir = cls.get_keys_dir - id = hexlify(repository.id).decode('ascii') + id = bin_to_hex(repository.id) keys_dir = get_keys_dir() if not os.path.exists(keys_dir): raise KeyfileNotFoundError(repository.path, keys_dir) @@ -313,7 +312,7 @@ class Borg0xxKeyfileKey(KeyfileKey): @classmethod def find_key_file(cls, repository): get_keys_dir = cls.get_keys_dir - id = hexlify(repository.id).decode('ascii') + id = bin_to_hex(repository.id) keys_dir = get_keys_dir() if not os.path.exists(keys_dir): raise KeyfileNotFoundError(repository.path, keys_dir) From e9605d67adebe7a5f01b66bcaf9ea35709e26b15 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Fri, 5 Aug 2016 21:45:12 +0200 Subject: [PATCH 0273/1387] RemoteReposity: prefetch can only be 'get'. --- src/borg/remote.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index d48fa949..b45fb9ec 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -424,6 +424,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. while not self.to_send and (calls or self.preload_ids) and len(waiting_for) < MAX_INFLIGHT: if calls: if is_preloaded: + assert cmd == "get", "is_preload is only supported for 'get'" if calls[0] in self.cache: waiting_for.append(fetch_from_cache(calls.pop(0))) else: @@ -438,7 +439,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. args = (self.preload_ids.pop(0),) self.msgid += 1 self.cache.setdefault(args, []).append(self.msgid) - self.to_send = msgpack.packb((1, self.msgid, cmd, args)) + self.to_send = msgpack.packb((1, self.msgid, 'get', args)) if self.to_send: try: From 2608a5620a3cb817271518d71977a8dc9c14b0dc Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Fri, 5 Aug 2016 21:48:23 +0200 Subject: [PATCH 0274/1387] RemoteRepository: Always store chunk ids in cache instead of rpc argument encoding of get request. --- src/borg/remote.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index b45fb9ec..02993367 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -425,20 +425,21 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. if calls: if is_preloaded: assert cmd == "get", "is_preload is only supported for 'get'" - if calls[0] in self.cache: - waiting_for.append(fetch_from_cache(calls.pop(0))) + if calls[0][0] in self.cache: + waiting_for.append(fetch_from_cache(calls.pop(0)[0])) else: args = calls.pop(0) - if cmd == 'get' and args in self.cache: - waiting_for.append(fetch_from_cache(args)) + if cmd == 'get' and args[0] in self.cache: + waiting_for.append(fetch_from_cache(args[0])) else: self.msgid += 1 waiting_for.append(self.msgid) self.to_send = msgpack.packb((1, self.msgid, cmd, args)) if not self.to_send and self.preload_ids: - args = (self.preload_ids.pop(0),) + chunk_id = self.preload_ids.pop(0) + args = (chunk_id,) self.msgid += 1 - self.cache.setdefault(args, []).append(self.msgid) + self.cache.setdefault(chunk_id, []).append(self.msgid) self.to_send = msgpack.packb((1, self.msgid, 'get', args)) if self.to_send: From 02557f16b05fb733509e906b9a4224dcb2ca755b Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Fri, 5 Aug 2016 21:51:01 +0200 Subject: [PATCH 0275/1387] RemoteRepository: Rename cache to chunkid_to_msgids. Also fetch_from_cache to pop_preload_msgid. --- src/borg/remote.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 02993367..824161f0 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -216,7 +216,7 @@ class RemoteRepository: self.preload_ids = [] self.msgid = 0 self.to_send = b'' - self.cache = {} + self.chunkid_to_msgids = {} self.ignore_responses = set() self.responses = {} self.ratelimit = SleepingBandwidthLimiter(args.remote_ratelimit * 1024 if args and args.remote_ratelimit else 0) @@ -350,10 +350,10 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. if not calls: return - def fetch_from_cache(args): - msgid = self.cache[args].pop(0) - if not self.cache[args]: - del self.cache[args] + def pop_preload_msgid(chunkid): + msgid = self.chunkid_to_msgids[chunkid].pop(0) + if not self.chunkid_to_msgids[chunkid]: + del self.chunkid_to_msgids[chunkid] return msgid def handle_error(error, res): @@ -425,12 +425,12 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. if calls: if is_preloaded: assert cmd == "get", "is_preload is only supported for 'get'" - if calls[0][0] in self.cache: - waiting_for.append(fetch_from_cache(calls.pop(0)[0])) + if calls[0][0] in self.chunkid_to_msgids: + waiting_for.append(pop_preload_msgid(calls.pop(0)[0])) else: args = calls.pop(0) - if cmd == 'get' and args[0] in self.cache: - waiting_for.append(fetch_from_cache(args[0])) + if cmd == 'get' and args[0] in self.chunkid_to_msgids: + waiting_for.append(pop_preload_msgid(args[0])) else: self.msgid += 1 waiting_for.append(self.msgid) @@ -439,7 +439,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. chunk_id = self.preload_ids.pop(0) args = (chunk_id,) self.msgid += 1 - self.cache.setdefault(chunk_id, []).append(self.msgid) + self.chunkid_to_msgids.setdefault(chunk_id, []).append(self.msgid) self.to_send = msgpack.packb((1, self.msgid, 'get', args)) if self.to_send: From 5f337e2c9c7015d17e7ba941e2ffa44d599a1a7e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 6 Oct 2016 22:46:37 +0200 Subject: [PATCH 0276/1387] borg.key: include chunk id in exception msgs this is a backport of bcdce91dfb2883c139011322a9e8086059fbe5c2 improvements on the exception msgs. --- borg/key.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/borg/key.py b/borg/key.py index 648d2193..7b65d909 100644 --- a/borg/key.py +++ b/borg/key.py @@ -105,10 +105,10 @@ class PlaintextKey(KeyBase): def decrypt(self, id, data): if data[0] != self.TYPE: - raise IntegrityError('Invalid encryption envelope') + raise IntegrityError('Chunk %s: Invalid encryption envelope' % bin_to_hex(id)) data = self.compressor.decompress(memoryview(data)[1:]) if id and sha256(data).digest() != id: - raise IntegrityError('Chunk id verification failed') + raise IntegrityError('Chunk %s: id verification failed' % bin_to_hex(id)) return data @@ -142,24 +142,24 @@ class AESKeyBase(KeyBase): def decrypt(self, id, data): if not (data[0] == self.TYPE or data[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)): - raise IntegrityError('Invalid encryption envelope') + raise IntegrityError('Chunk %s: Invalid encryption envelope' % bin_to_hex(id)) hmac_given = memoryview(data)[1:33] hmac_computed = memoryview(HMAC(self.enc_hmac_key, memoryview(data)[33:], sha256).digest()) if not compare_digest(hmac_computed, hmac_given): - raise IntegrityError('Encryption envelope checksum mismatch') + raise IntegrityError('Chunk %s: Encryption envelope checksum mismatch' % bin_to_hex(id)) self.dec_cipher.reset(iv=PREFIX + data[33:41]) data = self.compressor.decompress(self.dec_cipher.decrypt(data[41:])) if id: hmac_given = id hmac_computed = HMAC(self.id_key, data, sha256).digest() if not compare_digest(hmac_computed, hmac_given): - raise IntegrityError('Chunk id verification failed') + raise IntegrityError('Chunk %s: Chunk id verification failed' % bin_to_hex(id)) return data def extract_nonce(self, payload): if not (payload[0] == self.TYPE or payload[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)): - raise IntegrityError('Invalid encryption envelope') + raise IntegrityError('Manifest: Invalid encryption envelope') nonce = bytes_to_long(payload[33:41]) return nonce From 75624f8e05a6b3e9edf68b0b9dbbb4823daaef01 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 6 Oct 2016 22:58:02 +0200 Subject: [PATCH 0277/1387] vagrant: update image name of boxcutter debian7 boxes debian711* is 404. --- Vagrantfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 60f1b02a..b6af51a2 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -385,7 +385,7 @@ Vagrant.configure(2) do |config| end config.vm.define "wheezy32" do |b| - b.vm.box = "boxcutter/debian711-i386" + b.vm.box = "boxcutter/debian7-i386" b.vm.provision "packages prepare wheezy", :type => :shell, :inline => packages_prepare_wheezy b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("wheezy32") @@ -398,7 +398,7 @@ Vagrant.configure(2) do |config| end config.vm.define "wheezy64" do |b| - b.vm.box = "boxcutter/debian711" + b.vm.box = "boxcutter/debian7" b.vm.provision "packages prepare wheezy", :type => :shell, :inline => packages_prepare_wheezy b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("wheezy64") From 7434010cdd4ad524c5ac181299f04faa051ba9ad Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 6 Oct 2016 23:36:22 +0200 Subject: [PATCH 0278/1387] IntegrityError: add placeholder for message, fixes #1572 So that the message we give appears not only in the traceback, but also in the (short) error message. --- borg/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/borg/helpers.py b/borg/helpers.py index a452e17a..f7bc54c3 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -64,7 +64,7 @@ class ErrorWithTraceback(Error): class IntegrityError(ErrorWithTraceback): - """Data integrity error""" + """Data integrity error: {}""" class ExtensionModuleError(Error): From f31112b975e504c12ec9fa6fc1f30be10b03ec13 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 7 Oct 2016 04:09:05 +0200 Subject: [PATCH 0279/1387] clarify borg diff help, fixes #980 --- src/borg/archiver.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e469fcc9..71ed6d6f 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1937,10 +1937,11 @@ class Archiver: help='paths to extract; patterns are supported') diff_epilog = textwrap.dedent(""" - This command finds differences in files (contents, user, group, mode) between archives. + This command finds differences (file contents, user/group/mode) between archives. - Both archives need to be in the same repository, and a repository location may only - be specified for ARCHIVE1. + A repository location and an archive name must be specified for REPO_ARCHIVE1. + ARCHIVE2 is just another archive name in same repository (no repository location + allowed). For archives created with Borg 1.1 or newer diff automatically detects whether the archives are created with the same chunker params. If so, only chunk IDs @@ -1974,14 +1975,14 @@ class Archiver: subparser.add_argument('--sort', dest='sort', action='store_true', default=False, help='Sort the output lines by file path.') - subparser.add_argument('location', metavar='ARCHIVE1', + subparser.add_argument('location', metavar='REPO_ARCHIVE1', type=location_validator(archive=True), - help='archive') + help='repository location and ARCHIVE1 name') subparser.add_argument('archive2', metavar='ARCHIVE2', type=archivename_validator(), - help='archive to compare with ARCHIVE1 (no repository location)') + help='ARCHIVE2 name (no repository location allowed)') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, - help='paths to compare; patterns are supported') + help='paths of items inside the archives to compare; patterns are supported') rename_epilog = textwrap.dedent(""" This command renames an archive in the repository. From b88e82d99d1336ee5bb7b1601483405251b16759 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 10 Oct 2016 00:22:01 +0200 Subject: [PATCH 0280/1387] remove debug-xxx commands, fixes #1627 we use "debug xxx" subcommands now. docs updated. also makes "borg help" shorter as not all debug-xxx commands show up, but just 1 main "debug" command. --- docs/usage.rst | 4 +-- src/borg/archive.py | 2 +- src/borg/archiver.py | 65 ---------------------------------- src/borg/testsuite/archiver.py | 14 ++++---- 4 files changed, 10 insertions(+), 75 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 41d83040..685e07fa 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -660,10 +660,10 @@ Miscellaneous Help Debug Commands -------------- -There are some more commands (all starting with "debug-") which are all +There is a ``borg debug`` command that has some subcommands which are all **not intended for normal use** and **potentially very dangerous** if used incorrectly. -For example, ``borg debug-put-obj`` and ``borg debug-delete-obj`` will only do +For example, ``borg debug put-obj`` and ``borg debug delete-obj`` will only do what their name suggests: put objects into repo / delete objects from repo. Please note: diff --git a/src/borg/archive.py b/src/borg/archive.py index a081261b..621ad50b 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1256,7 +1256,7 @@ class ArchiveChecker: def report(msg, chunk_id, chunk_no): cid = bin_to_hex(chunk_id) - msg += ' [chunk: %06d_%s]' % (chunk_no, cid) # see debug-dump-archive-items + msg += ' [chunk: %06d_%s]' % (chunk_no, cid) # see "debug dump-archive-items" self.error_found = True logger.error(msg) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 71ed6d6f..6d1fcf03 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2467,13 +2467,6 @@ class Archiver: reports and debugging problems. If a traceback happens, this information is already appended at the end of the traceback. """) - subparser = subparsers.add_parser('debug-info', parents=[common_parser], add_help=False, - description=self.do_debug_info.__doc__, - epilog=debug_info_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help='show system infos for debugging / bug reports (debug)') - subparser.set_defaults(func=self.do_debug_info) - subparser = debug_parsers.add_parser('info', parents=[common_parser], add_help=False, description=self.do_debug_info.__doc__, epilog=debug_info_epilog, @@ -2484,16 +2477,6 @@ class Archiver: debug_dump_archive_items_epilog = textwrap.dedent(""" This command dumps raw (but decrypted and decompressed) archive items (only metadata) to files. """) - subparser = subparsers.add_parser('debug-dump-archive-items', parents=[common_parser], add_help=False, - description=self.do_debug_dump_archive_items.__doc__, - epilog=debug_dump_archive_items_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help='dump archive items (metadata) (debug)') - subparser.set_defaults(func=self.do_debug_dump_archive_items) - subparser.add_argument('location', metavar='ARCHIVE', - type=location_validator(archive=True), - help='archive to dump') - subparser = debug_parsers.add_parser('dump-archive-items', parents=[common_parser], add_help=False, description=self.do_debug_dump_archive_items.__doc__, epilog=debug_dump_archive_items_epilog, @@ -2507,16 +2490,6 @@ class Archiver: debug_dump_repo_objs_epilog = textwrap.dedent(""" This command dumps raw (but decrypted and decompressed) repo objects to files. """) - subparser = subparsers.add_parser('debug-dump-repo-objs', parents=[common_parser], add_help=False, - description=self.do_debug_dump_repo_objs.__doc__, - epilog=debug_dump_repo_objs_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help='dump repo objects (debug)') - subparser.set_defaults(func=self.do_debug_dump_repo_objs) - subparser.add_argument('location', metavar='REPOSITORY', - type=location_validator(archive=False), - help='repo to dump') - subparser = debug_parsers.add_parser('dump-repo-objs', parents=[common_parser], add_help=False, description=self.do_debug_dump_repo_objs.__doc__, epilog=debug_dump_repo_objs_epilog, @@ -2530,20 +2503,6 @@ class Archiver: debug_get_obj_epilog = textwrap.dedent(""" This command gets an object from the repository. """) - subparser = subparsers.add_parser('debug-get-obj', parents=[common_parser], add_help=False, - description=self.do_debug_get_obj.__doc__, - epilog=debug_get_obj_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help='get object from repository (debug)') - subparser.set_defaults(func=self.do_debug_get_obj) - subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', - type=location_validator(archive=False), - help='repository to use') - subparser.add_argument('id', metavar='ID', type=str, - help='hex object ID to get from the repo') - subparser.add_argument('path', metavar='PATH', type=str, - help='file to write object data into') - subparser = debug_parsers.add_parser('get-obj', parents=[common_parser], add_help=False, description=self.do_debug_get_obj.__doc__, epilog=debug_get_obj_epilog, @@ -2561,18 +2520,6 @@ class Archiver: debug_put_obj_epilog = textwrap.dedent(""" This command puts objects into the repository. """) - subparser = subparsers.add_parser('debug-put-obj', parents=[common_parser], add_help=False, - description=self.do_debug_put_obj.__doc__, - epilog=debug_put_obj_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help='put object to repository (debug)') - subparser.set_defaults(func=self.do_debug_put_obj) - subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', - type=location_validator(archive=False), - help='repository to use') - subparser.add_argument('paths', metavar='PATH', nargs='+', type=str, - help='file(s) to read and create object(s) from') - subparser = debug_parsers.add_parser('put-obj', parents=[common_parser], add_help=False, description=self.do_debug_put_obj.__doc__, epilog=debug_put_obj_epilog, @@ -2588,18 +2535,6 @@ class Archiver: debug_delete_obj_epilog = textwrap.dedent(""" This command deletes objects from the repository. """) - subparser = subparsers.add_parser('debug-delete-obj', parents=[common_parser], add_help=False, - description=self.do_debug_delete_obj.__doc__, - epilog=debug_delete_obj_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help='delete object from repository (debug)') - subparser.set_defaults(func=self.do_debug_delete_obj) - subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', - type=location_validator(archive=False), - help='repository to use') - subparser.add_argument('ids', metavar='IDs', nargs='+', type=str, - help='hex object ID(s) to delete from the repo') - subparser = debug_parsers.add_parser('delete-obj', parents=[common_parser], add_help=False, description=self.do_debug_delete_obj.__doc__, epilog=debug_delete_obj_epilog, diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index cb79d6b2..37fa7965 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1547,7 +1547,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') with changedir('output'): - output = self.cmd('debug-dump-archive-items', self.repository_location + '::test') + output = self.cmd('debug', 'dump-archive-items', self.repository_location + '::test') output_dir = sorted(os.listdir('output')) assert len(output_dir) > 0 and output_dir[0].startswith('000000_') assert 'Done.' in output @@ -1557,7 +1557,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') with changedir('output'): - output = self.cmd('debug-dump-repo-objs', self.repository_location) + output = self.cmd('debug', 'dump-repo-objs', self.repository_location) output_dir = sorted(os.listdir('output')) assert len(output_dir) > 0 and output_dir[0].startswith('000000_') assert 'Done.' in output @@ -1567,18 +1567,18 @@ class ArchiverTestCase(ArchiverTestCaseBase): data = b'some data' hexkey = sha256(data).hexdigest() self.create_regular_file('file', contents=data) - output = self.cmd('debug-put-obj', self.repository_location, 'input/file') + output = self.cmd('debug', 'put-obj', self.repository_location, 'input/file') assert hexkey in output - output = self.cmd('debug-get-obj', self.repository_location, hexkey, 'output/file') + output = self.cmd('debug', 'get-obj', self.repository_location, hexkey, 'output/file') assert hexkey in output with open('output/file', 'rb') as f: data_read = f.read() assert data == data_read - output = self.cmd('debug-delete-obj', self.repository_location, hexkey) + output = self.cmd('debug', 'delete-obj', self.repository_location, hexkey) assert "deleted" in output - output = self.cmd('debug-delete-obj', self.repository_location, hexkey) + output = self.cmd('debug', 'delete-obj', self.repository_location, hexkey) assert "not found" in output - output = self.cmd('debug-delete-obj', self.repository_location, 'invalid') + output = self.cmd('debug', 'delete-obj', self.repository_location, 'invalid') assert "is invalid" in output def test_init_interrupt(self): From a0df60e1b86d8e4b7efdb721090c5245524e01aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ketelaars?= Date: Sat, 8 Oct 2016 08:29:53 +0200 Subject: [PATCH 0281/1387] FUSE on OpenBSD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Concerning #1696: OpenBSD offers in kernel support for FUSE 2.6. Borg relies on llfuse, which relies on FUSE >2.9. I'm not aware of plans to bring FUSE on OpenBSD to a more recent version. Signed-off-by: Björn Ketelaars --- Vagrantfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index b6af51a2..7b09c375 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -114,7 +114,6 @@ def packages_openbsd chsh -s /usr/local/bin/bash vagrant pkg_add openssl pkg_add lz4 - # pkg_add fuse # does not install, sdl dependency missing pkg_add git # no fakeroot pkg_add py3-setuptools ln -sf /usr/local/bin/python3.4 /usr/local/bin/python3 From f50068944d5a7840d2be6a4c53d854fc56372de8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 10 Oct 2016 05:18:43 +0200 Subject: [PATCH 0282/1387] update changed repo location immediately after acceptance fixes #1524 before, if a longer backup got interrupted before commit(), it asked same question again. --- borg/cache.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/borg/cache.py b/borg/cache.py index e293d9e1..04c7ad5b 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -77,6 +77,9 @@ class Cache: if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", retry=False, env_var_override='BORG_RELOCATED_REPO_ACCESS_IS_OK'): raise self.RepositoryAccessAborted() + # adapt on-disk config immediately if the new location was accepted + self.begin_txn() + self.commit() if sync and self.manifest.id != self.manifest_id: # If repository is older than the cache something fishy is going on From 9ad9ae8ff0ef3c3d3bfe38d32dde71b3312e465f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 9 Oct 2016 18:37:32 +0200 Subject: [PATCH 0283/1387] Repository.scan(): avoid iterating same repo segments/objects repeatedly segments: avoid some listdir() objects: avoid repeatedly re-reading object headers / seeking in segment Fixes #1610. --- src/borg/repository.py | 49 ++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index e0f006c2..f1c97e13 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -775,12 +775,10 @@ class Repository: self.index = self.open_index(transaction_id) at_start = marker is None # smallest valid seg is 0, smallest valid offs is 8 - marker_segment, marker_offset = (0, 0) if at_start else self.index[marker] + start_segment, start_offset = (0, 0) if at_start else self.index[marker] result = [] - for segment, filename in self.io.segment_iterator(): - if segment < marker_segment: - continue - obj_iterator = self.io.iter_objects(segment, read_data=False, include_data=False) + for segment, filename in self.io.segment_iterator(start_segment): + obj_iterator = self.io.iter_objects(segment, start_offset, read_data=False, include_data=False) while True: try: tag, id, offset, size = next(obj_iterator) @@ -788,7 +786,11 @@ class Repository: # either end-of-segment or an error - we can not seek to objects at # higher offsets than one that has an error in the header fields break - if segment == marker_segment and offset <= marker_offset: + if start_offset > 0: + # we are using a marker and the marker points to the last object we have already + # returned in the previous scan() call - thus, we need to skip this one object. + # also, for the next segment, we need to start at offset 0. + start_offset = 0 continue if tag == TAG_PUT and (segment, offset) == self.index.get(id): # we have found an existing and current object @@ -886,14 +888,25 @@ class LoggedIO: os.posix_fadvise(fd.fileno(), 0, 0, os.POSIX_FADV_DONTNEED) fd.close() - def segment_iterator(self, reverse=False): + def segment_iterator(self, segment=None, reverse=False): + if segment is None: + segment = 0 if not reverse else 2 ** 32 - 1 data_path = os.path.join(self.path, 'data') - dirs = sorted((dir for dir in os.listdir(data_path) if dir.isdigit()), key=int, reverse=reverse) + start_segment_dir = segment // self.segments_per_dir + dirs = os.listdir(data_path) + if not reverse: + dirs = [dir for dir in dirs if dir.isdigit() and int(dir) >= start_segment_dir] + else: + dirs = [dir for dir in dirs if dir.isdigit() and int(dir) <= start_segment_dir] + dirs = sorted(dirs, key=int, reverse=reverse) for dir in dirs: filenames = os.listdir(os.path.join(data_path, dir)) - sorted_filenames = sorted((filename for filename in filenames - if filename.isdigit()), key=int, reverse=reverse) - for filename in sorted_filenames: + if not reverse: + filenames = [filename for filename in filenames if filename.isdigit() and int(filename) >= segment] + else: + filenames = [filename for filename in filenames if filename.isdigit() and int(filename) <= segment] + filenames = sorted(filenames, key=int, reverse=reverse) + for filename in filenames: yield int(filename), os.path.join(data_path, dir, filename) def get_latest_segment(self): @@ -999,7 +1012,7 @@ class LoggedIO: def segment_size(self, segment): return os.path.getsize(self.segment_filename(segment)) - def iter_objects(self, segment, include_data=False, read_data=True): + def iter_objects(self, segment, offset=0, include_data=False, read_data=True): """ Return object iterator for *segment*. @@ -1009,10 +1022,14 @@ class LoggedIO: The iterator returns four-tuples of (tag, key, offset, data|size). """ fd = self.get_fd(segment) - fd.seek(0) - if fd.read(MAGIC_LEN) != MAGIC: - raise IntegrityError('Invalid segment magic [segment {}, offset {}]'.format(segment, 0)) - offset = MAGIC_LEN + fd.seek(offset) + if offset == 0: + # we are touching this segment for the first time, check the MAGIC. + # Repository.scan() calls us with segment > 0 when it continues an ongoing iteration + # from a marker position - but then we have checked the magic before already. + if fd.read(MAGIC_LEN) != MAGIC: + raise IntegrityError('Invalid segment magic [segment {}, offset {}]'.format(segment, 0)) + offset = MAGIC_LEN header = fd.read(self.header_fmt.size) while header: size, tag, key, data = self._read(fd, self.header_fmt, header, segment, offset, From 4fc5a35572680a995ffc54d4afa1254452e3bc01 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 10 Oct 2016 06:10:39 +0200 Subject: [PATCH 0284/1387] better messages for cache newer than repo, fixes #1700 --- borg/cache.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/borg/cache.py b/borg/cache.py index e293d9e1..2d3dba58 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -20,8 +20,11 @@ import msgpack class Cache: """Client Side cache """ + class RepositoryIDNotUnique(Error): + """Cache is newer than repository - do you have multiple, independently updated repos with same ID?""" + class RepositoryReplay(Error): - """Cache is newer than repository, refusing to continue""" + """Cache is newer than repository - this is either an attack or unsafe (multiple repos with same ID)""" class CacheInitAbortedError(Error): """Cache initialization aborted""" @@ -81,7 +84,10 @@ class Cache: if sync and self.manifest.id != self.manifest_id: # If repository is older than the cache something fishy is going on if self.timestamp and self.timestamp > manifest.timestamp: - raise self.RepositoryReplay() + if isinstance(key, PlaintextKey): + raise self.RepositoryIDNotUnique() + else: + raise self.RepositoryReplay() # Make sure an encrypted repository has not been swapped for an unencrypted repository if self.key_type is not None and self.key_type != str(key.TYPE): raise self.EncryptionMethodMismatch() From cf0359eba75a8233b248946c8c7f65480d08dee6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 10 Oct 2016 19:16:35 +0200 Subject: [PATCH 0285/1387] allow pathes with colons, fixes #1705 also: - refactor / deduplicate the location parsing regexes - add comments - add more tests for Location parsing --- borg/helpers.py | 19 ++++++++++----- borg/testsuite/helpers.py | 49 ++++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/borg/helpers.py b/borg/helpers.py index f7bc54c3..ef4b0687 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -768,20 +768,27 @@ class Location: """Object representing a repository / archive location """ proto = user = host = port = path = archive = None + # path may not contain :: (it ends at :: or string end), but may contain single colons. + # to avoid ambiguities with other regexes, it must also not start with ":". + path_re = r'(?!:)(?P([^:]|(:(?!:)))+)' + # optional ::archive_name at the end, archive name must not contain "/". # borg mount's FUSE filesystem creates one level of directories from - # the archive names. Thus, we must not accept "/" in archive names. + # the archive names and of course "/" is not valid in a directory name. + optional_archive_re = r'(?:::(?P[^/]+))?$' + # regexes for misc. kinds of supported location specifiers: ssh_re = re.compile(r'(?Pssh)://(?:(?P[^@]+)@)?' r'(?P[^:/#]+)(?::(?P\d+))?' - r'(?P[^:]+)(?:::(?P[^/]+))?$') + + path_re + optional_archive_re) file_re = re.compile(r'(?Pfile)://' - r'(?P[^:]+)(?:::(?P[^/]+))?$') + + path_re + optional_archive_re) + # note: scp_re is also use for local pathes scp_re = re.compile(r'((?:(?P[^@]+)@)?(?P[^:/]+):)?' - r'(?P[^:]+)(?:::(?P[^/]+))?$') - # get the repo from BORG_RE env and the optional archive from param. + + path_re + optional_archive_re) + # get the repo from BORG_REPO env and the optional archive from param. # if the syntax requires giving REPOSITORY (see "borg mount"), # use "::" to let it use the env var. # if REPOSITORY argument is optional, it'll automatically use the env. - env_re = re.compile(r'(?:::(?P[^/]+)?)?$') + env_re = re.compile(r'(?:::$)|' + optional_archive_re) def __init__(self, text=''): self.orig = text diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py index cbe4cd9d..8c9c20e5 100644 --- a/borg/testsuite/helpers.py +++ b/borg/testsuite/helpers.py @@ -39,6 +39,8 @@ class TestLocationWithoutEnv: "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')" assert repr(Location('ssh://user@host:1234/some/path')) == \ "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)" + assert repr(Location('ssh://user@host/some/path')) == \ + "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)" def test_file(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) @@ -75,6 +77,15 @@ class TestLocationWithoutEnv: assert repr(Location('some/relative/path')) == \ "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)" + def test_with_colons(self, monkeypatch): + monkeypatch.delenv('BORG_REPO', raising=False) + assert repr(Location('/abs/path:w:cols::arch:col')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols', archive='arch:col')" + assert repr(Location('/abs/path:with:colons::archive')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:with:colons', archive='archive')" + assert repr(Location('/abs/path:with:colons')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:with:colons', archive=None)" + def test_underspecified(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) with pytest.raises(ValueError): @@ -84,11 +95,6 @@ class TestLocationWithoutEnv: with pytest.raises(ValueError): Location() - def test_no_double_colon(self, monkeypatch): - monkeypatch.delenv('BORG_REPO', raising=False) - with pytest.raises(ValueError): - Location('ssh://localhost:22/path:archive') - def test_no_slashes(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) with pytest.raises(ValueError): @@ -119,43 +125,64 @@ class TestLocationWithEnv: monkeypatch.setenv('BORG_REPO', 'ssh://user@host:1234/some/path') assert repr(Location('::archive')) == \ "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')" - assert repr(Location()) == \ + assert repr(Location('::')) == \ "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)" + assert repr(Location()) == \ + "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)" def test_file(self, monkeypatch): monkeypatch.setenv('BORG_REPO', 'file:///some/path') assert repr(Location('::archive')) == \ "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive')" - assert repr(Location()) == \ + assert repr(Location('::')) == \ "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive=None)" + assert repr(Location()) == \ + "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive=None)" def test_scp(self, monkeypatch): monkeypatch.setenv('BORG_REPO', 'user@host:/some/path') assert repr(Location('::archive')) == \ "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive='archive')" - assert repr(Location()) == \ + assert repr(Location('::')) == \ "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)" + assert repr(Location()) == \ + "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)" def test_folder(self, monkeypatch): monkeypatch.setenv('BORG_REPO', 'path') assert repr(Location('::archive')) == \ "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive')" - assert repr(Location()) == \ + assert repr(Location('::')) == \ "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)" + assert repr(Location()) == \ + "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)" def test_abspath(self, monkeypatch): monkeypatch.setenv('BORG_REPO', '/some/absolute/path') assert repr(Location('::archive')) == \ "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive='archive')" - assert repr(Location()) == \ + assert repr(Location('::')) == \ "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)" + assert repr(Location()) == \ + "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)" def test_relpath(self, monkeypatch): monkeypatch.setenv('BORG_REPO', 'some/relative/path') assert repr(Location('::archive')) == \ "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive='archive')" - assert repr(Location()) == \ + assert repr(Location('::')) == \ "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)" + assert repr(Location()) == \ + "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)" + + def test_with_colons(self, monkeypatch): + monkeypatch.setenv('BORG_REPO', '/abs/path:w:cols') + assert repr(Location('::arch:col')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols', archive='arch:col')" + assert repr(Location('::')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols', archive=None)" + assert repr(Location()) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols', archive=None)" def test_no_slashes(self, monkeypatch): monkeypatch.setenv('BORG_REPO', '/some/absolute/path') From e9ba14c6862bcf55be56660c78a9376a2ad959ff Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 10 Oct 2016 23:28:22 +0200 Subject: [PATCH 0286/1387] Location parsing regexes: use verbose REs just added whitespace and comments, no semantic changes --- borg/helpers.py | 44 +++++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/borg/helpers.py b/borg/helpers.py index ef4b0687..24a8231a 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -768,27 +768,49 @@ class Location: """Object representing a repository / archive location """ proto = user = host = port = path = archive = None - # path may not contain :: (it ends at :: or string end), but may contain single colons. + + # path must not contain :: (it ends at :: or string end), but may contain single colons. # to avoid ambiguities with other regexes, it must also not start with ":". - path_re = r'(?!:)(?P([^:]|(:(?!:)))+)' + path_re = r""" + (?!:) # not starting with ":" + (?P([^:]|(:(?!:)))+) # any chars, but no "::" + """ # optional ::archive_name at the end, archive name must not contain "/". # borg mount's FUSE filesystem creates one level of directories from # the archive names and of course "/" is not valid in a directory name. - optional_archive_re = r'(?:::(?P[^/]+))?$' + optional_archive_re = r""" + (?: + :: # "::" as separator + (?P[^/]+) # archive name must not contain "/" + )?$""" # must match until the end + # regexes for misc. kinds of supported location specifiers: - ssh_re = re.compile(r'(?Pssh)://(?:(?P[^@]+)@)?' - r'(?P[^:/#]+)(?::(?P\d+))?' - + path_re + optional_archive_re) - file_re = re.compile(r'(?Pfile)://' - + path_re + optional_archive_re) + ssh_re = re.compile(r""" + (?Pssh):// # ssh:// + (?:(?P[^@]+)@)? # user@ (optional) + (?P[^:/#]+)(?::(?P\d+))? # host or host:port + """ + path_re + optional_archive_re, re.VERBOSE) # path or path::archive + + file_re = re.compile(r""" + (?Pfile):// # file:// + """ + path_re + optional_archive_re, re.VERBOSE) # path or path::archive + # note: scp_re is also use for local pathes - scp_re = re.compile(r'((?:(?P[^@]+)@)?(?P[^:/]+):)?' - + path_re + optional_archive_re) + scp_re = re.compile(r""" + ( + (?:(?P[^@]+)@)? # user@ (optional) + (?P[^:/]+): # host: (don't match / in host to disambiguate from file:) + )? # user@host: part is optional + """ + path_re + optional_archive_re, re.VERBOSE) # path with optional archive + # get the repo from BORG_REPO env and the optional archive from param. # if the syntax requires giving REPOSITORY (see "borg mount"), # use "::" to let it use the env var. # if REPOSITORY argument is optional, it'll automatically use the env. - env_re = re.compile(r'(?:::$)|' + optional_archive_re) + env_re = re.compile(r""" # the repo part is fetched from BORG_REPO + (?:::$) # just "::" is ok (when a pos. arg is required, no archive) + | # or + """ + optional_archive_re, re.VERBOSE) # archive name (optional, may be empty) def __init__(self, text=''): self.orig = text From 546c77f73d59413ce64a556d57981ceba2d5fadb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 11 Oct 2016 00:12:25 +0200 Subject: [PATCH 0287/1387] ssh:// Location URL - remove not needed # nobody could make sense of it, so guess it is a mistake. --- borg/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/borg/helpers.py b/borg/helpers.py index 24a8231a..14caeec3 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -788,7 +788,7 @@ class Location: ssh_re = re.compile(r""" (?Pssh):// # ssh:// (?:(?P[^@]+)@)? # user@ (optional) - (?P[^:/#]+)(?::(?P\d+))? # host or host:port + (?P[^:/]+)(?::(?P\d+))? # host or host:port """ + path_re + optional_archive_re, re.VERBOSE) # path or path::archive file_re = re.compile(r""" From 8d9475f704864b595abde6f5479bf95dbf250668 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 13 Oct 2016 03:53:26 +0200 Subject: [PATCH 0288/1387] fuse_mount contextmanager: accept any options not just the -o mount_options, but any options borg mount would take. simpler, more flexible. --- borg/testsuite/__init__.py | 6 ++---- borg/testsuite/archiver.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/borg/testsuite/__init__.py b/borg/testsuite/__init__.py index 64c240c2..6b85538b 100644 --- a/borg/testsuite/__init__.py +++ b/borg/testsuite/__init__.py @@ -94,11 +94,9 @@ class BaseTestCase(unittest.TestCase): self._assert_dirs_equal_cmp(sub_diff) @contextmanager - def fuse_mount(self, location, mountpoint, mount_options=None): + def fuse_mount(self, location, mountpoint, *options): os.mkdir(mountpoint) - args = ['mount', location, mountpoint] - if mount_options: - args += '-o', mount_options + args = ['mount', location, mountpoint] + list(options) self.cmd(*args, fork=True) self.wait_for_mount(mountpoint) yield diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 706319bd..8508639e 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1119,7 +1119,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with pytest.raises(OSError) as excinfo: open(os.path.join(mountpoint, path)) assert excinfo.value.errno == errno.EIO - with self.fuse_mount(self.repository_location + '::archive', mountpoint, 'allow_damaged_files'): + with self.fuse_mount(self.repository_location + '::archive', mountpoint, '-o', 'allow_damaged_files'): open(os.path.join(mountpoint, path)).close() def verify_aes_counter_uniqueness(self, method): From c9a3a201e59f6674c5d7f2ef4c8bd2ea4f7812a2 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 13 Oct 2016 20:02:31 +0200 Subject: [PATCH 0289/1387] travis: use 3.6-dev nightly points to python 3.7 now. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 853ad541..0fdcfa6a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ matrix: os: linux dist: trusty env: TOXENV=py35 - - python: nightly + - python: 3.6-dev os: linux dist: trusty env: TOXENV=py36 @@ -34,7 +34,7 @@ matrix: osx_image: xcode6.4 env: TOXENV=py35 allow_failures: - - python: nightly + - python: 3.6-dev install: - ./.travis/install.sh From 4e4847ccceacb64191f3cf7a31159f9ce993767d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 13 Oct 2016 23:59:28 +0200 Subject: [PATCH 0290/1387] point XDG_*_HOME to temp dirs for tests, fixes #1714 otherwise it spoils the user's nonces and cache dirs with lots of files. also: remove all BORG_* env vars from the outer environment --- conftest.py | 13 +++++++++++++ src/borg/testsuite/helpers.py | 31 ++++++++----------------------- src/borg/testsuite/key.py | 11 ----------- src/borg/testsuite/nonces.py | 11 ----------- 4 files changed, 21 insertions(+), 45 deletions(-) diff --git a/conftest.py b/conftest.py index 5caf8a09..312f57a2 100644 --- a/conftest.py +++ b/conftest.py @@ -1,5 +1,7 @@ import os +import pytest + from borg.logger import setup_logging # Ensure that the loggers exist for all tests @@ -16,6 +18,17 @@ def pytest_configure(config): constants.PBKDF2_ITERATIONS = 1 +@pytest.fixture(autouse=True) +def clean_env(tmpdir_factory, monkeypatch): + # avoid that we access / modify the user's normal .config / .cache directory: + monkeypatch.setenv('XDG_CONFIG_HOME', tmpdir_factory.mktemp('xdg-config-home')) + monkeypatch.setenv('XDG_CACHE_HOME', tmpdir_factory.mktemp('xdg-cache-home')) + # also avoid to use anything from the outside environment: + keys = [key for key in os.environ if key.startswith('BORG_')] + for key in keys: + monkeypatch.delenv(key, raising=False) + + def pytest_report_header(config, startdir): tests = { "BSD flags": has_lchflags, diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 55569e96..bca3eb22 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -605,44 +605,29 @@ class TestParseTimestamp(BaseTestCase): self.assert_equal(parse_timestamp('2015-04-19T20:25:00'), datetime(2015, 4, 19, 20, 25, 0, 0, timezone.utc)) -def test_get_cache_dir(): +def test_get_cache_dir(monkeypatch): """test that get_cache_dir respects environment""" - # reset BORG_CACHE_DIR in order to test default - old_env = None - if os.environ.get('BORG_CACHE_DIR'): - old_env = os.environ['BORG_CACHE_DIR'] - del(os.environ['BORG_CACHE_DIR']) + monkeypatch.delenv('XDG_CACHE_HOME', raising=False) assert get_cache_dir() == os.path.join(os.path.expanduser('~'), '.cache', 'borg') - os.environ['XDG_CACHE_HOME'] = '/var/tmp/.cache' + monkeypatch.setenv('XDG_CACHE_HOME', '/var/tmp/.cache') assert get_cache_dir() == os.path.join('/var/tmp/.cache', 'borg') - os.environ['BORG_CACHE_DIR'] = '/var/tmp' + monkeypatch.setenv('BORG_CACHE_DIR', '/var/tmp') assert get_cache_dir() == '/var/tmp' - # reset old env - if old_env is not None: - os.environ['BORG_CACHE_DIR'] = old_env -def test_get_keys_dir(): +def test_get_keys_dir(monkeypatch): """test that get_keys_dir respects environment""" - # reset BORG_KEYS_DIR in order to test default - old_env = None - if os.environ.get('BORG_KEYS_DIR'): - old_env = os.environ['BORG_KEYS_DIR'] - del(os.environ['BORG_KEYS_DIR']) + monkeypatch.delenv('XDG_CONFIG_HOME', raising=False) assert get_keys_dir() == os.path.join(os.path.expanduser('~'), '.config', 'borg', 'keys') - os.environ['XDG_CONFIG_HOME'] = '/var/tmp/.config' + monkeypatch.setenv('XDG_CONFIG_HOME', '/var/tmp/.config') assert get_keys_dir() == os.path.join('/var/tmp/.config', 'borg', 'keys') - os.environ['BORG_KEYS_DIR'] = '/var/tmp' + monkeypatch.setenv('BORG_KEYS_DIR', '/var/tmp') assert get_keys_dir() == '/var/tmp' - # reset old env - if old_env is not None: - os.environ['BORG_KEYS_DIR'] = old_env def test_get_nonces_dir(monkeypatch): """test that get_nonces_dir respects environment""" monkeypatch.delenv('XDG_CONFIG_HOME', raising=False) - monkeypatch.delenv('BORG_NONCES_DIR', raising=False) assert get_nonces_dir() == os.path.join(os.path.expanduser('~'), '.config', 'borg', 'key-nonces') monkeypatch.setenv('XDG_CONFIG_HOME', '/var/tmp/.config') assert get_nonces_dir() == os.path.join('/var/tmp/.config', 'borg', 'key-nonces') diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index 94b45539..c2e637bd 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -14,17 +14,6 @@ from ..helpers import get_nonces_dir from ..key import PlaintextKey, PassphraseKey, KeyfileKey, Passphrase, PasswordRetriesExceeded, bin_to_hex -@pytest.fixture(autouse=True) -def clean_env(monkeypatch): - # Workaround for some tests (testsuite/archiver) polluting the environment - monkeypatch.delenv('BORG_PASSPHRASE', False) - - -@pytest.fixture(autouse=True) -def nonce_dir(tmpdir_factory, monkeypatch): - monkeypatch.setenv('XDG_CONFIG_HOME', tmpdir_factory.mktemp('xdg-config-home')) - - class TestKey: class MockArgs: location = Location(tempfile.mkstemp()[1]) diff --git a/src/borg/testsuite/nonces.py b/src/borg/testsuite/nonces.py index 88405f56..14d1f52d 100644 --- a/src/borg/testsuite/nonces.py +++ b/src/borg/testsuite/nonces.py @@ -10,17 +10,6 @@ from ..remote import InvalidRPCMethod from .. import nonces # for monkey patching NONCE_SPACE_RESERVATION -@pytest.fixture(autouse=True) -def clean_env(monkeypatch): - # Workaround for some tests (testsuite/archiver) polluting the environment - monkeypatch.delenv('BORG_PASSPHRASE', False) - - -@pytest.fixture(autouse=True) -def nonce_dir(tmpdir_factory, monkeypatch): - monkeypatch.setenv('XDG_CONFIG_HOME', tmpdir_factory.mktemp('xdg-config-home')) - - class TestNonceManager: class MockRepository: From f3efcdbd2eee1fe9b632630c12773a93c11df2b4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 14 Oct 2016 00:46:43 +0200 Subject: [PATCH 0291/1387] point XDG_*_HOME to temp dirs for tests, fixes #1714 otherwise it spoils the user's nonces and cache dirs with lots of files. also: remove all BORG_* env vars from the outer environment fix get_*_dir tests to use monkeypatch. --- borg/testsuite/helpers.py | 30 ++++++++---------------------- conftest.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 22 deletions(-) create mode 100644 conftest.py diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py index 8c9c20e5..f7b53ad7 100644 --- a/borg/testsuite/helpers.py +++ b/borg/testsuite/helpers.py @@ -617,38 +617,24 @@ class TestParseTimestamp(BaseTestCase): self.assert_equal(parse_timestamp('2015-04-19T20:25:00'), datetime(2015, 4, 19, 20, 25, 0, 0, timezone.utc)) -def test_get_cache_dir(): +def test_get_cache_dir(monkeypatch): """test that get_cache_dir respects environment""" - # reset BORG_CACHE_DIR in order to test default - old_env = None - if os.environ.get('BORG_CACHE_DIR'): - old_env = os.environ['BORG_CACHE_DIR'] - del(os.environ['BORG_CACHE_DIR']) + monkeypatch.delenv('XDG_CACHE_HOME', raising=False) assert get_cache_dir() == os.path.join(os.path.expanduser('~'), '.cache', 'borg') - os.environ['XDG_CACHE_HOME'] = '/var/tmp/.cache' + monkeypatch.setenv('XDG_CACHE_HOME', '/var/tmp/.cache') assert get_cache_dir() == os.path.join('/var/tmp/.cache', 'borg') - os.environ['BORG_CACHE_DIR'] = '/var/tmp' + monkeypatch.setenv('BORG_CACHE_DIR', '/var/tmp') assert get_cache_dir() == '/var/tmp' - # reset old env - if old_env is not None: - os.environ['BORG_CACHE_DIR'] = old_env -def test_get_keys_dir(): +def test_get_keys_dir(monkeypatch): """test that get_keys_dir respects environment""" - # reset BORG_KEYS_DIR in order to test default - old_env = None - if os.environ.get('BORG_KEYS_DIR'): - old_env = os.environ['BORG_KEYS_DIR'] - del(os.environ['BORG_KEYS_DIR']) + monkeypatch.delenv('XDG_CONFIG_HOME', raising=False) assert get_keys_dir() == os.path.join(os.path.expanduser('~'), '.config', 'borg', 'keys') - os.environ['XDG_CONFIG_HOME'] = '/var/tmp/.config' + monkeypatch.setenv('XDG_CONFIG_HOME', '/var/tmp/.config') assert get_keys_dir() == os.path.join('/var/tmp/.config', 'borg', 'keys') - os.environ['BORG_KEYS_DIR'] = '/var/tmp' + monkeypatch.setenv('BORG_KEYS_DIR', '/var/tmp') assert get_keys_dir() == '/var/tmp' - # reset old env - if old_env is not None: - os.environ['BORG_KEYS_DIR'] = old_env @pytest.fixture() diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..9d5513b5 --- /dev/null +++ b/conftest.py @@ -0,0 +1,14 @@ +import os + +import pytest + + +@pytest.fixture(autouse=True) +def clean_env(tmpdir_factory, monkeypatch): + # avoid that we access / modify the user's normal .config / .cache directory: + monkeypatch.setenv('XDG_CONFIG_HOME', tmpdir_factory.mktemp('xdg-config-home')) + monkeypatch.setenv('XDG_CACHE_HOME', tmpdir_factory.mktemp('xdg-cache-home')) + # also avoid to use anything from the outside environment: + keys = [key for key in os.environ if key.startswith('BORG_')] + for key in keys: + monkeypatch.delenv(key, raising=False) From 2679963cb9dfec51f377267e429d13d2a01340f3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 14 Oct 2016 04:42:13 +0200 Subject: [PATCH 0292/1387] add conftest.py hack needed for borg 1.0.x --- conftest.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/conftest.py b/conftest.py index 9d5513b5..d80aed4c 100644 --- a/conftest.py +++ b/conftest.py @@ -1,8 +1,29 @@ import os +import sys import pytest +# This is a hack to fix path problems because "borg" (the package) is in the source root. +# When importing the conftest an "import borg" can incorrectly import the borg from the +# source root instead of the one installed in the environment. +# The workaround is to remove entries pointing there from the path and check whether "borg" +# is still importable. If it is not, then it has not been installed in the environment +# and the entries are put back. +# +# TODO: After moving the package to src/: remove this. + +original_path = list(sys.path) +for entry in original_path: + if entry == '' or entry.endswith('/borg'): + sys.path.remove(entry) + +try: + import borg +except ImportError: + sys.path = original_path + + @pytest.fixture(autouse=True) def clean_env(tmpdir_factory, monkeypatch): # avoid that we access / modify the user's normal .config / .cache directory: From 2b27a06595fad7bc416c2e0e839f0713d623c3ee Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 14 Oct 2016 04:44:06 +0200 Subject: [PATCH 0293/1387] use monkeypatch to set env vars but only on pytest based tests. --- borg/testsuite/benchmark.py | 14 +++++++------- borg/testsuite/helpers.py | 25 ++++++++++++------------- borg/testsuite/upgrader.py | 8 ++++---- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/borg/testsuite/benchmark.py b/borg/testsuite/benchmark.py index 9751bc1a..b3400c2f 100644 --- a/borg/testsuite/benchmark.py +++ b/borg/testsuite/benchmark.py @@ -14,13 +14,13 @@ from .archiver import changedir, cmd @pytest.yield_fixture -def repo_url(request, tmpdir): - os.environ['BORG_PASSPHRASE'] = '123456' - os.environ['BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'] = 'YES' - os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'YES' - os.environ['BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK'] = 'yes' - os.environ['BORG_KEYS_DIR'] = str(tmpdir.join('keys')) - os.environ['BORG_CACHE_DIR'] = str(tmpdir.join('cache')) +def repo_url(request, tmpdir, monkeypatch): + monkeypatch.setenv('BORG_PASSPHRASE', '123456') + monkeypatch.setenv('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING', 'YES') + monkeypatch.setenv('BORG_DELETE_I_KNOW_WHAT_I_AM_DOING', 'YES') + monkeypatch.setenv('BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK', 'yes') + monkeypatch.setenv('BORG_KEYS_DIR', str(tmpdir.join('keys'))) + monkeypatch.setenv('BORG_CACHE_DIR', str(tmpdir.join('cache'))) yield str(tmpdir.join('repository')) tmpdir.remove(rec=1) diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py index f7b53ad7..10574f5a 100644 --- a/borg/testsuite/helpers.py +++ b/borg/testsuite/helpers.py @@ -17,7 +17,7 @@ from ..helpers import Location, format_file_size, format_timedelta, format_line, ProgressIndicatorPercent, ProgressIndicatorEndless, load_excludes, parse_pattern, \ PatternMatcher, RegexPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, \ Buffer -from . import BaseTestCase, environment_variable, FakeInputs +from . import BaseTestCase, FakeInputs class BigIntTestCase(BaseTestCase): @@ -653,8 +653,8 @@ def test_stats_basic(stats): assert stats.usize == 10 -def tests_stats_progress(stats, columns=80): - os.environ['COLUMNS'] = str(columns) +def tests_stats_progress(stats, monkeypatch, columns=80): + monkeypatch.setenv('COLUMNS', str(columns)) out = StringIO() stats.show_progress(stream=out) s = '20 B O 10 B C 10 B D 0 N ' @@ -811,21 +811,20 @@ def test_yes_input_custom(): assert not yes(falsish=('NOPE', ), input=input) -def test_yes_env(): +def test_yes_env(monkeypatch): for value in TRUISH: - with environment_variable(OVERRIDE_THIS=value): - assert yes(env_var_override='OVERRIDE_THIS') + monkeypatch.setenv('OVERRIDE_THIS', value) + assert yes(env_var_override='OVERRIDE_THIS') for value in FALSISH: - with environment_variable(OVERRIDE_THIS=value): - assert not yes(env_var_override='OVERRIDE_THIS') + monkeypatch.setenv('OVERRIDE_THIS', value) + assert not yes(env_var_override='OVERRIDE_THIS') -def test_yes_env_default(): +def test_yes_env_default(monkeypatch): for value in DEFAULTISH: - with environment_variable(OVERRIDE_THIS=value): - assert yes(env_var_override='OVERRIDE_THIS', default=True) - with environment_variable(OVERRIDE_THIS=value): - assert not yes(env_var_override='OVERRIDE_THIS', default=False) + monkeypatch.setenv('OVERRIDE_THIS', value) + assert yes(env_var_override='OVERRIDE_THIS', default=True) + assert not yes(env_var_override='OVERRIDE_THIS', default=False) def test_yes_defaults(): diff --git a/borg/testsuite/upgrader.py b/borg/testsuite/upgrader.py index 013a8d00..fb5b5e6f 100644 --- a/borg/testsuite/upgrader.py +++ b/borg/testsuite/upgrader.py @@ -97,7 +97,7 @@ class MockArgs: @pytest.fixture() -def attic_key_file(attic_repo, tmpdir): +def attic_key_file(attic_repo, tmpdir, monkeypatch): """ create an attic key file from the given repo, in the keys subdirectory of the given tmpdir @@ -112,13 +112,13 @@ def attic_key_file(attic_repo, tmpdir): # we use the repo dir for the created keyfile, because we do # not want to clutter existing keyfiles - os.environ['ATTIC_KEYS_DIR'] = keys_dir + monkeypatch.setenv('ATTIC_KEYS_DIR', keys_dir) # we use the same directory for the converted files, which # will clutter the previously created one, which we don't care # about anyways. in real runs, the original key will be retained. - os.environ['BORG_KEYS_DIR'] = keys_dir - os.environ['ATTIC_PASSPHRASE'] = 'test' + monkeypatch.setenv('BORG_KEYS_DIR', keys_dir) + monkeypatch.setenv('ATTIC_PASSPHRASE', 'test') return attic.key.KeyfileKey.create(attic_repo, MockArgs(keys_dir)) From e829e8372d0a9f862c4f048ac7fdc5dc8ad60366 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 13 Oct 2016 00:38:04 +0200 Subject: [PATCH 0294/1387] implement /./relpath hack, fixes #1655 --- borg/helpers.py | 16 +++++++++++----- borg/remote.py | 4 +++- borg/testsuite/helpers.py | 10 +++++++++- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/borg/helpers.py b/borg/helpers.py index 14caeec3..2cceb2af 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -835,26 +835,32 @@ class Location: return True def _parse(self, text): + def normpath_special(p): + # avoid that normpath strips away our relative path hack and even makes p absolute + relative = p.startswith('/./') + p = os.path.normpath(p) + return ('/.' + p) if relative else p + m = self.ssh_re.match(text) if m: self.proto = m.group('proto') self.user = m.group('user') self.host = m.group('host') self.port = m.group('port') and int(m.group('port')) or None - self.path = os.path.normpath(m.group('path')) + self.path = normpath_special(m.group('path')) self.archive = m.group('archive') return True m = self.file_re.match(text) if m: self.proto = m.group('proto') - self.path = os.path.normpath(m.group('path')) + self.path = normpath_special(m.group('path')) self.archive = m.group('archive') return True m = self.scp_re.match(text) if m: self.user = m.group('user') self.host = m.group('host') - self.path = os.path.normpath(m.group('path')) + self.path = normpath_special(m.group('path')) self.archive = m.group('archive') self.proto = self.host and 'ssh' or 'file' return True @@ -885,9 +891,9 @@ class Location: return self.path else: if self.path and self.path.startswith('~'): - path = '/' + self.path + path = '/' + self.path # /~/x = path x relative to home dir elif self.path and not self.path.startswith('/'): - path = '/~/' + self.path + path = '/./' + self.path # /./x = path x relative to cwd else: path = self.path return 'ssh://{}{}{}{}'.format('{}@'.format(self.user) if self.user else '', diff --git a/borg/remote.py b/borg/remote.py index 20d0c185..e0480231 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -129,8 +129,10 @@ class RepositoryServer: # pragma: no cover def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False): path = os.fsdecode(path) - if path.startswith('/~'): + if path.startswith('/~'): # /~/x = path x relative to home dir, /~username/x = relative to "user" home dir path = path[1:] + elif path.startswith('/./'): # /./x = path x relative to cwd + path = path[3:] path = os.path.realpath(os.path.expanduser(path)) if self.restrict_to_paths: # if --restrict-to-path P is given, we make sure that we only operate in/below path P. diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py index 10574f5a..1087b552 100644 --- a/borg/testsuite/helpers.py +++ b/borg/testsuite/helpers.py @@ -69,6 +69,8 @@ class TestLocationWithoutEnv: "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive='archive')" assert repr(Location('/some/absolute/path')) == \ "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)" + assert repr(Location('ssh://user@host/some/path')) == \ + "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)" def test_relpath(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) @@ -76,6 +78,12 @@ class TestLocationWithoutEnv: "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive='archive')" assert repr(Location('some/relative/path')) == \ "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)" + assert repr(Location('ssh://user@host/./some/path')) == \ + "Location(proto='ssh', user='user', host='host', port=None, path='/./some/path', archive=None)" + assert repr(Location('ssh://user@host/~/some/path')) == \ + "Location(proto='ssh', user='user', host='host', port=None, path='/~/some/path', archive=None)" + assert repr(Location('ssh://user@host/~user/some/path')) == \ + "Location(proto='ssh', user='user', host='host', port=None, path='/~user/some/path', archive=None)" def test_with_colons(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) @@ -107,7 +115,7 @@ class TestLocationWithoutEnv: 'ssh://user@host:1234/some/path::archive'] for location in locations: assert Location(location).canonical_path() == \ - Location(Location(location).canonical_path()).canonical_path() + Location(Location(location).canonical_path()).canonical_path(), "failed: %s" % location def test_format_path(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) From 26e6ac4fea2c1f913cc0998fd8592ce0abe8095a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 13 Oct 2016 03:08:57 +0200 Subject: [PATCH 0295/1387] borg mount --first / --last / --sort / --prefix, fixes #1542 also: use consider_part_files when dealing with multiple archives / whole repo mount --- src/borg/archiver.py | 8 ++------ src/borg/fuse.py | 17 +++++++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 11ace705..736f725b 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -853,12 +853,7 @@ class Archiver: return self.exit_code with cache_if_remote(repository) as cached_repo: - if args.location.archive: - archive = Archive(repository, key, manifest, args.location.archive, - consider_part_files=args.consider_part_files) - else: - archive = None - operations = FuseOperations(key, repository, manifest, archive, cached_repo) + operations = FuseOperations(key, repository, manifest, args, cached_repo, archiver=self) logger.info("Mounting filesystem") try: operations.mount(args.mountpoint, args.options, args.foreground) @@ -2115,6 +2110,7 @@ class Archiver: help='stay in foreground, do not daemonize') subparser.add_argument('-o', dest='options', type=str, help='Extra mount options') + self.add_archives_filters_args(subparser) info_epilog = textwrap.dedent(""" This command displays detailed information about the specified archive or repository. diff --git a/src/borg/fuse.py b/src/borg/fuse.py index b822332d..02b41b55 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -58,11 +58,12 @@ class FuseOperations(llfuse.Operations): allow_damaged_files = False versions = False - def __init__(self, key, repository, manifest, archive, cached_repo): + def __init__(self, key, repository, manifest, args, cached_repo, archiver): super().__init__() + self.archiver = archiver self.repository_uncached = repository self.repository = cached_repo - self.archive = archive + self.args = args self.manifest = manifest self.key = key self._inode_count = 0 @@ -79,11 +80,15 @@ class FuseOperations(llfuse.Operations): def _create_filesystem(self): self._create_dir(parent=1) # first call, create root dir (inode == 1) - if self.archive: - self.process_archive(self.archive) + if self.args.location.archive: + archive = Archive(self.repository_uncached, self.key, self.manifest, self.args.location.archive, + consider_part_files=self.args.consider_part_files) + self.process_archive(archive) else: - for name in self.manifest.archives: - archive = Archive(self.repository_uncached, self.key, self.manifest, name) + archive_names = (x.name for x in self.archiver._get_filtered_archives(self.args, self.manifest)) + for name in archive_names: + archive = Archive(self.repository_uncached, self.key, self.manifest, name, + consider_part_files=self.args.consider_part_files) if self.versions: # process archives immediately self.process_archive(archive) From 694c3978a10452dc60992cd2914f0589a265927f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 13 Oct 2016 04:40:33 +0200 Subject: [PATCH 0296/1387] refactor Archiver._get_filtered_archives -> Archives.list_filtered it did not belong into Archiver class (did not use "self"), but in into Archives. --- src/borg/archiver.py | 23 ++++------------------- src/borg/fuse.py | 5 ++--- src/borg/helpers.py | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 736f725b..6fe3c823 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -783,7 +783,7 @@ class Archiver: if args.location.archive: archive_names = (args.location.archive,) else: - archive_names = tuple(x.name for x in self._get_filtered_archives(args, manifest)) + archive_names = tuple(x.name for x in manifest.archives.list_filtered(args)) if not archive_names: return self.exit_code @@ -853,7 +853,7 @@ class Archiver: return self.exit_code with cache_if_remote(repository) as cached_repo: - operations = FuseOperations(key, repository, manifest, args, cached_repo, archiver=self) + operations = FuseOperations(key, repository, manifest, args, cached_repo) logger.info("Mounting filesystem") try: operations.mount(args.mountpoint, args.options, args.foreground) @@ -904,7 +904,7 @@ class Archiver: format = "{archive:<36} {time} [{id}]{NL}" formatter = ArchiveFormatter(format) - for archive_info in self._get_filtered_archives(args, manifest): + for archive_info in manifest.archives.list_filtered(args): write(safe_encode(formatter.format_item(archive_info))) return self.exit_code @@ -924,7 +924,7 @@ class Archiver: if args.location.archive: archive_names = (args.location.archive,) else: - archive_names = tuple(x.name for x in self._get_filtered_archives(args, manifest)) + archive_names = tuple(x.name for x in manifest.archives.list_filtered(args)) if not archive_names: return self.exit_code @@ -2626,21 +2626,6 @@ class Archiver: logger.warning("Using a pure-python msgpack! This will result in lower performance.") return args.func(args) - def _get_filtered_archives(self, args, manifest): - if args.location.archive: - raise Error('The options --first, --last and --prefix can only be used on repository targets.') - - archives = manifest.archives.list(prefix=args.prefix) - - for sortkey in reversed(args.sort_by.split(',')): - archives.sort(key=attrgetter(sortkey)) - if args.last: - archives.reverse() - - n = args.first or args.last or len(archives) - - return archives[:n] - def sig_info_handler(sig_no, stack): # pragma: no cover """search the stack for infos about the currently processed file and print them""" diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 02b41b55..9cdc86ae 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -58,9 +58,8 @@ class FuseOperations(llfuse.Operations): allow_damaged_files = False versions = False - def __init__(self, key, repository, manifest, args, cached_repo, archiver): + def __init__(self, key, repository, manifest, args, cached_repo): super().__init__() - self.archiver = archiver self.repository_uncached = repository self.repository = cached_repo self.args = args @@ -85,7 +84,7 @@ class FuseOperations(llfuse.Operations): consider_part_files=self.args.consider_part_files) self.process_archive(archive) else: - archive_names = (x.name for x in self.archiver._get_filtered_archives(self.args, self.manifest)) + archive_names = (x.name for x in self.manifest.archives.list_filtered(self.args)) for name in archive_names: archive = Archive(self.repository_uncached, self.key, self.manifest, name, consider_part_files=self.args.consider_part_files) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 94545fcb..89f09bce 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -154,6 +154,21 @@ class Archives(abc.MutableMapping): archives.reverse() return archives + def list_filtered(self, args): + """ + get a filtered list of archives, considering --first/last/prefix/sort + """ + if args.location.archive: + raise Error('The options --first, --last and --prefix can only be used on repository targets.') + archives = self.list(prefix=args.prefix) + for sortkey in reversed(args.sort_by.split(',')): + archives.sort(key=attrgetter(sortkey)) + if args.last: + archives.reverse() + n = args.first or args.last or len(archives) + return archives[:n] + + def set_raw_dict(self, d): """set the dict we get from the msgpack unpacker""" for k, v in d.items(): From b5f98580552ecbea13a322785293306aa8281ce5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 13 Oct 2016 05:21:52 +0200 Subject: [PATCH 0297/1387] move first/last/sort_by-multiple functionality into Manifest.list also: rename list_filtered to list_considering --- src/borg/archive.py | 2 +- src/borg/archiver.py | 12 ++++++------ src/borg/fuse.py | 2 +- src/borg/helpers.py | 29 +++++++++++++---------------- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 621ad50b..bfc58361 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1295,7 +1295,7 @@ class ArchiveChecker: if archive is None: # we need last N or all archives - archive_infos = self.manifest.archives.list(sort_by='ts', reverse=True) + archive_infos = self.manifest.archives.list(sort_by=['ts'], reverse=True) if prefix is not None: archive_infos = [info for info in archive_infos if info.name.startswith(prefix)] num_archives = len(archive_infos) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 6fe3c823..40024261 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -783,7 +783,7 @@ class Archiver: if args.location.archive: archive_names = (args.location.archive,) else: - archive_names = tuple(x.name for x in manifest.archives.list_filtered(args)) + archive_names = tuple(x.name for x in manifest.archives.list_considering(args)) if not archive_names: return self.exit_code @@ -825,7 +825,7 @@ class Archiver: else: msg.append("You requested to completely DELETE the repository *including* all archives it " "contains:") - for archive_info in manifest.archives.list(sort_by='ts'): + for archive_info in manifest.archives.list(sort_by=['ts']): msg.append(format_archive(archive_info)) msg.append("Type 'YES' if you understand this and want to continue: ") msg = '\n'.join(msg) @@ -904,7 +904,7 @@ class Archiver: format = "{archive:<36} {time} [{id}]{NL}" formatter = ArchiveFormatter(format) - for archive_info in manifest.archives.list_filtered(args): + for archive_info in manifest.archives.list_considering(args): write(safe_encode(formatter.format_item(archive_info))) return self.exit_code @@ -924,7 +924,7 @@ class Archiver: if args.location.archive: archive_names = (args.location.archive,) else: - archive_names = tuple(x.name for x in manifest.archives.list_filtered(args)) + archive_names = tuple(x.name for x in manifest.archives.list_considering(args)) if not archive_names: return self.exit_code @@ -976,7 +976,7 @@ class Archiver: '"keep-secondly", "keep-minutely", "keep-hourly", "keep-daily", ' '"keep-weekly", "keep-monthly" or "keep-yearly" settings must be specified.') return self.exit_code - archives_checkpoints = manifest.archives.list(sort_by='ts', reverse=True) # just a ArchiveInfo list + archives_checkpoints = manifest.archives.list(sort_by=['ts'], reverse=True) # just a ArchiveInfo list if args.prefix: archives_checkpoints = [arch for arch in archives_checkpoints if arch.name.startswith(args.prefix)] is_checkpoint = re.compile(r'\.checkpoint(\.\d+)?$').search @@ -1094,7 +1094,7 @@ class Archiver: if args.target is not None: self.print_error('--target: Need to specify single archive') return self.exit_code - for archive in manifest.archives.list(sort_by='ts'): + for archive in manifest.archives.list(sort_by=['ts']): name = archive.name if recreater.is_temporary_archive(name): continue diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 9cdc86ae..8de141fe 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -84,7 +84,7 @@ class FuseOperations(llfuse.Operations): consider_part_files=self.args.consider_part_files) self.process_archive(archive) else: - archive_names = (x.name for x in self.manifest.archives.list_filtered(self.args)) + archive_names = (x.name for x in self.manifest.archives.list_considering(self.args)) for name in archive_names: archive = Archive(self.repository_uncached, self.key, self.manifest, name, consider_part_files=self.args.consider_part_files) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 89f09bce..42d96b16 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -142,32 +142,29 @@ class Archives(abc.MutableMapping): name = safe_encode(name) del self._archives[name] - def list(self, sort_by=None, reverse=False, prefix=''): + def list(self, sort_by=(), reverse=False, prefix='', first=None, last=None): """ Inexpensive Archive.list_archives replacement if we just need .name, .id, .ts - Returns list of borg.helpers.ArchiveInfo instances + Returns list of borg.helpers.ArchiveInfo instances. + sort_by can be a list of sort keys, they are applied in reverse order. """ + if isinstance(sort_by, (str, bytes)): + raise TypeError('sort_by must be a sequence of str') archives = [x for x in self.values() if x.name.startswith(prefix)] - if sort_by is not None: - archives = sorted(archives, key=attrgetter(sort_by)) - if reverse: + for sortkey in reversed(sort_by): + archives.sort(key=attrgetter(sortkey)) + if reverse or last: archives.reverse() - return archives + n = first or last or len(archives) + return archives[:n] - def list_filtered(self, args): + def list_considering(self, args): """ - get a filtered list of archives, considering --first/last/prefix/sort + get a list of archives, considering --first/last/prefix/sort cmdline args """ if args.location.archive: raise Error('The options --first, --last and --prefix can only be used on repository targets.') - archives = self.list(prefix=args.prefix) - for sortkey in reversed(args.sort_by.split(',')): - archives.sort(key=attrgetter(sortkey)) - if args.last: - archives.reverse() - n = args.first or args.last or len(archives) - return archives[:n] - + return self.list(sort_by=args.sort_by.split(','), prefix=args.prefix, first=args.first, last=args.last) def set_raw_dict(self, d): """set the dict we get from the msgpack unpacker""" From 5a12c6377a7930e3496ec9562d0dc0963752b5ed Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 15 Oct 2016 01:24:05 +0200 Subject: [PATCH 0298/1387] fuse mount options: add tests --- src/borg/testsuite/archiver.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index aa1a99ee..a2c44280 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1508,6 +1508,28 @@ class ArchiverTestCase(ArchiverTestCaseBase): with self.fuse_mount(self.repository_location + '::archive', mountpoint, '-o', 'allow_damaged_files'): open(os.path.join(mountpoint, path)).close() + @unittest.skipUnless(has_llfuse, 'llfuse not installed') + def test_fuse_mount_options(self): + self.cmd('init', self.repository_location) + self.create_src_archive('arch11') + self.create_src_archive('arch12') + self.create_src_archive('arch21') + self.create_src_archive('arch22') + + mountpoint = os.path.join(self.tmpdir, 'mountpoint') + with self.fuse_mount(self.repository_location, mountpoint, '--first=2', '--sort=name'): + assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch11', 'arch12'] + with self.fuse_mount(self.repository_location, mountpoint, '--last=2', '--sort=name'): + assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch21', 'arch22'] + with self.fuse_mount(self.repository_location, mountpoint, '--prefix=arch1'): + assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch11', 'arch12'] + with self.fuse_mount(self.repository_location, mountpoint, '--prefix=arch2'): + assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch21', 'arch22'] + with self.fuse_mount(self.repository_location, mountpoint, '--prefix=arch'): + assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch11', 'arch12', 'arch21', 'arch22'] + with self.fuse_mount(self.repository_location, mountpoint, '--prefix=nope'): + assert sorted(os.listdir(os.path.join(mountpoint))) == [] + def verify_aes_counter_uniqueness(self, method): seen = set() # Chunks already seen used = set() # counter values already used From 4ca0ed150b628a164c524e8df5fd58ed09d1c41f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 15 Oct 2016 15:21:38 +0200 Subject: [PATCH 0299/1387] vagrant / openbsd: use own box, fixes #1695 self-made basic openbsd box includes pre-installed rsync as vagrant is unable to install rsync. as the self-made box now has enough space on /, remove workaround for low space. delete line from tox.ini with sed so it does not try to install llfuse (which is broken on openbsd). --- Vagrantfile | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 7b09c375..5d81b398 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -106,10 +106,6 @@ end def packages_openbsd return <<-EOF . ~/.profile - mkdir -p /home/vagrant/borg - rsync -aH /vagrant/borg/ /home/vagrant/borg/ - rm -rf /vagrant/borg - ln -sf /home/vagrant/borg /vagrant/ pkg_add bash chsh -s /usr/local/bin/bash vagrant pkg_add openssl @@ -121,6 +117,8 @@ def packages_openbsd easy_install-3.4 pip pip3 install virtualenv touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile + # avoid that breaking llfuse install breaks borgbackup install under tox: + sed -i.bak '/fuse.txt/d' /vagrant/borg/borg/tox.ini EOF end @@ -271,12 +269,12 @@ def run_tests(boxname) . ~/.bash_profile cd /vagrant/borg/borg . ../borg-env/bin/activate - if which pyenv > /dev/null; then + if which pyenv 2> /dev/null; then # for testing, use the earliest point releases of the supported python versions: pyenv global 3.4.0 3.5.0 fi # otherwise: just use the system python - if which fakeroot > /dev/null; then + if which fakeroot 2> /dev/null; then echo "Running tox WITH fakeroot -u" fakeroot -u tox --skip-missing-interpreters else @@ -440,7 +438,7 @@ Vagrant.configure(2) do |config| end config.vm.define "openbsd64" do |b| - b.vm.box = "kaorimatz/openbsd-5.9-amd64" + b.vm.box = "openbsd60-64" # note: basic openbsd install for vagrant WITH sudo and rsync pre-installed b.vm.provider :virtualbox do |v| v.memory = 768 end From 63ff002b19fd71697a6341db3ebe92a20387406b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 17 Oct 2016 02:03:52 +0200 Subject: [PATCH 0300/1387] vagrant / netbsd: use own box, fixes #1671, fixes #1728 self-made basic netbsd box includes pre-installed rsync as vagrant is unable to install rsync. delete line from tox.ini with sed so it does not try to install llfuse (which is broken on netbsd). --- Vagrantfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index 5d81b398..d5e3bb57 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -144,6 +144,8 @@ def packages_netbsd easy_install-3.4 pip pip install virtualenv touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile + # fuse does not work good enough (see above), do not install llfuse: + sed -i.bak '/fuse.txt/d' /vagrant/borg/borg/tox.ini EOF end @@ -449,7 +451,7 @@ Vagrant.configure(2) do |config| end config.vm.define "netbsd64" do |b| - b.vm.box = "alex-skimlinks/netbsd-6.1.5-amd64" + b.vm.box = "netbsd70-64" b.vm.provider :virtualbox do |v| v.memory = 768 end From 498e71846c92bc938cd49fcbd5d03e95e9f6bafb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 17 Oct 2016 03:16:13 +0200 Subject: [PATCH 0301/1387] add borg debug refcount-obj ID subcommand --- borg/archiver.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/borg/archiver.py b/borg/archiver.py index fb75f8dd..ad822452 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -808,6 +808,22 @@ class Archiver: print('Done.') return EXIT_SUCCESS + @with_repository(manifest=False, exclusive=True, cache=True) + def do_debug_refcount_obj(self, args, repository, manifest, key, cache): + """display refcounts for the objects with the given IDs""" + for hex_id in args.ids: + try: + id = unhexlify(hex_id) + except ValueError: + print("object id %s is invalid." % hex_id) + else: + try: + refcount = cache.chunks[id][0] + print("object %s has %d referrers [info from chunks cache]." % (hex_id, refcount)) + except KeyError: + print("object %s not found [info from chunks cache]." % hex_id) + return EXIT_SUCCESS + @with_repository(lock=False, manifest=False) def do_break_lock(self, args, repository): """Break the repository lock (e.g. in case it was left by a dead borg.""" @@ -1782,6 +1798,33 @@ class Archiver: subparser.add_argument('ids', metavar='IDs', nargs='+', type=str, help='hex object ID(s) to delete from the repo') + debug_refcount_obj_epilog = textwrap.dedent(""" + This command displays the reference count for objects from the repository. + """) + subparser = subparsers.add_parser('debug-refcount-obj', parents=[common_parser], + description=self.do_debug_refcount_obj.__doc__, + epilog=debug_refcount_obj_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='show refcount for object from repository (debug)') + subparser.set_defaults(func=self.do_debug_refcount_obj) + subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', + type=location_validator(archive=False), + help='repository to use') + subparser.add_argument('ids', metavar='IDs', nargs='+', type=str, + help='hex object ID(s) to show refcounts for') + + subparser = debug_parsers.add_parser('refcount-obj', parents=[common_parser], + description=self.do_debug_refcount_obj.__doc__, + epilog=debug_refcount_obj_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='show refcount for object from repository (debug)') + subparser.set_defaults(func=self.do_debug_refcount_obj) + subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', + type=location_validator(archive=False), + help='repository to use') + subparser.add_argument('ids', metavar='IDs', nargs='+', type=str, + help='hex object ID(s) to show refcounts for') + return parser def get_args(self, argv, cmd): From 4d7af95ad23db3be40b36a856e57172b1e0f1743 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 17 Oct 2016 02:14:19 +0200 Subject: [PATCH 0302/1387] update 1.0 CHANGES --- docs/changes.rst | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index b1c8a1ba..8f8dc9a8 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,8 +50,8 @@ The best check that everything is ok is to run a dry-run extraction:: borg extract -v --dry-run REPO::ARCHIVE -Version 1.0.8rc1 (not released yet) ------------------------------------ +Version 1.0.8rc1 (2016-10-17) +----------------------------- Bug fixes: @@ -63,15 +63,22 @@ Bug fixes: also correctly processes broken symlinks. before this regressed to a crash (5b45385) a broken symlink would've been skipped. - process_symlink: fix missing backup_io() - Fixes a chmod/chown/chgrp/unlink/rename/... crash race between getting dirents - and dispatching to process_symlink. -- yes(): abort on wrong answers, saying so + Fixes a chmod/chown/chgrp/unlink/rename/... crash race between getting + dirents and dispatching to process_symlink. +- yes(): abort on wrong answers, saying so, #1622 - fixed exception borg serve raised when connection was closed before reposiory was openend. add an error message for this. - fix read-from-closed-FD issue, #1551 (this seems not to get triggered in 1.0.x, but was discovered in master) - hashindex: fix iterators (always raise StopIteration when exhausted) (this seems not to get triggered in 1.0.x, but was discovered in master) +- enable relative pathes in ssh:// repo URLs, via /./relpath hack, fixes #1655 +- allow repo pathes with colons, fixes #1705 +- update changed repo location immediately after acceptance, #1524 +- fix debug get-obj / delete-obj crash if object not found and remote repo, + #1684 +- pyinstaller: use a spec file to build borg.exe binary, exclude osxfuse dylib + on Mac OS X (avoids mismatch lib <-> driver), #1619 New features: @@ -82,6 +89,8 @@ New features: special "paper" format with by line checksums for printed backups. For the paper format, the import is an interactive process which checks each line as soon as it is input. +- add "borg debug-refcount-obj" to determine a repo objects' referrer counts, + #1352 Other changes: @@ -90,10 +99,19 @@ Other changes: - setup.py: Add subcommand support to build_usage. - remote: change exception message for unexpected RPC data format to indicate dataflow direction. -- vagrant: +- improved messages / error reporting: + + - IntegrityError: add placeholder for message, so that the message we give + appears not only in the traceback, but also in the (short) error message, + #1572 + - borg.key: include chunk id in exception msgs, #1571 + - better messages for cache newer than repo, fixes #1700 +- vagrant (testing/build VMs): - upgrade OSXfuse / FUSE for macOS to 3.5.2 - - update Debian Wheezy boxes to 7.11 + - update Debian Wheezy boxes, #1686 + - openbsd / netbsd: use own boxes, fixes misc rsync installation and + fuse/llfuse related testing issues, #1695 #1696 #1670 #1671 #1728 - docs: - add docs for "key export" and "key import" commands, #1641 @@ -109,12 +127,17 @@ Other changes: - add debug-info usage help file - internals.rst: fix typos - setup.py: fix build_usage to always process all commands + - added docs explaining multiple --restrict-to-path flags, #1602 + - add more specific warning about write-access debug commands, #1587 + - clarify FAQ regarding backup of virtual machines, #1672 - tests: - work around fuse xattr test issue with recent fakeroot - simplify repo/hashindex tests - travis: test fuse-enabled borg, use trusty to have a recent FUSE - re-enable fuse tests for RemoteArchiver (no deadlocks any more) + - clean env for pytest based tests, #1714 + - fuse_mount contextmanager: accept any options Version 1.0.7 (2016-08-19) From b3133f6394230662f5cffd55c294908dc750777a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 17 Oct 2016 03:36:49 +0200 Subject: [PATCH 0303/1387] update api.rst --- docs/api.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 8dfc8cce..89a95304 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -27,6 +27,10 @@ API Documentation :members: :undoc-members: +.. automodule:: borg.keymanager + :members: + :undoc-members: + .. automodule:: borg.logger :members: :undoc-members: From 3550b24ed99807d5dbad2ec28d2f2e8f72933461 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 17 Oct 2016 03:39:38 +0200 Subject: [PATCH 0304/1387] ran build_usage --- docs/usage/info.rst.inc | 9 +++++++++ docs/usage/key_export.rst.inc | 10 +++++----- docs/usage/serve.rst.inc | 6 +++++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index 68c9194e..cd745b53 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -35,3 +35,12 @@ Description ~~~~~~~~~~~ This command displays some detailed information about the specified archive. + +Please note that the deduplicated sizes of the individual archives do not add +up to the deduplicated size of the repository ("all archives"), because the two +are meaning different things: + +This archive / deduplicated size = amount of data stored ONLY for this archive + = unique chunks of this archive. +All archives / deduplicated size = amount of data stored in the repo + = all chunks in the repository. diff --git a/docs/usage/key_export.rst.inc b/docs/usage/key_export.rst.inc index 47e9e119..a87c8192 100644 --- a/docs/usage/key_export.rst.inc +++ b/docs/usage/key_export.rst.inc @@ -42,14 +42,14 @@ If repository encryption is used, the repository is inaccessible without the key. This command allows to backup this essential key. There are two backup formats. The normal backup format is suitable for -digital storage as a file. The ``--paper`` backup format is optimized for -print out and later type-in, with per line checks to reduce problems -with manual input. +digital storage as a file. The ``--paper`` backup format is optimized +for printing and typing in while importing, with per line checks to +reduce problems with manual input. For repositories using keyfile encryption the key is saved locally on the system that is capable of doing backups. To guard against loss -of this key the key needs to be backed up independent of the main data -backup. +of this key, the key needs to be backed up independently of the main +data backup. For repositories using the repokey encryption the key is saved in the repository in the config file. A backup is thus not strictly needed, diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index eb57a975..48f97074 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -29,7 +29,11 @@ borg serve --umask M set umask to M (local and remote, default: 0077) --remote-path PATH set remote path to executable (default: "borg") --restrict-to-path PATH - restrict repository access to PATH + restrict repository access to PATH. Can be specified + multiple times to allow the client access to several + directories. Access to all sub-directories is granted + implicitly; PATH doesn't need to directly point to a + repository. --append-only only allow appending to repository segment files Description From 57402b120bdc51e8b574010a01aaa6d72abf8c16 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 17 Oct 2016 17:37:33 +0200 Subject: [PATCH 0305/1387] vagrant: no chown when rsyncing it tries to do chown -R vagrant.vagrant, but some boxes do not have a vagrant group and break there. also, we do our own chown in the provisioning scripts. --- Vagrantfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index d5e3bb57..53ea02c3 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -301,7 +301,7 @@ end Vagrant.configure(2) do |config| # use rsync to copy content to the folder - config.vm.synced_folder ".", "/vagrant/borg/borg", :type => "rsync", :rsync__args => ["--verbose", "--archive", "--delete", "-z"] + config.vm.synced_folder ".", "/vagrant/borg/borg", :type => "rsync", :rsync__args => ["--verbose", "--archive", "--delete", "-z"], :rsync__chown => false # do not let the VM access . on the host machine via the default shared folder! config.vm.synced_folder ".", "/vagrant", disabled: true From f10700841faaea976300dfc48cfef0e985935ad2 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 19 Oct 2016 00:56:22 +0200 Subject: [PATCH 0306/1387] add comment about requiring llfuse, fixes #1726 --- requirements.d/fuse.txt | 2 +- setup.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/requirements.d/fuse.txt b/requirements.d/fuse.txt index be35d2ae..0df0f338 100644 --- a/requirements.d/fuse.txt +++ b/requirements.d/fuse.txt @@ -1,4 +1,4 @@ # low-level FUSE support library for "borg mount" -# see comments setup.py about this version requirement. +# please see the comments in setup.py about llfuse. llfuse<2.0 diff --git a/setup.py b/setup.py index 8ba33e73..ffb9aae9 100644 --- a/setup.py +++ b/setup.py @@ -21,12 +21,17 @@ on_rtd = os.environ.get('READTHEDOCS') # Also, we might use some rather recent API features. install_requires = ['msgpack-python>=0.4.6', ] +# note for package maintainers: if you package borgbackup for distribution, +# please add llfuse as a *requirement* on all platforms that have a working +# llfuse package. "borg mount" needs llfuse to work. +# if you do not have llfuse, do not require it, most of borgbackup will work. extras_require = { # llfuse 0.40 (tested, proven, ok), needs FUSE version >= 2.8.0 # llfuse 0.41 (tested shortly, looks ok), needs FUSE version >= 2.8.0 # llfuse 0.41.1 (tested shortly, looks ok), needs FUSE version >= 2.8.0 # llfuse 0.42 (tested shortly, looks ok), needs FUSE version >= 2.8.0 # llfuse 1.0 (tested shortly, looks ok), needs FUSE version >= 2.8.0 + # llfuse 1.1.1 (tested shortly, looks ok), needs FUSE version >= 2.8.0 # llfuse 2.0 will break API 'fuse': ['llfuse<2.0', ], } From c000eb8083f07c1143e1dcae2ef1514c725fac96 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 19 Oct 2016 01:13:44 +0200 Subject: [PATCH 0307/1387] add clarification about append-only mode, fixes #1689 --- docs/usage.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/usage.rst b/docs/usage.rst index 0452c164..d9fcc3a2 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -800,9 +800,14 @@ That's all to it. Drawbacks +++++++++ -As data is only appended, and nothing deleted, commands like ``prune`` or ``delete`` +As data is only appended, and nothing removed, commands like ``prune`` or ``delete`` won't free disk space, they merely tag data as deleted in a new transaction. +Be aware that as soon as you write to the repo in non-append-only mode (e.g. prune, +delete or create archives from an admin machine), it will remove the deleted objects +permanently (including the ones that were already marked as deleted, but not removed, +in append-only mode). + Note that you can go back-and-forth between normal and append-only operation by editing the configuration file, it's not a "one way trip". From ade405eae012629b397cdc0c88f2044ee64f9c31 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 19 Oct 2016 01:51:25 +0200 Subject: [PATCH 0308/1387] ignore security.selinux xattrs, fixes #1735 they fail the FUSE tests on centos7. --- borg/testsuite/__init__.py | 13 +++++++++++-- borg/testsuite/archiver.py | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/borg/testsuite/__init__.py b/borg/testsuite/__init__.py index 6b85538b..8e09771d 100644 --- a/borg/testsuite/__init__.py +++ b/borg/testsuite/__init__.py @@ -35,6 +35,15 @@ if sys.platform.startswith('netbsd'): setup_logging() +def no_selinux(x): + # selinux fails our FUSE tests, thus ignore selinux xattrs + SELINUX_KEY = 'security.selinux' + if isinstance(x, dict): + return {k: v for k, v in x.items() if k != SELINUX_KEY} + if isinstance(x, list): + return [k for k in x if k != SELINUX_KEY] + + class BaseTestCase(unittest.TestCase): """ """ @@ -87,8 +96,8 @@ class BaseTestCase(unittest.TestCase): else: d1.append(round(s1.st_mtime_ns, st_mtime_ns_round)) d2.append(round(s2.st_mtime_ns, st_mtime_ns_round)) - d1.append(get_all(path1, follow_symlinks=False)) - d2.append(get_all(path2, follow_symlinks=False)) + d1.append(no_selinux(get_all(path1, follow_symlinks=False))) + d2.append(no_selinux(get_all(path2, follow_symlinks=False))) self.assert_equal(d1, d2) for sub_diff in diff.subdirs.values(): self._assert_dirs_equal_cmp(sub_diff) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 8508639e..4a470c2b 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -26,7 +26,7 @@ from ..key import RepoKey, KeyfileKey, Passphrase from ..keymanager import RepoIdMismatch, NotABorgKeyFile from ..remote import RemoteRepository, PathNotAllowed from ..repository import Repository -from . import BaseTestCase, changedir, environment_variable +from . import BaseTestCase, changedir, environment_variable, no_selinux try: import llfuse @@ -1067,7 +1067,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): in_fn = 'input/fusexattr' out_fn = os.path.join(mountpoint, 'input', 'fusexattr') if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path): - assert xattr.listxattr(out_fn) == ['user.foo', ] + assert no_selinux(xattr.listxattr(out_fn)) == ['user.foo', ] assert xattr.getxattr(out_fn, 'user.foo') == b'bar' else: assert xattr.listxattr(out_fn) == [] From e02d1a66b89cef5606cac1eed5eabcc7e9520183 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 19 Oct 2016 17:38:30 +0200 Subject: [PATCH 0309/1387] fix byte range error in test, fixes #1740 --- src/borg/testsuite/key.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index c197028d..95c74e36 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -155,7 +155,7 @@ class TestKey: def _corrupt_byte(self, key, data, offset): data = bytearray(data) - data[offset] += 1 + data[offset] ^= 1 with pytest.raises(IntegrityError): key.decrypt(b'', data) @@ -186,7 +186,7 @@ class TestKey: id = key.id_hash(plaintext) key.assert_id(id, plaintext) id_changed = bytearray(id) - id_changed[0] += 1 + id_changed[0] ^= 1 with pytest.raises(IntegrityError): key.assert_id(id_changed, plaintext) plaintext_changed = plaintext + b'1' From 4c884fd0753ca2ff442bcd43be8abdd43ba91c35 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 18 Oct 2016 03:57:42 +0200 Subject: [PATCH 0310/1387] borg check --first / --last / --sort / --prefix, fixes #1663 --- src/borg/archive.py | 30 ++++++++++++++---------------- src/borg/archiver.py | 14 +++++--------- src/borg/testsuite/archiver.py | 6 ++++++ 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index bfc58361..e831be22 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -978,19 +978,19 @@ class ArchiveChecker: self.error_found = False self.possibly_superseded = set() - def check(self, repository, repair=False, archive=None, last=None, prefix=None, verify_data=False, - save_space=False): + def check(self, repository, repair=False, archive=None, first=0, last=0, sort_by='', prefix='', + verify_data=False, save_space=False): """Perform a set of checks on 'repository' :param repair: enable repair mode, write updated or corrected data into repository :param archive: only check this archive - :param last: only check this number of recent archives + :param first/last/sort_by: only check this number of first/last archives ordered by sort_by :param prefix: only check archives with this prefix :param verify_data: integrity verification of data referenced by archives :param save_space: Repository.commit(save_space) """ logger.info('Starting archive consistency check...') - self.check_all = archive is None and last is None and prefix is None + self.check_all = archive is None and not any((first, last, prefix)) self.repair = repair self.repository = repository self.init_chunks() @@ -1003,7 +1003,7 @@ class ArchiveChecker: self.manifest = self.rebuild_manifest() else: self.manifest, _ = Manifest.load(repository, key=self.key) - self.rebuild_refcounts(archive=archive, last=last, prefix=prefix) + self.rebuild_refcounts(archive=archive, first=first, last=last, sort_by=sort_by, prefix=prefix) self.orphan_chunks_check() self.finish(save_space=save_space) if self.error_found: @@ -1160,7 +1160,7 @@ class ArchiveChecker: logger.info('Manifest rebuild complete.') return manifest - def rebuild_refcounts(self, archive=None, last=None, prefix=None): + def rebuild_refcounts(self, archive=None, first=0, last=0, sort_by='', prefix=''): """Rebuild object reference counts by walking the metadata Missing and/or incorrect data is repaired when detected @@ -1294,12 +1294,11 @@ class ArchiveChecker: i += 1 if archive is None: - # we need last N or all archives - archive_infos = self.manifest.archives.list(sort_by=['ts'], reverse=True) - if prefix is not None: - archive_infos = [info for info in archive_infos if info.name.startswith(prefix)] - num_archives = len(archive_infos) - end = None if last is None else min(num_archives, last) + sort_by = sort_by.split(',') + if any((first, last, prefix)): + archive_infos = self.manifest.archives.list(sort_by=sort_by, prefix=prefix, first=first, last=last) + else: + archive_infos = self.manifest.archives.list(sort_by=sort_by) else: # we only want one specific archive info = self.manifest.archives.get(archive) @@ -1308,12 +1307,11 @@ class ArchiveChecker: archive_infos = [] else: archive_infos = [info] - num_archives = 1 - end = 1 + num_archives = len(archive_infos) with cache_if_remote(self.repository) as repository: - for i, info in enumerate(archive_infos[:end]): - logger.info('Analyzing archive {} ({}/{})'.format(info.name, num_archives - i, num_archives)) + for i, info in enumerate(archive_infos): + logger.info('Analyzing archive {} ({}/{})'.format(info.name, i + 1, num_archives)) archive_id = info.id if archive_id not in self.chunks: logger.error('Archive metadata block is missing!') diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 40024261..bc0f5ae5 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -206,16 +206,16 @@ class Archiver: truish=('YES', ), retry=False, env_var_override='BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'): return EXIT_ERROR - if args.repo_only and args.verify_data: - self.print_error("--repository-only and --verify-data contradict each other. Please select one.") + if args.repo_only and any((args.verify_data, args.first, args.last, args.prefix)): + self.print_error("--repository-only contradicts --first, --last, --prefix and --verify-data arguments.") return EXIT_ERROR if not args.archives_only: if not repository.check(repair=args.repair, save_space=args.save_space): return EXIT_WARNING if not args.repo_only and not ArchiveChecker().check( repository, repair=args.repair, archive=args.location.archive, - last=args.last, prefix=args.prefix, verify_data=args.verify_data, - save_space=args.save_space): + first=args.first, last=args.last, sort_by=args.sort_by or 'ts', prefix=args.prefix, + verify_data=args.verify_data, save_space=args.save_space): return EXIT_WARNING return EXIT_SUCCESS @@ -1660,14 +1660,10 @@ class Archiver: subparser.add_argument('--save-space', dest='save_space', action='store_true', default=False, help='work slower, but using less space') - subparser.add_argument('--last', dest='last', - type=int, default=None, metavar='N', - help='only check last N archives (Default: all)') - subparser.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, - help='only consider archive names starting with this prefix') subparser.add_argument('-p', '--progress', dest='progress', action='store_true', default=False, help="""show progress display while checking""") + self.add_archives_filters_args(subparser) change_passphrase_epilog = textwrap.dedent(""" The key files used for repository encryption are optionally passphrase diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index a2c44280..ecaa6be6 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2014,6 +2014,12 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.assert_in('Starting archive consistency check', output) output = self.cmd('check', '-v', '--archives-only', '--prefix=archive2', self.repository_location, exit_code=0) self.assert_not_in('archive1', output) + output = self.cmd('check', '-v', '--archives-only', '--first=1', self.repository_location, exit_code=0) + self.assert_in('archive1', output) + self.assert_not_in('archive2', output) + output = self.cmd('check', '-v', '--archives-only', '--last=1', self.repository_location, exit_code=0) + self.assert_not_in('archive1', output) + self.assert_in('archive2', output) def test_missing_file_chunk(self): archive, repository = self.open_archive('archive1') From 2b90e45dd18d204e57c7973138e1384a5f1bcde3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 4 Sep 2016 03:01:29 +0200 Subject: [PATCH 0311/1387] vagrant: fix fuse permission issues on linux/freebsd, fixes #1544 --- Vagrantfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Vagrantfile b/Vagrantfile index 53ea02c3..cf440fb5 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -25,6 +25,8 @@ def packages_debianoid # for building borgbackup and dependencies: apt-get install -y libssl-dev libacl1-dev liblz4-dev libfuse-dev fuse pkg-config usermod -a -G fuse $username + chgrp fuse /dev/fuse + chmod 666 /dev/fuse apt-get install -y fakeroot build-essential git apt-get install -y python3-dev python3-setuptools # for building python: @@ -45,6 +47,8 @@ def packages_redhatted # for building borgbackup and dependencies: yum install -y openssl-devel openssl libacl-devel libacl lz4-devel fuse-devel fuse pkgconfig usermod -a -G fuse vagrant + chgrp fuse /dev/fuse + chmod 666 /dev/fuse yum install -y fakeroot gcc git patch # needed to compile msgpack-python (otherwise it will use slow fallback code): yum install -y gcc-c++ @@ -96,6 +100,8 @@ def packages_freebsd kldload fuse sysctl vfs.usermount=1 pw groupmod operator -M vagrant + # /dev/fuse has group operator + chmod 666 /dev/fuse touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile # install all the (security and other) updates, packages pkg update From f9aa74e7e1a5baebc105a35323dd0c446dd56032 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 4 Sep 2016 04:32:16 +0200 Subject: [PATCH 0312/1387] skip fuse test for borg binary + fakeroot strange: works on wheezy, blows up on xenial --- borg/testsuite/archiver.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 4a470c2b..ee68f61b 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -27,6 +27,7 @@ from ..keymanager import RepoIdMismatch, NotABorgKeyFile from ..remote import RemoteRepository, PathNotAllowed from ..repository import Repository from . import BaseTestCase, changedir, environment_variable, no_selinux +from .platform import fakeroot_detected try: import llfuse @@ -1316,6 +1317,12 @@ class ArchiverTestCaseBinary(ArchiverTestCase): def test_overwrite(self): pass + def test_fuse(self): + if fakeroot_detected(): + unittest.skip('test_fuse with the binary is not compatible with fakeroot') + else: + super().test_fuse() + class ArchiverCheckTestCase(ArchiverTestCaseBase): From d49a782796413e4b1492f73b3dd213fd696fefe6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 21 Oct 2016 03:43:38 +0200 Subject: [PATCH 0313/1387] implement borgmajor/minor/patch placeholders, fixes #1694 --- borg/__init__.py | 10 +++++++++- borg/archiver.py | 18 +++++++++++++++--- borg/helpers.py | 4 ++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/borg/__init__.py b/borg/__init__.py index e292841a..2dcf9531 100644 --- a/borg/__init__.py +++ b/borg/__init__.py @@ -1,3 +1,11 @@ -# This is a python package +import re from ._version import version as __version__ + +version_re = r'(\d+)\.(\d+)\.(\d+)' + +m = re.match(version_re, __version__) +if m: + __version_tuple__ = tuple(map(int, m.groups())) +else: + raise RuntimeError("Can't parse __version__: %r" % __version__) diff --git a/borg/archiver.py b/borg/archiver.py index ad822452..f417c245 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -949,7 +949,19 @@ class Archiver: {borgversion} - The version of borg. + The version of borg, e.g.: 1.0.8rc1 + + {borgmajor} + + The version of borg, only the major version, e.g.: 1 + + {borgminor} + + The version of borg, only major and minor version, e.g.: 1.0 + + {borgpatch} + + The version of borg, only major, minor and patch version, e.g.: 1.0.8 Examples:: @@ -1229,8 +1241,8 @@ class Archiver: '.checkpoint.N' (with N being a number), because these names are used for checkpoints and treated in special ways. - In the archive name, you may use the following format tags: - {now}, {utcnow}, {fqdn}, {hostname}, {user}, {pid}, {borgversion} + In the archive name, you may use the following placeholders: + {now}, {utcnow}, {fqdn}, {hostname}, {user} and some others. To speed up pulling backups over sshfs and similar network file systems which do not provide correct inode information the --ignore-inode flag can be used. This diff --git a/borg/helpers.py b/borg/helpers.py index 2cceb2af..a4469c99 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -28,6 +28,7 @@ from fnmatch import translate from operator import attrgetter from . import __version__ as borg_version +from . import __version_tuple__ as borg_version_tuple from . import hashindex from . import chunker from . import crypto @@ -585,6 +586,9 @@ def replace_placeholders(text): 'utcnow': current_time.utcnow(), 'user': uid2user(os.getuid(), os.getuid()), 'borgversion': borg_version, + 'borgmajor': '%d' % borg_version_tuple[:1], + 'borgminor': '%d.%d' % borg_version_tuple[:2], + 'borgpatch': '%d.%d.%d' % borg_version_tuple[:3], } return format_line(text, data) From d363907b5afd38ba34510ecdf4a9ebdd13c0225f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 21 Oct 2016 04:15:33 +0200 Subject: [PATCH 0314/1387] document BORG_NONCES_DIR, fixes #1592 --- docs/usage.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index 685e07fa..54bda11d 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -111,6 +111,9 @@ Directories and files: Default to '~/.config/borg/keys'. This directory contains keys for encrypted repositories. BORG_KEY_FILE When set, use the given filename as repository key file. + BORG_NONCES_DIR + Default to '~/.config/borg/key-nonces'. This directory contains information borg uses to + track its usage of NONCES ("numbers used once" - usually in encryption context). BORG_CACHE_DIR Default to '~/.cache/borg'. This directory contains the local cache and might need a lot of space for dealing with big repositories). From 22f77b908f31cd4d0de77a16a77f868843322f34 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 22 Oct 2016 00:20:50 +0200 Subject: [PATCH 0315/1387] implement borgmajor/minor/patch placeholders, fixes #1694 --- borg/__init__.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/borg/__init__.py b/borg/__init__.py index 2dcf9531..9ac0e0f9 100644 --- a/borg/__init__.py +++ b/borg/__init__.py @@ -1,11 +1,6 @@ -import re +from distutils.version import LooseVersion from ._version import version as __version__ -version_re = r'(\d+)\.(\d+)\.(\d+)' -m = re.match(version_re, __version__) -if m: - __version_tuple__ = tuple(map(int, m.groups())) -else: - raise RuntimeError("Can't parse __version__: %r" % __version__) +__version_tuple__ = tuple(LooseVersion(__version__).version[:3]) From 766a43afc36dddec631fba93dada75a3e7d125fd Mon Sep 17 00:00:00 2001 From: Radu Ciorba Date: Thu, 15 Sep 2016 21:27:16 +0300 Subject: [PATCH 0316/1387] shortcut hashindex_set by having hashindex_lookup hint about address --- src/borg/_hashindex.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/borg/_hashindex.c b/src/borg/_hashindex.c index 0a92ca60..7847cc5c 100644 --- a/src/borg/_hashindex.c +++ b/src/borg/_hashindex.c @@ -109,12 +109,16 @@ hashindex_index(HashIndex *index, const void *key) } static int -hashindex_lookup(HashIndex *index, const void *key) +hashindex_lookup(HashIndex *index, const void *key, int *skip_hint) { int didx = -1; int start = hashindex_index(index, key); int idx = start; - for(;;) { + int offset; + for(offset=0;;offset++) { + if (skip_hint != NULL) { + (*skip_hint) = offset; + } if(BUCKET_IS_EMPTY(index, idx)) { return -1; @@ -369,7 +373,7 @@ hashindex_write(HashIndex *index, const char *path) static const void * hashindex_get(HashIndex *index, const void *key) { - int idx = hashindex_lookup(index, key); + int idx = hashindex_lookup(index, key, NULL); if(idx < 0) { return NULL; } @@ -379,7 +383,8 @@ hashindex_get(HashIndex *index, const void *key) static int hashindex_set(HashIndex *index, const void *key, const void *value) { - int idx = hashindex_lookup(index, key); + int offset = 0; + int idx = hashindex_lookup(index, key, &offset); uint8_t *ptr; if(idx < 0) { @@ -387,8 +392,9 @@ hashindex_set(HashIndex *index, const void *key, const void *value) if(!hashindex_resize(index, grow_size(index->num_buckets))) { return 0; } + offset = 0; } - idx = hashindex_index(index, key); + idx = (hashindex_index(index, key) + offset) % index->num_buckets; while(!BUCKET_IS_EMPTY(index, idx) && !BUCKET_IS_DELETED(index, idx)) { idx = (idx + 1) % index->num_buckets; } @@ -407,7 +413,7 @@ hashindex_set(HashIndex *index, const void *key, const void *value) static int hashindex_delete(HashIndex *index, const void *key) { - int idx = hashindex_lookup(index, key); + int idx = hashindex_lookup(index, key, NULL); if (idx < 0) { return 1; } From 88f7f8673dacc5d00ffdd34f8e17f5b19a7e938b Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Mon, 24 Oct 2016 01:09:09 +0200 Subject: [PATCH 0317/1387] docs/deployment.rst: do not use bare variables in ansible snippet The current snippet throws deprecation warnings: ``` [DEPRECATION WARNING]: Using bare variables is deprecated. Update your playbooks so that the environment value uses the full variable syntax ('{{auth_users}}'). This feature will be removed in a future release. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg. ``` --- docs/deployment.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/deployment.rst b/docs/deployment.rst index c73c6ddb..9586c72a 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -149,10 +149,10 @@ package manager to install and keep borg up-to-date. - authorized_key: user="{{ user }}" key="{{ item.key }}" key_options='command="cd {{ pool }}/{{ item.host }};borg serve --restrict-to-path {{ pool }}/{{ item.host }}",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc' - with_items: auth_users + with_items: "{{ auth_users }}" - file: path="{{ home }}/.ssh/authorized_keys" owner="{{ user }}" group="{{ group }}" mode=0600 state=file - file: path="{{ pool }}/{{ item.host }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory - with_items: auth_users + with_items: "{{ auth_users }}" Enhancements ------------ From 48fa449e39c3984303e9190c1e85050abb0fcffb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 23 Oct 2016 20:36:05 +0200 Subject: [PATCH 0318/1387] assert_dirs_equal: add ignore_bsdflags and ignore_xattrs argument bsdflags are not supported in the FUSE mount. xattrs are supported, but are tested separately. --- borg/testsuite/__init__.py | 15 ++++++++------- borg/testsuite/archiver.py | 11 ++++++++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/borg/testsuite/__init__.py b/borg/testsuite/__init__.py index 8e09771d..42b935d1 100644 --- a/borg/testsuite/__init__.py +++ b/borg/testsuite/__init__.py @@ -60,11 +60,11 @@ class BaseTestCase(unittest.TestCase): yield self.assert_true(os.path.exists(path), '{} should exist'.format(path)) - def assert_dirs_equal(self, dir1, dir2): + def assert_dirs_equal(self, dir1, dir2, **kwargs): diff = filecmp.dircmp(dir1, dir2) - self._assert_dirs_equal_cmp(diff) + self._assert_dirs_equal_cmp(diff, **kwargs) - def _assert_dirs_equal_cmp(self, diff): + def _assert_dirs_equal_cmp(self, diff, ignore_bsdflags=False, ignore_xattrs=False): self.assert_equal(diff.left_only, []) self.assert_equal(diff.right_only, []) self.assert_equal(diff.diff_files, []) @@ -77,7 +77,7 @@ class BaseTestCase(unittest.TestCase): # Assume path2 is on FUSE if st_dev is different fuse = s1.st_dev != s2.st_dev attrs = ['st_mode', 'st_uid', 'st_gid', 'st_rdev'] - if has_lchflags: + if has_lchflags and not ignore_bsdflags: attrs.append('st_flags') if not fuse or not os.path.isdir(path1): # dir nlink is always 1 on our fuse filesystem @@ -96,11 +96,12 @@ class BaseTestCase(unittest.TestCase): else: d1.append(round(s1.st_mtime_ns, st_mtime_ns_round)) d2.append(round(s2.st_mtime_ns, st_mtime_ns_round)) - d1.append(no_selinux(get_all(path1, follow_symlinks=False))) - d2.append(no_selinux(get_all(path2, follow_symlinks=False))) + if not ignore_xattrs: + d1.append(no_selinux(get_all(path1, follow_symlinks=False))) + d2.append(no_selinux(get_all(path2, follow_symlinks=False))) self.assert_equal(d1, d2) for sub_diff in diff.subdirs.values(): - self._assert_dirs_equal_cmp(sub_diff) + self._assert_dirs_equal_cmp(sub_diff, ignore_bsdflags=ignore_bsdflags, ignore_xattrs=ignore_xattrs) @contextmanager def fuse_mount(self, location, mountpoint, *options): diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index ee68f61b..cbda687b 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1040,11 +1040,16 @@ class ArchiverTestCase(ArchiverTestCaseBase): mountpoint = os.path.join(self.tmpdir, 'mountpoint') # mount the whole repository, archive contents shall show up in archivename subdirs of mountpoint: with self.fuse_mount(self.repository_location, mountpoint): - self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive', 'input')) - self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive2', 'input')) + # bsdflags are not supported by the FUSE mount + # we also ignore xattrs here, they are tested separately + self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive', 'input'), + ignore_bsdflags=True, ignore_xattrs=True) + self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive2', 'input'), + ignore_bsdflags=True, ignore_xattrs=True) # mount only 1 archive, its contents shall show up directly in mountpoint: with self.fuse_mount(self.repository_location + '::archive', mountpoint): - self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input')) + self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input'), + ignore_bsdflags=True, ignore_xattrs=True) # regular file in_fn = 'input/file1' out_fn = os.path.join(mountpoint, 'input', 'file1') From ede3b4a3549cc1f71e47a2a0e213d94a55e6135d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 24 Oct 2016 00:49:03 +0200 Subject: [PATCH 0319/1387] fuse: test troublesome xattrs last --- borg/testsuite/archiver.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index cbda687b..3a147856 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1069,20 +1069,6 @@ class ArchiverTestCase(ArchiverTestCaseBase): # read with open(in_fn, 'rb') as in_f, open(out_fn, 'rb') as out_f: assert in_f.read() == out_f.read() - # list/read xattrs - in_fn = 'input/fusexattr' - out_fn = os.path.join(mountpoint, 'input', 'fusexattr') - if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path): - assert no_selinux(xattr.listxattr(out_fn)) == ['user.foo', ] - assert xattr.getxattr(out_fn, 'user.foo') == b'bar' - else: - assert xattr.listxattr(out_fn) == [] - try: - xattr.getxattr(out_fn, 'user.foo') - except OSError as e: - assert e.errno == llfuse.ENOATTR - else: - assert False, "expected OSError(ENOATTR), but no error was raised" # hardlink (to 'input/file1') in_fn = 'input/hardlink' out_fn = os.path.join(mountpoint, 'input', 'hardlink') @@ -1102,6 +1088,20 @@ class ArchiverTestCase(ArchiverTestCaseBase): out_fn = os.path.join(mountpoint, 'input', 'fifo1') sto = os.stat(out_fn) assert stat.S_ISFIFO(sto.st_mode) + # list/read xattrs + in_fn = 'input/fusexattr' + out_fn = os.path.join(mountpoint, 'input', 'fusexattr') + if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path): + assert no_selinux(xattr.listxattr(out_fn)) == ['user.foo', ] + assert xattr.getxattr(out_fn, 'user.foo') == b'bar' + else: + assert xattr.listxattr(out_fn) == [] + try: + xattr.getxattr(out_fn, 'user.foo') + except OSError as e: + assert e.errno == llfuse.ENOATTR + else: + assert False, "expected OSError(ENOATTR), but no error was raised" @unittest.skipUnless(has_llfuse, 'llfuse not installed') def test_fuse_allow_damaged_files(self): From 02ecf047801e515c6ba57f8f1560cf6d354aa379 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 24 Oct 2016 01:19:36 +0200 Subject: [PATCH 0320/1387] fuse tests: catch ENOTSUP on freebsd seems like fuse does not support xattrs there at all. --- borg/testsuite/archiver.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 3a147856..3cd317b7 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1089,19 +1089,26 @@ class ArchiverTestCase(ArchiverTestCaseBase): sto = os.stat(out_fn) assert stat.S_ISFIFO(sto.st_mode) # list/read xattrs - in_fn = 'input/fusexattr' - out_fn = os.path.join(mountpoint, 'input', 'fusexattr') - if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path): - assert no_selinux(xattr.listxattr(out_fn)) == ['user.foo', ] - assert xattr.getxattr(out_fn, 'user.foo') == b'bar' - else: - assert xattr.listxattr(out_fn) == [] - try: - xattr.getxattr(out_fn, 'user.foo') - except OSError as e: - assert e.errno == llfuse.ENOATTR + try: + in_fn = 'input/fusexattr' + out_fn = os.path.join(mountpoint, 'input', 'fusexattr') + if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path): + assert no_selinux(xattr.listxattr(out_fn)) == ['user.foo', ] + assert xattr.getxattr(out_fn, 'user.foo') == b'bar' else: - assert False, "expected OSError(ENOATTR), but no error was raised" + assert xattr.listxattr(out_fn) == [] + try: + xattr.getxattr(out_fn, 'user.foo') + except OSError as e: + assert e.errno == llfuse.ENOATTR + else: + assert False, "expected OSError(ENOATTR), but no error was raised" + except OSError as err: + if sys.platform.startswith(('freebsd', )) and err.errno == errno.ENOTSUP: + # some systems have no xattr support on FUSE + pass + else: + raise @unittest.skipUnless(has_llfuse, 'llfuse not installed') def test_fuse_allow_damaged_files(self): From baa77c0e0483a6e09035462dcfaee4bde2a90d06 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 18 Oct 2016 21:36:23 +0200 Subject: [PATCH 0321/1387] avoid previous_location mismatch, fixes #1741 due to the changed canonicalization for relative pathes in PR #1711 (implement /./ relpath hack), there would be a changed repo location warning and the user would be asked if this is ok. this would break automation and require manual intervention, which is unwanted. thus, we automatically fix the previous_location config entry, if it only changed in the expected way, but still means the same location. --- borg/cache.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/borg/cache.py b/borg/cache.py index 43ef5c94..53fdae7c 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -10,7 +10,7 @@ from .key import PlaintextKey from .logger import create_logger logger = create_logger() from .helpers import Error, get_cache_dir, decode_dict, int_to_bigint, \ - bigint_to_int, format_file_size, yes, bin_to_hex + bigint_to_int, format_file_size, yes, bin_to_hex, Location from .locking import Lock from .hashindex import ChunkIndex @@ -140,10 +140,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" with open(os.path.join(self.path, 'files'), 'wb') as fd: pass # empty file - def _do_open(self): - self.config = configparser.ConfigParser(interpolation=None) - config_path = os.path.join(self.path, 'config') - self.config.read(config_path) + def _check_upgrade(self, config_path): try: cache_version = self.config.getint('cache', 'version') wanted_version = 1 @@ -154,6 +151,25 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" except configparser.NoSectionError: self.close() raise Exception('%s does not look like a Borg cache.' % config_path) from None + # borg < 1.0.8rc1 had different canonicalization for the repo location (see #1655 and #1741). + cache_loc = self.config.get('cache', 'previous_location', fallback=None) + if cache_loc: + repo_loc = self.repository._location.canonical_path() + rl = Location(repo_loc) + cl = Location(cache_loc) + if cl.proto == rl.proto and cl.user == rl.user and cl.host == rl.host and cl.port == rl.port \ + and \ + cl.path and rl.path and \ + cl.path.startswith('/~/') and rl.path.startswith('/./') and cl.path[3:] == rl.path[3:]: + # everything is same except the expected change in relative path canonicalization, + # update previous_location to avoid warning / user query about changed location: + self.config.set('cache', 'previous_location', repo_loc) + + def _do_open(self): + self.config = configparser.ConfigParser(interpolation=None) + config_path = os.path.join(self.path, 'config') + self.config.read(config_path) + self._check_upgrade(config_path) self.id = self.config.get('cache', 'repository') self.manifest_id = unhexlify(self.config.get('cache', 'manifest')) self.timestamp = self.config.get('cache', 'timestamp', fallback=None) From 2df8b740ddd00827ff07246e172d054a7a602a02 Mon Sep 17 00:00:00 2001 From: Andrew Skalski Date: Wed, 25 May 2016 18:14:27 -0400 Subject: [PATCH 0322/1387] RemoteRepository: Fix busy wait in call_many, fixes #940 (cherry picked from commit 731f6241faa254c379a9bc25dd21133d3604d32a) --- borg/remote.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/borg/remote.py b/borg/remote.py index e0480231..00c3114a 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -20,6 +20,8 @@ RPC_PROTOCOL_VERSION = 2 BUFSIZE = 10 * 1024 * 1024 +MAX_INFLIGHT = 100 + class ConnectionClosed(Error): """Connection closed by remote host""" @@ -316,7 +318,6 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. calls = list(calls) waiting_for = [] - w_fds = [self.stdin_fd] while wait or calls: while waiting_for: try: @@ -330,6 +331,10 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. return except KeyError: break + if self.to_send or ((calls or self.preload_ids) and len(waiting_for) < MAX_INFLIGHT): + w_fds = [self.stdin_fd] + else: + w_fds = [] r, w, x = select.select(self.r_fds, w_fds, self.x_fds, 1) if x: raise Exception('FD exception occurred') @@ -362,7 +367,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. else: sys.stderr.write("Remote: " + line) if w: - while not self.to_send and (calls or self.preload_ids) and len(waiting_for) < 100: + while not self.to_send and (calls or self.preload_ids) and len(waiting_for) < MAX_INFLIGHT: if calls: if is_preloaded: if calls[0] in self.cache: @@ -389,8 +394,6 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. # that the fd should be writable if e.errno != errno.EAGAIN: raise - if not self.to_send and not (calls or self.preload_ids): - w_fds = [] self.ignore_responses |= set(waiting_for) def check(self, repair=False, save_space=False): From 9b9179312d0c471746f8c0e21a0dbd5b5ddffe92 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 28 Oct 2016 03:32:37 +0200 Subject: [PATCH 0323/1387] ssh: repo url docs - fix typo --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f7105d6a..d2e803f0 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -249,7 +249,7 @@ is installed on the remote host, in which case the following syntax is used:: or:: - $ borg init ssh://user@hostname:port//path/to/repo + $ borg init ssh://user@hostname:port/path/to/repo Remote operations over SSH can be automated with SSH keys. You can restrict the use of the SSH keypair by prepending a forced command to the SSH public key in From ca15cc80e52a89dabdd7cf45b6ca609d039e7f3d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 28 Oct 2016 04:36:38 +0200 Subject: [PATCH 0324/1387] document repo URLs / archive location --- docs/usage.rst | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index d9fcc3a2..e72f3cb0 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -12,6 +12,76 @@ command in detail. General ------- +Repository URLs +~~~~~~~~~~~~~~~ + +**Local filesystem** (or locally mounted network filesystem): + +``/path/to/repo`` - filesystem path to repo directory, absolute path + +``path/to/repo`` - filesystem path to repo directory, relative path + +Also, stuff like ``~/path/to/repo`` or ``~other/path/to/repo`` works (this is +expanded by your shell). + + +**Remote repositories** accessed via ssh user@host: + +``user@host:/path/to/repo`` - remote repo, absolute path + +``ssh://user@host:port/path/to/repo`` - same, alternative syntax, port can be given + + +**Remote repositories with relative pathes** can be given using this syntax: + +``user@host:path/to/repo`` - path relative to current directory + +``user@host:~/path/to/repo`` - path relative to user's home directory + +``user@host:~other/path/to/repo`` - path relative to other's home directory + +Note: giving ``user@host:/./path/to/repo`` or ``user@host:/~/path/to/repo`` or +``user@host:/~other/path/to/repo``is also supported, but not required here. + + +**Remote repositories with relative pathes, alternative syntax with port**: + +``ssh://user@host:port/./path/to/repo`` - path relative to current directory + +``ssh://user@host:port/~/path/to/repo`` - path relative to user's home directory + +``ssh://user@host:port/~other/path/to/repo`` - path relative to other's home directory + + +If you frequently need the same repo URL, it is a good idea to set the +``BORG_REPO`` environment variable to set a default for the repo URL: + +:: + + export BORG_REPO='ssh://user@host:port/path/to/repo' + +Then just leave away the repo URL if only a repo URL is needed and you want +to use the default - it will be read from BORG_REPO then. + +Use ``::`` syntax to give the repo URL when syntax requires giving a positional +argument for the repo (e.g. ``borg mount :: /mnt``). + + +Repository / Archive Locations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Many commands want either a repository (just give the repo URL, see above) or +an archive location, which is a repo URL followed by ``::archive_name``. + +Archive names must not contain the ``/`` (slash) character. For simplicity, +maybe also avoid blanks or other characters that have special meaning on the +shell or in a filesystem (borg mount will use the archive name as directory +name). + +If you have set BORG_REPO (see above) and an archive location is needed, use +``::archive_name`` - the repo URL part is then read from BORG_REPO. + + Type of log output ~~~~~~~~~~~~~~~~~~ From e0298b293212a243c53486cb99f0c430225dc6d2 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 28 Oct 2016 04:51:46 +0200 Subject: [PATCH 0325/1387] simplify quickstart only give one possible ssh url syntax, all others are documented in usage chapter. --- docs/quickstart.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index d2e803f0..75d14bca 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -247,9 +247,7 @@ is installed on the remote host, in which case the following syntax is used:: $ borg init user@hostname:/path/to/repo -or:: - - $ borg init ssh://user@hostname:port/path/to/repo +Note: please see the usage chapter for a full documentation of repo URLs. Remote operations over SSH can be automated with SSH keys. You can restrict the use of the SSH keypair by prepending a forced command to the SSH public key in From 11e97803938a6766b298238790f81de4480840c2 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 28 Oct 2016 04:57:15 +0200 Subject: [PATCH 0326/1387] quickstart: add a comment about other (remote) filesystems --- docs/quickstart.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 75d14bca..26387aec 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -263,3 +263,7 @@ mounting the remote filesystem, for example, using sshfs:: $ sshfs user@hostname:/path/to /path/to $ borg init /path/to/repo $ fusermount -u /path/to + +You can also use other remote filesystems in a similar way. Just be careful, +not all filesystems out there are really stable and working good enough to +be acceptable for backup usage. From a16c7d8e16c472d05034d2c5350283f95d86db19 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 28 Oct 2016 05:04:23 +0200 Subject: [PATCH 0327/1387] mention file:// --- docs/usage.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/usage.rst b/docs/usage.rst index e72f3cb0..8743f6ac 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -24,6 +24,7 @@ Repository URLs Also, stuff like ``~/path/to/repo`` or ``~other/path/to/repo`` works (this is expanded by your shell). +Note: you may also prepend a ``file://`` to a filesystem path to get URL style. **Remote repositories** accessed via ssh user@host: From 20f4a1f4785920a394374401d5ee5b612805b845 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 25 Oct 2016 21:23:23 +0200 Subject: [PATCH 0328/1387] update CHANGES (1.0-maint) --- docs/changes.rst | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 8f8dc9a8..70c1d372 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,6 +50,48 @@ The best check that everything is ok is to run a dry-run extraction:: borg extract -v --dry-run REPO::ARCHIVE +Version 1.0.8 (2016-10-29) +-------------------------- + +Bug fixes: + +- RemoteRepository: Fix busy wait in call_many, #940 + +New features: + +- implement borgmajor/borgminor/borgpatch placeholders, #1694 + {borgversion} was already there (full version string). With the new + placeholders you can now also get e.g. 1 or 1.0 or 1.0.8. + +Other changes: + +- avoid previous_location mismatch, #1741 + + due to the changed canonicalization for relative pathes in PR #1711 / #1655 + (implement /./ relpath hack), there would be a changed repo location warning + and the user would be asked if this is ok. this would break automation and + require manual intervention, which is unwanted. + + thus, we automatically fix the previous_location config entry, if it only + changed in the expected way, but still means the same location. + +- docs: + + - deployment.rst: do not use bare variables in ansible snippet + - add clarification about append-only mode, #1689 + - setup.py: add comment about requiring llfuse, #1726 + - update usage.rst / api.rst + - repo url / archive location docs + typo fix + - quickstart: add a comment about other (remote) filesystems + +- vagrant / tests: + + - no chown when rsyncing (fixes boxes w/o vagrant group) + - fix fuse permission issues on linux/freebsd, #1544 + - skip fuse test for borg binary + fakeroot + - ignore security.selinux xattrs, fixes tests on centos, #1735 + + Version 1.0.8rc1 (2016-10-17) ----------------------------- @@ -72,8 +114,8 @@ Bug fixes: (this seems not to get triggered in 1.0.x, but was discovered in master) - hashindex: fix iterators (always raise StopIteration when exhausted) (this seems not to get triggered in 1.0.x, but was discovered in master) -- enable relative pathes in ssh:// repo URLs, via /./relpath hack, fixes #1655 -- allow repo pathes with colons, fixes #1705 +- enable relative pathes in ssh:// repo URLs, via /./relpath hack, #1655 +- allow repo pathes with colons, #1705 - update changed repo location immediately after acceptance, #1524 - fix debug get-obj / delete-obj crash if object not found and remote repo, #1684 @@ -105,7 +147,7 @@ Other changes: appears not only in the traceback, but also in the (short) error message, #1572 - borg.key: include chunk id in exception msgs, #1571 - - better messages for cache newer than repo, fixes #1700 + - better messages for cache newer than repo, #1700 - vagrant (testing/build VMs): - upgrade OSXfuse / FUSE for macOS to 3.5.2 From 84637322f29ec7a54e16440fe92ab76bfe113510 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 29 Oct 2016 01:58:21 +0200 Subject: [PATCH 0329/1387] ran build_usage --- docs/usage/help.rst.inc | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index b079dd2d..b1025fb5 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -128,7 +128,19 @@ borg help placeholders {borgversion} - The version of borg. + The version of borg, e.g.: 1.0.8rc1 + + {borgmajor} + + The version of borg, only the major version, e.g.: 1 + + {borgminor} + + The version of borg, only major and minor version, e.g.: 1.0 + + {borgpatch} + + The version of borg, only major, minor and patch version, e.g.: 1.0.8 Examples:: From aa62a164d9e918d7cc54c47725e78571e4fb288e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 31 Oct 2016 02:36:49 +0100 Subject: [PATCH 0330/1387] vagrant: do not try to install llfuse on centos6 it breaks everything... --- Vagrantfile | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index cf440fb5..229a9391 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -40,8 +40,8 @@ def packages_debianoid EOF end -def packages_redhatted - return <<-EOF +def packages_redhatted(version) + script = <<-EOF yum install -y epel-release yum update -y # for building borgbackup and dependencies: @@ -58,6 +58,13 @@ def packages_redhatted #pip install virtualenv touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile EOF + if version == "centos6" + script += <<-EOF + # avoid that breaking llfuse install breaks borgbackup install under tox: + sed -i.bak '/fuse.txt/d' /vagrant/borg/borg/tox.ini + EOF + end + return script end def packages_darwin @@ -325,7 +332,7 @@ Vagrant.configure(2) do |config| b.vm.provider :virtualbox do |v| v.memory = 768 end - b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted + b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted("centos7") b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos7_64") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("centos7_64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("centos7_64") @@ -335,7 +342,7 @@ Vagrant.configure(2) do |config| config.vm.define "centos6_32" do |b| b.vm.box = "centos6-32" - b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted + b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted("centos6") b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos6_32") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("centos6_32") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("centos6_32") @@ -348,7 +355,7 @@ Vagrant.configure(2) do |config| b.vm.provider :virtualbox do |v| v.memory = 768 end - b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted + b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted("centos6") b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos6_64") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("centos6_64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("centos6_64") From f2ed60b80bf3cf8341e1d6a2e6515152c7862538 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 31 Oct 2016 05:02:19 +0100 Subject: [PATCH 0331/1387] vagrant: fix fuse test for darwin, fixes #1546 otherwise the "input" dir is root.wheel -rwx------ and gives a PermissionError when trying to access it. --- borg/testsuite/archiver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 3cd317b7..4a39d64a 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -211,6 +211,7 @@ class ArchiverTestCaseBase(BaseTestCase): os.environ['BORG_KEYS_DIR'] = self.keys_path os.environ['BORG_CACHE_DIR'] = self.cache_path os.mkdir(self.input_path) + os.chmod(self.input_path, 0o777) # avoid troubles with fakeroot / FUSE os.mkdir(self.output_path) os.mkdir(self.keys_path) os.mkdir(self.cache_path) From 5b0bff196233a5cac0c8d4970016d9ea29275502 Mon Sep 17 00:00:00 2001 From: ololoru Date: Mon, 24 Oct 2016 21:17:58 -0500 Subject: [PATCH 0332/1387] add windows virtual machine * Default memory size of Windows VM is 2048 mb * Stop using mls-openssh and replace it with cygwin sshd by updating service start path in registry --- Vagrantfile | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/Vagrantfile b/Vagrantfile index cf440fb5..e9992089 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -155,6 +155,57 @@ def packages_netbsd EOF end +# Install required cygwin packages and configure environment +# +# Microsoft/EdgeOnWindows10 image has MLS-OpenSSH installed by default, +# which is based on cygwin x86_64 but should not be used together with cygwin. +# In order to have have cygwin compatible bash 'ImagePath' is replaced with +# cygrunsrv of newly installed cygwin +# +# supported cygwin versions: +# x86_64 +# x86 +def packages_cygwin(version) + setup_exe = "setup-#{version}.exe" + + return <<-EOF + mkdir -p /cygdrive/c/cygwin + powershell -Command '$client = new-object System.Net.WebClient; $client.DownloadFile("https://www.cygwin.com/#{setup_exe}","C:\\cygwin\\#{setup_exe}")' + echo ' + REM --- Change to use different CygWin platform and final install path + set CYGSETUP=#{setup_exe} + REM --- Install build version of CygWin in a subfolder + set OURPATH=%cd% + set CYGBUILD="C:\\cygwin\\CygWin" + set CYGMIRROR=ftp://mirrors.kernel.org/sourceware/cygwin/ + set BUILDPKGS=python3,python3-setuptools,binutils,gcc-g++,libopenssl,openssl-devel,git,make,openssh,liblz4-devel,liblz4_1,rsync,curl,python-devel + %CYGSETUP% -q -B -o -n -R %CYGBUILD% -L -D -s %CYGMIRROR% -P %BUILDPKGS% + cd /d C:\\cygwin\\CygWin\\bin + regtool set /HKLM/SYSTEM/CurrentControlSet/Services/OpenSSHd/ImagePath "C:\\cygwin\\CygWin\\bin\\cygrunsrv.exe" + bash -c "ssh-host-config --no" + ' > /cygdrive/c/cygwin/install.bat + cd /cygdrive/c/cygwin && cmd.exe /c install.bat + + echo "alias mkdir='mkdir -p'" > ~/.profile + echo "export CYGWIN_ROOT=/cygdrive/c/cygwin/CygWin" >> ~/.profile + echo 'export PATH=$PATH:$CYGWIN_ROOT/bin' >> ~/.profile + + echo '' > ~/.bash_profile + + cmd.exe /c 'setx /m PATH "%PATH%;C:\\cygwin\\CygWin\\bin"' + source ~/.profile + echo 'db_home: windows' > $CYGWIN_ROOT/etc/nsswitch.conf + EOF +end + +def install_cygwin_venv + return <<-EOF + easy_install-3.4 pip + pip install virtualenv + EOF +end + + def install_pyenv(boxname) return <<-EOF curl -s -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash @@ -466,4 +517,31 @@ Vagrant.configure(2) do |config| b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg_no_fuse("netbsd64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("netbsd64") end + + config.vm.define "windows10" do |b| + b.vm.box = "Microsoft/EdgeOnWindows10" + b.vm.guest = :windows + b.vm.boot_timeout = 180 + b.vm.graceful_halt_timeout = 120 + + b.ssh.shell = "sh -l" + b.ssh.username = "IEUser" + b.ssh.password = "Passw0rd!" + b.ssh.insert_key = false + + b.vm.provider :virtualbox do |v| + v.memory = 2048 + #v.gui = true + end + + # fix permissions placeholder + b.vm.provision "fix perms", :type => :shell, :privileged => false, :inline => "echo 'fix permission placeholder'" + + b.vm.provision "packages cygwin", :type => :shell, :privileged => false, :inline => packages_cygwin("x86_64") + b.vm.provision :reload + b.vm.provision "cygwin install pip", :type => :shell, :privileged => false, :inline => install_cygwin_venv + b.vm.provision "cygwin build env", :type => :shell, :privileged => false, :inline => build_sys_venv("windows10") + b.vm.provision "cygwin install borg", :type => :shell, :privileged => false, :inline => install_borg_no_fuse("windows10") + b.vm.provision "cygwin run tests", :type => :shell, :privileged => false, :inline => run_tests("windows10") + end end From 1b462cdbed5493bdb1e746e318b7f1157866f8cc Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 2 Nov 2016 03:01:49 +0100 Subject: [PATCH 0333/1387] vagrant: remove llfuse from tox.ini at a central place --- Vagrantfile | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index f8e23bb7..646669af 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -40,8 +40,8 @@ def packages_debianoid EOF end -def packages_redhatted(version) - script = <<-EOF +def packages_redhatted + return <<-EOF yum install -y epel-release yum update -y # for building borgbackup and dependencies: @@ -58,13 +58,6 @@ def packages_redhatted(version) #pip install virtualenv touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile EOF - if version == "centos6" - script += <<-EOF - # avoid that breaking llfuse install breaks borgbackup install under tox: - sed -i.bak '/fuse.txt/d' /vagrant/borg/borg/tox.ini - EOF - end - return script end def packages_darwin @@ -130,8 +123,6 @@ def packages_openbsd easy_install-3.4 pip pip3 install virtualenv touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile - # avoid that breaking llfuse install breaks borgbackup install under tox: - sed -i.bak '/fuse.txt/d' /vagrant/borg/borg/tox.ini EOF end @@ -157,8 +148,6 @@ def packages_netbsd easy_install-3.4 pip pip install virtualenv touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile - # fuse does not work good enough (see above), do not install llfuse: - sed -i.bak '/fuse.txt/d' /vagrant/borg/borg/tox.ini EOF end @@ -289,6 +278,8 @@ def install_borg_no_fuse(boxname) rm -rf borg/__pycache__ borg/support/__pycache__ borg/testsuite/__pycache__ pip install -r requirements.d/development.txt pip install -e . + # do not install llfuse into the virtualenvs built by tox: + sed -i.bak '/fuse.txt/d' tox.ini EOF end @@ -383,7 +374,7 @@ Vagrant.configure(2) do |config| b.vm.provider :virtualbox do |v| v.memory = 768 end - b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted("centos7") + b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos7_64") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("centos7_64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("centos7_64") @@ -393,7 +384,7 @@ Vagrant.configure(2) do |config| config.vm.define "centos6_32" do |b| b.vm.box = "centos6-32" - b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted("centos6") + b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos6_32") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("centos6_32") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("centos6_32") @@ -406,7 +397,7 @@ Vagrant.configure(2) do |config| b.vm.provider :virtualbox do |v| v.memory = 768 end - b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted("centos6") + b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos6_64") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("centos6_64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("centos6_64") From df7191e55c6e09a8bdd194a5c3688114d549ea2c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 2 Nov 2016 04:09:16 +0100 Subject: [PATCH 0334/1387] skip remote tests on cygwin remote is broken and hangs infinitely on cygwin. https://github.com/borgbackup/borg/issues/1268 --- borg/testsuite/archiver.py | 2 +- borg/testsuite/repository.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 4a39d64a..b0fa1a11 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1444,7 +1444,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.cmd('check', self.repository_location, exit_code=0) self.cmd('extract', '--dry-run', self.repository_location + '::archive1', exit_code=0) - +@pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteArchiverTestCase(ArchiverTestCase): prefix = '__testsuite__:' diff --git a/borg/testsuite/repository.py b/borg/testsuite/repository.py index b95093e4..e217137a 100644 --- a/borg/testsuite/repository.py +++ b/borg/testsuite/repository.py @@ -4,6 +4,8 @@ import sys import tempfile from unittest.mock import patch +import pytest + from ..hashindex import NSIndex from ..helpers import Location, IntegrityError from ..locking import Lock, LockFailed @@ -413,6 +415,7 @@ class RepositoryCheckTestCase(RepositoryTestCaseBase): self.assert_equal(self.repository.get(H(0)), b'data2') +@pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteRepositoryTestCase(RepositoryTestCase): def open(self, create=False): @@ -443,6 +446,7 @@ class RemoteRepositoryTestCase(RepositoryTestCase): assert self.repository.borg_cmd(args, testing=False) == ['borg-0.28.2', 'serve', '--umask=077', '--info'] +@pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteRepositoryCheckTestCase(RepositoryCheckTestCase): def open(self, create=False): From c8c0495724dd43460d29d20759b65efa4b303acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Thu, 3 Nov 2016 14:15:06 -0400 Subject: [PATCH 0335/1387] faster quickstart move the note about free space after the step by step example. it is unlikely that users will hit out of space conditions on their first run, and at the end of the example, they will see the not anyways. this is to make the documentation less scary for new users and easier to use. --- docs/quickstart.rst | 88 ++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 3e4e66cb..8a10ac9b 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -5,51 +5,8 @@ Quick Start =========== -This chapter will get you started with |project_name|. The first section -presents a simple step by step example that uses |project_name| to backup data. -The next section continues by showing how backups can be automated. - -Important note about free space -------------------------------- - -Before you start creating backups, please make sure that there is *always* -a good amount of free space on the filesystem that has your backup repository -(and also on ~/.cache). A few GB should suffice for most hard-drive sized -repositories. See also :ref:`cache-memory-usage`. - -Borg doesn't use space reserved for root on repository disks (even when run as root), -on file systems which do not support this mechanism (e.g. XFS) we recommend to -reserve some space in Borg itself just to be safe by adjusting the -``additional_free_space`` setting in the ``[repository]`` section of a repositories -``config`` file. A good starting point is ``2G``. - -If |project_name| runs out of disk space, it tries to free as much space as it -can while aborting the current operation safely, which allows to free more space -by deleting/pruning archives. This mechanism is not bullet-proof in some -circumstances [1]_. - -If you *really* run out of disk space, it can be hard or impossible to free space, -because |project_name| needs free space to operate - even to delete backup -archives. - -You can use some monitoring process or just include the free space information -in your backup log files (you check them regularly anyway, right?). - -Also helpful: - -- create a big file as a "space reserve", that you can delete to free space -- if you use LVM: use a LV + a filesystem that you can resize later and have - some unallocated PEs you can add to the LV. -- consider using quotas -- use `prune` regularly - -.. [1] This failsafe can fail in these circumstances: - - - The underlying file system doesn't support statvfs(2), or returns incorrect - data, or the repository doesn't reside on a single file system - - Other tasks fill the disk simultaneously - - Hard quotas (which may not be reflected in statvfs(2)) - +This chapter will get you started with |project_name| and covers +various use cases. A step by step example ---------------------- @@ -117,6 +74,47 @@ A step by step example ``--verbose`` or ``--info``) option to adjust the log level to INFO to get other informational messages. +Important note about free space +------------------------------- + +Before you start creating backups, please make sure that there is *always* +a good amount of free space on the filesystem that has your backup repository +(and also on ~/.cache). A few GB should suffice for most hard-drive sized +repositories. See also :ref:`cache-memory-usage`. + +Borg doesn't use space reserved for root on repository disks (even when run as root), +on file systems which do not support this mechanism (e.g. XFS) we recommend to +reserve some space in Borg itself just to be safe by adjusting the +``additional_free_space`` setting in the ``[repository]`` section of a repositories +``config`` file. A good starting point is ``2G``. + +If |project_name| runs out of disk space, it tries to free as much space as it +can while aborting the current operation safely, which allows to free more space +by deleting/pruning archives. This mechanism is not bullet-proof in some +circumstances [1]_. + +If you *really* run out of disk space, it can be hard or impossible to free space, +because |project_name| needs free space to operate - even to delete backup +archives. + +You can use some monitoring process or just include the free space information +in your backup log files (you check them regularly anyway, right?). + +Also helpful: + +- create a big file as a "space reserve", that you can delete to free space +- if you use LVM: use a LV + a filesystem that you can resize later and have + some unallocated PEs you can add to the LV. +- consider using quotas +- use `prune` regularly + +.. [1] This failsafe can fail in these circumstances: + + - The underlying file system doesn't support statvfs(2), or returns incorrect + data, or the repository doesn't reside on a single file system + - Other tasks fill the disk simultaneously + - Hard quotas (which may not be reflected in statvfs(2)) + Automating backups ------------------ From 7f9a147e46a3d2923b9b4cc2e0d786a2a43f5df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Thu, 3 Nov 2016 14:03:48 -0400 Subject: [PATCH 0336/1387] move fork differences to FAQ it seems now that the fork is more of historical value than a current thing. people interested in the differences between borg and attic can look in the FAQ, but I do not see why this is present in the README. a new section regarding compatibility is created to keep that warning in place. --- README.rst | 39 ++++----------------------------------- docs/faq.rst | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/README.rst b/README.rst index 57af3957..e9a59f4e 100644 --- a/README.rst +++ b/README.rst @@ -78,8 +78,8 @@ Main features **Free and Open Source Software** * security and functionality can be audited independently - * licensed under the BSD (3-clause) license - + * licensed under the BSD (3-clause) license, see `License`_ for the + complete license Easy to use ----------- @@ -143,37 +143,8 @@ Links `Mailing List `_ * `License `_ -Notes ------ - -Borg is a fork of `Attic`_ and maintained by "`The Borg collective`_". - -.. _Attic: https://github.com/jborg/attic -.. _The Borg collective: https://borgbackup.readthedocs.org/en/latest/authors.html - -Differences between Attic and Borg -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Here's a (incomplete) list of some major changes: - -* more open, faster paced development (see `issue #1 `_) -* lots of attic issues fixed (see `issue #5 `_) -* less chunk management overhead (less memory and disk usage for chunks index) -* faster remote cache resync (useful when backing up multiple machines into same repo) -* compression: no, lz4, zlib or lzma compression, adjustable compression levels -* repokey replaces problematic passphrase mode (you can't change the passphrase nor the pbkdf2 iteration count in "passphrase" mode) -* simple sparse file support, great for virtual machine disk files -* can read special files (e.g. block devices) or from stdin, write to stdout -* mkdir-based locking is more compatible than attic's posix locking -* uses fadvise to not spoil / blow up the fs cache -* better error messages / exception handling -* better logging, screen output, progress indication -* tested on misc. Linux systems, 32 and 64bit, FreeBSD, OpenBSD, NetBSD, Mac OS X - -Please read the `ChangeLog`_ (or ``docs/changes.rst`` in the source distribution) for more -information. - -BORG IS NOT COMPATIBLE WITH ORIGINAL ATTIC (but there is a one-way conversion). +Compatibility notes +=================== EXPECT THAT WE WILL BREAK COMPATIBILITY REPEATEDLY WHEN MAJOR RELEASE NUMBER CHANGES (like when going from 0.x.y to 1.0.0 or from 1.x.y to 2.0.0). @@ -182,8 +153,6 @@ NOT RELEASED DEVELOPMENT VERSIONS HAVE UNKNOWN COMPATIBILITY PROPERTIES. THIS IS SOFTWARE IN DEVELOPMENT, DECIDE YOURSELF WHETHER IT FITS YOUR NEEDS. -Borg is distributed under a 3-clause BSD license, see `License`_ for the complete license. - |doc| |build| |coverage| |bestpractices| .. |doc| image:: https://readthedocs.org/projects/borgbackup/badge/?version=stable diff --git a/docs/faq.rst b/docs/faq.rst index 47d0ce1a..3622b3cf 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -512,3 +512,32 @@ Borg intends to be: * do not break compatibility accidentally, without a good reason or without warning. allow compatibility breaking for other cases. * if major version number changes, it may have incompatible changes + +What are the differences between Attic and Borg? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Borg is a fork of `Attic`_ and maintained by "`The Borg collective`_". + +.. _Attic: https://github.com/jborg/attic +.. _The Borg collective: https://borgbackup.readthedocs.org/en/latest/authors.html + +Here's a (incomplete) list of some major changes: + +* more open, faster paced development (see `issue #1 `_) +* lots of attic issues fixed (see `issue #5 `_) +* less chunk management overhead (less memory and disk usage for chunks index) +* faster remote cache resync (useful when backing up multiple machines into same repo) +* compression: no, lz4, zlib or lzma compression, adjustable compression levels +* repokey replaces problematic passphrase mode (you can't change the passphrase nor the pbkdf2 iteration count in "passphrase" mode) +* simple sparse file support, great for virtual machine disk files +* can read special files (e.g. block devices) or from stdin, write to stdout +* mkdir-based locking is more compatible than attic's posix locking +* uses fadvise to not spoil / blow up the fs cache +* better error messages / exception handling +* better logging, screen output, progress indication +* tested on misc. Linux systems, 32 and 64bit, FreeBSD, OpenBSD, NetBSD, Mac OS X + +Please read the `ChangeLog`_ (or ``docs/changes.rst`` in the source distribution) for more +information. + +Borg is not compatible with original attic (but there is a one-way conversion). From 18f3d64e4ca3b775e47083a3b7065f7308f8c877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Thu, 3 Nov 2016 12:38:31 -0400 Subject: [PATCH 0337/1387] PDF docs: add logo and fix authors the PDF documentation looks much better with those --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 26d405d1..8087b430 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -199,12 +199,12 @@ htmlhelp_basename = 'borgdoc' # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Borg.tex', 'Borg Documentation', - 'see "AUTHORS" file', 'manual'), + 'The Borg Collective', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +latex_logo = '_static/logo.png' # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. From 5def2350d06b1bdcb5d5ec5e3880bdbaa5bbbc78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Thu, 3 Nov 2016 12:54:07 -0500 Subject: [PATCH 0338/1387] fix PDF rendering structure without those changes, all of the toctree document headings do not show up. they are considered to be "below" the last heading of the README file. we also remove the "Notes" section from the readme as there is only one note, regarding the fork. we introduce a stub "introduction" element in the toctree, otherwise it is impossible for the PDF rendered to render the README correctly. this is to workaround a bug in the PDF renderer. --- README.rst | 12 ++++++------ docs/book.rst | 22 ++++++++++++++++++++++ docs/conf.py | 2 +- docs/index.rst | 2 ++ docs/introduction.rst | 8 ++++++++ 5 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 docs/book.rst create mode 100644 docs/introduction.rst diff --git a/README.rst b/README.rst index e9a59f4e..5044bc4e 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ .. highlight:: bash What is BorgBackup? -=================== +------------------- BorgBackup (short: Borg) is a deduplicating backup program. Optionally, it supports compression and authenticated encryption. @@ -20,7 +20,7 @@ downloaded Borg, ``docs/installation.rst`` to get started with Borg. .. _installation manual: https://borgbackup.readthedocs.org/en/stable/installation.html Main features -------------- +~~~~~~~~~~~~~ **Space efficient storage** Deduplication based on content-defined chunking is used to reduce the number @@ -82,7 +82,7 @@ Main features complete license Easy to use ------------ +~~~~~~~~~~~ Initialize a new backup repository and create a backup archive:: @@ -114,7 +114,7 @@ Now doing another backup, just to show off the great deduplication: For a graphical frontend refer to our complementary project `BorgWeb `_. Checking Release Authenticity and Security Contact -================================================== +-------------------------------------------------- `Releases `_ are signed with this GPG key, please use GPG to verify their authenticity. @@ -130,7 +130,7 @@ The public key can be fetched from any GPG keyserver, but be careful: you must use the **full fingerprint** to check that you got the correct key. Links -===== +----- * `Main Web Site `_ * `Releases `_, @@ -144,7 +144,7 @@ Links * `License `_ Compatibility notes -=================== +------------------- EXPECT THAT WE WILL BREAK COMPATIBILITY REPEATEDLY WHEN MAJOR RELEASE NUMBER CHANGES (like when going from 0.x.y to 1.0.0 or from 1.x.y to 2.0.0). diff --git a/docs/book.rst b/docs/book.rst new file mode 100644 index 00000000..33e75683 --- /dev/null +++ b/docs/book.rst @@ -0,0 +1,22 @@ +.. include:: global.rst.inc + +Borg documentation +================== + +.. when you add an element here, do not forget to add it to index.rst + +.. toctree:: + :maxdepth: 3 + + introduction + installation + quickstart + usage + deployment + faq + support + resources + changes + internals + development + authors diff --git a/docs/conf.py b/docs/conf.py index 8087b430..cd27d3fd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -198,7 +198,7 @@ htmlhelp_basename = 'borgdoc' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'Borg.tex', 'Borg Documentation', + ('book', 'Borg.tex', 'Borg Documentation', 'The Borg Collective', 'manual'), ] diff --git a/docs/index.rst b/docs/index.rst index 89a907de..67e7a766 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,6 +6,8 @@ Borg Documentation .. include:: ../README.rst +.. when you add an element here, do not forget to add it to book.rst + .. toctree:: :maxdepth: 2 diff --git a/docs/introduction.rst b/docs/introduction.rst new file mode 100644 index 00000000..ab8bd32c --- /dev/null +++ b/docs/introduction.rst @@ -0,0 +1,8 @@ +Introduction +============ + +.. this shim is here to fix the structure in the PDF + rendering. without this stub, the elements in the toctree of + index.rst show up a level below the README file included + +.. include:: ../README.rst From aeb10d1a85649fcb099898ede07ffb2a63b2de39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Thu, 3 Nov 2016 13:57:49 -0400 Subject: [PATCH 0339/1387] show URLs in PDF, better font --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index cd27d3fd..8163bc7e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -193,7 +193,7 @@ htmlhelp_basename = 'borgdoc' #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +latex_font_size = '12pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). @@ -214,7 +214,7 @@ latex_logo = '_static/logo.png' #latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +latex_show_urls = 'footnote' # Additional stuff for the LaTeX preamble. #latex_preamble = '' From b4d038878518b7cba3221a87a1110604834d497c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Thu, 3 Nov 2016 14:10:38 -0400 Subject: [PATCH 0340/1387] move security verification to support section the rationale is to simplify the README file to the bare minimum. security researchers will be able to find the contact information if they look minimally and people installing the software will find a link where relevant (in binary releases only, since all the others have other trust paths) --- README.rst | 16 ---------------- docs/installation.rst | 3 +++ docs/support.rst | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index 5044bc4e..af05ff04 100644 --- a/README.rst +++ b/README.rst @@ -113,22 +113,6 @@ Now doing another backup, just to show off the great deduplication: For a graphical frontend refer to our complementary project `BorgWeb `_. -Checking Release Authenticity and Security Contact --------------------------------------------------- - -`Releases `_ are signed with this GPG key, -please use GPG to verify their authenticity. - -In case you discover a security issue, please use this contact for reporting it privately -and please, if possible, use encrypted E-Mail: - -Thomas Waldmann - -GPG Key Fingerprint: 6D5B EF9A DD20 7580 5747 B70F 9F88 FB52 FAF7 B393 - -The public key can be fetched from any GPG keyserver, but be careful: you must -use the **full fingerprint** to check that you got the correct key. - Links ----- diff --git a/docs/installation.rst b/docs/installation.rst index 523f43cd..ff5cf7d1 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -64,6 +64,9 @@ and compare that to our latest release and review the :doc:`changes`. Standalone Binary ----------------- +.. note:: Releases are signed with an OpenPGP key, see + :ref:`security-contact` for more instructions. + |project_name| binaries (generated with `pyinstaller`_) are available on the releases_ page for the following platforms: diff --git a/docs/support.rst b/docs/support.rst index 9d64621f..5ee34de9 100644 --- a/docs/support.rst +++ b/docs/support.rst @@ -56,3 +56,21 @@ As a developer, you can become a Bounty Hunter and win bounties (earn money) by contributing to |project_name|, a free and open source software project. We might also use BountySource to fund raise for some bigger goals. + +.. _security-contact: + +Security +-------- + +In case you discover a security issue, please use this contact for reporting it privately +and please, if possible, use encrypted E-Mail: + +Thomas Waldmann + +GPG Key Fingerprint: 6D5B EF9A DD20 7580 5747 B70F 9F88 FB52 FAF7 B393 + +The public key can be fetched from any GPG keyserver, but be careful: you must +use the **full fingerprint** to check that you got the correct key. + +`Releases `_ are signed with this GPG key, +please use GPG to verify their authenticity. From 19ae2a78701091aaa9df048f9f589ffd234c8653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Fri, 4 Nov 2016 10:28:53 -0400 Subject: [PATCH 0341/1387] add FAQ about security --- docs/faq.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index 3622b3cf..49b837a1 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -203,6 +203,13 @@ Thus: - have media at another place - have a relatively recent backup on your media +How do I report security issue with |project_name|? +--------------------------------------------------- + +Send a private email to the :ref:`security-contact` if you think you +have discovered a security issue. Please disclose security issues +responsibly. + Why do I get "connection closed by remote" after a while? --------------------------------------------------------- From 0cda9d6bd31f767cdb9026ccd585bc979b29548e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Fri, 4 Nov 2016 10:31:27 -0400 Subject: [PATCH 0342/1387] add link to security contact in README --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index af05ff04..e7f892c7 100644 --- a/README.rst +++ b/README.rst @@ -137,6 +137,9 @@ NOT RELEASED DEVELOPMENT VERSIONS HAVE UNKNOWN COMPATIBILITY PROPERTIES. THIS IS SOFTWARE IN DEVELOPMENT, DECIDE YOURSELF WHETHER IT FITS YOUR NEEDS. +Security issues should be reported to the :ref:`security-contact` (or +see ``docs/suppport.rst`` in the source distribution). + |doc| |build| |coverage| |bestpractices| .. |doc| image:: https://readthedocs.org/projects/borgbackup/badge/?version=stable From 319ecd81bb74788d135c89e844409598f05a270c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Fri, 4 Nov 2016 11:26:02 -0400 Subject: [PATCH 0343/1387] fix links in standalone README github and standalone docutils don't parse :ref: tags correctly --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e7f892c7..2c407c1d 100644 --- a/README.rst +++ b/README.rst @@ -126,6 +126,7 @@ Links * `Web-Chat (IRC) `_ and `Mailing List `_ * `License `_ +* `Security contact `_ Compatibility notes ------------------- @@ -137,7 +138,7 @@ NOT RELEASED DEVELOPMENT VERSIONS HAVE UNKNOWN COMPATIBILITY PROPERTIES. THIS IS SOFTWARE IN DEVELOPMENT, DECIDE YOURSELF WHETHER IT FITS YOUR NEEDS. -Security issues should be reported to the :ref:`security-contact` (or +Security issues should be reported to the `Security contact`_ (or see ``docs/suppport.rst`` in the source distribution). |doc| |build| |coverage| |bestpractices| From af23eea39dc3e5e97c6262dbccad6e863171d03f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Fri, 4 Nov 2016 20:14:59 -0500 Subject: [PATCH 0344/1387] fix levels in authors section that way in the TOC, it doesn't look like borg authors are attic --- AUTHORS | 4 ++-- docs/authors.rst | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 4788133e..9749b1c5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,5 +1,5 @@ -Borg Contributors ("The Borg Collective") -========================================= +Borg authors ("The Borg Collective") +------------------------------------ - Thomas Waldmann - Antoine Beaupré diff --git a/docs/authors.rst b/docs/authors.rst index c368035d..bca9b5de 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -1,5 +1,8 @@ .. include:: global.rst.inc +Authors +======= + .. include:: ../AUTHORS License From ec0ef2eaca7b41d71932ac9d54c069fddc5267bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Fri, 4 Nov 2016 21:15:31 -0400 Subject: [PATCH 0345/1387] remove third level headings, too verbose --- docs/book.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book.rst b/docs/book.rst index 33e75683..679a0522 100644 --- a/docs/book.rst +++ b/docs/book.rst @@ -6,7 +6,7 @@ Borg documentation .. when you add an element here, do not forget to add it to index.rst .. toctree:: - :maxdepth: 3 + :maxdepth: 2 introduction installation From 057ad07d4b5967f82af6ffcac6a45e002cd34093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Fri, 4 Nov 2016 21:16:41 -0400 Subject: [PATCH 0346/1387] fix a heading that was incorrectly set --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index 49b837a1..a7fc74b1 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -521,7 +521,7 @@ Borg intends to be: * if major version number changes, it may have incompatible changes What are the differences between Attic and Borg? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------ Borg is a fork of `Attic`_ and maintained by "`The Borg collective`_". From adfd66d636e56cf21a7027e4169fcaf2d48b0197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Fri, 4 Nov 2016 21:27:21 -0400 Subject: [PATCH 0347/1387] restore normal size otherwise code examples overflow --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 8163bc7e..94e088d5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -193,7 +193,7 @@ htmlhelp_basename = 'borgdoc' #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -latex_font_size = '12pt' +#latex_font_size = '12pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). From d490292be32a44bd5a5595d8f12509cff0d7217b Mon Sep 17 00:00:00 2001 From: Oleg Drokin Date: Sat, 2 Jul 2016 21:14:47 -0400 Subject: [PATCH 0348/1387] Detect and delete stale locks when it's safe If BORG_UNIQUE_HOSTNAME shell variable is set, stale locks in both cache and repository are deleted. Stale lock is defined as a lock that's originating from the same hostname as us, and correspond to a pid that no longer exists. This fixes #562 --- src/borg/cache.py | 3 +- src/borg/locking.py | 102 ++++++++++++++++++++++++++++++++++++++--- src/borg/repository.py | 3 +- 3 files changed, 100 insertions(+), 8 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 99d5bf23..8a7ad4aa 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -75,6 +75,7 @@ class Cache: self.key = key self.manifest = manifest self.path = path or os.path.join(get_cache_dir(), repository.id_str) + self.unique_hostname = bool(os.environ.get('BORG_UNIQUE_HOSTNAME')) self.do_files = do_files # Warn user before sending data to a never seen before unencrypted repository if not os.path.exists(self.path): @@ -202,7 +203,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def open(self, lock_wait=None): if not os.path.isdir(self.path): raise Exception('%s Does not look like a Borg cache' % self.path) - self.lock = Lock(os.path.join(self.path, 'lock'), exclusive=True, timeout=lock_wait).acquire() + self.lock = Lock(os.path.join(self.path, 'lock'), exclusive=True, timeout=lock_wait, kill_stale_locks=self.unique_hostname).acquire() self.rollback() def close(self): diff --git a/src/borg/locking.py b/src/borg/locking.py index 5405c231..56f28092 100644 --- a/src/borg/locking.py +++ b/src/borg/locking.py @@ -1,6 +1,8 @@ +import errno import json import os import socket +import sys import time from .helpers import Error, ErrorWithTraceback @@ -17,10 +19,36 @@ _hostname = socket.gethostname() def get_id(): """Get identification tuple for 'us'""" + + # If changing the thread_id to ever be non-zero, also revisit the check_lock_stale() below. thread_id = 0 return _hostname, _pid, thread_id +def check_lock_stale(host, pid, thread): + """Check if the host, pid, thread combination corresponds to a dead process on our local node or not.""" + if host != _hostname: + return False + + if thread != 0: + # Currently thread is always 0, if we ever decide to set this to a non-zero value, this code needs to be revisited too to do a sensible thing + return False + + try: + # This may not work in Windows. + # This does not kill anything, 0 means "see if we can send a signal to this process or not". + # Possible errors: No such process (== stale lock) or permission denied (not a stale lock) + # If the exception is not raised that means such a pid is valid and we can send a signal to it (== not a stale lock too). + os.kill(pid, 0) + return False + except OSError as err: + if err.errno != errno.ESRCH: + return False + pass + + return True + + class TimeoutTimer: """ A timer for timeout checks (can also deal with no timeout, give timeout=None [default]). @@ -109,12 +137,14 @@ class ExclusiveLock: This makes sure the lock is released again if the block is left, no matter how (e.g. if an exception occurred). """ - def __init__(self, path, timeout=None, sleep=None, id=None): + def __init__(self, path, timeout=None, sleep=None, id=None, kill_stale_locks=False): self.timeout = timeout self.sleep = sleep self.path = os.path.abspath(path) self.id = id or get_id() self.unique_name = os.path.join(self.path, "%s.%d-%x" % self.id) + self.ok_to_kill_stale_locks = kill_stale_locks + self.stale_warning_printed = False def __enter__(self): return self.acquire() @@ -137,6 +167,8 @@ class ExclusiveLock: except FileExistsError: # already locked if self.by_me(): return self + if self.kill_stale_lock(): + pass if timer.timed_out_or_sleep(): raise LockTimeout(self.path) except OSError as err: @@ -160,6 +192,47 @@ class ExclusiveLock: def by_me(self): return os.path.exists(self.unique_name) + def kill_stale_lock(self): + for name in os.listdir(self.path): + + try: + host_pid, thread_str = name.rsplit('-', 1) + host, pid_str = host_pid.rsplit('.', 1) + pid = int(pid_str) + thread = int(thread_str) + except ValueError: + # Malformed lock name? Or just some new format we don't understand? + # It's safer to just exit + return False + + if not check_lock_stale(host, pid, thread): + return False + + if not self.ok_to_kill_stale_locks: + if not self.stale_warning_printed: + print(("Found stale lock %s, but not deleting because BORG_UNIQUE_HOSTNAME is not set." % name), file=sys.stderr) + self.stale_warning_printed = True + return False + + try: + os.unlink(os.path.join(self.path, name)) + print(("Killed stale lock %s." % name), file=sys.stderr) + except OSError as err: + if not self.stale_warning_printed: + print(("Found stale lock %s, but cannot delete due to %s" % (name, str(err))), file=sys.stderr) + self.stale_warning_printed = True + return False + + try: + os.rmdir(self.path) + except OSError: + # Directory is not empty = we lost the race to somebody else + # Permission denied = we cannot operate anyway + # other error like EIO = we cannot operate and it's unsafe too. + return False + + return True + def break_lock(self): if self.is_locked(): for name in os.listdir(self.path): @@ -174,17 +247,34 @@ class LockRoster: Note: you usually should call the methods with an exclusive lock held, to avoid conflicting access by multiple threads/processes/machines. """ - def __init__(self, path, id=None): + def __init__(self, path, id=None, kill_stale_locks=False): self.path = path self.id = id or get_id() + self.ok_to_kill_zombie_locks = kill_stale_locks def load(self): try: with open(self.path) as f: data = json.load(f) + + # Just nuke the stale locks early on load + if self.ok_to_kill_zombie_locks: + for key in (SHARED, EXCLUSIVE): + elements = set() + try: + for e in data[key]: + (host, pid, thread) = e + if not check_lock_stale(host, pid, thread): + elements.add(tuple(e)) + else: + print(("Removed stale %s roster lock for pid %d." % (key, pid)), file=sys.stderr) + data[key] = list(list(e) for e in elements) + except KeyError: + pass except (FileNotFoundError, ValueError): # no or corrupt/empty roster file? data = {} + return data def save(self, data): @@ -235,18 +325,18 @@ class Lock: This makes sure the lock is released again if the block is left, no matter how (e.g. if an exception occurred). """ - def __init__(self, path, exclusive=False, sleep=None, timeout=None, id=None): + def __init__(self, path, exclusive=False, sleep=None, timeout=None, id=None, kill_stale_locks=False): self.path = path self.is_exclusive = exclusive self.sleep = sleep self.timeout = timeout self.id = id or get_id() # globally keeping track of shared and exclusive lockers: - self._roster = LockRoster(path + '.roster', id=id) + self._roster = LockRoster(path + '.roster', id=id, kill_stale_locks=kill_stale_locks) # an exclusive lock, used for: # - holding while doing roster queries / updates - # - holding while the Lock instance itself is exclusive - self._lock = ExclusiveLock(path + '.exclusive', id=id, timeout=timeout) + # - holding while the Lock itself is exclusive + self._lock = ExclusiveLock(path + '.exclusive', id=id, timeout=timeout, kill_stale_locks=kill_stale_locks) def __enter__(self): return self.acquire() diff --git a/src/borg/repository.py b/src/borg/repository.py index f1c97e13..106642d8 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -121,6 +121,7 @@ class Repository: self.do_create = create self.exclusive = exclusive self.append_only = append_only + self.unique_hostname = bool(os.environ.get('BORG_UNIQUE_HOSTNAME')) def __del__(self): if self.lock: @@ -254,7 +255,7 @@ class Repository: if not os.path.isdir(path): raise self.DoesNotExist(path) if lock: - self.lock = Lock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait).acquire() + self.lock = Lock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait, kill_stale_locks=self.unique_hostname).acquire() else: self.lock = None self.config = ConfigParser(interpolation=None) From c562f7750c80c71976c919dab62e6a9d4d27db2b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 1 Oct 2016 18:33:41 +0200 Subject: [PATCH 0349/1387] Move platform-dependent code to platform package --- src/borg/locking.py | 49 ++++--------------------------- src/borg/platform/__init__.py | 1 + src/borg/platform/posix.pyx | 55 +++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 43 deletions(-) diff --git a/src/borg/locking.py b/src/borg/locking.py index 56f28092..02c1c758 100644 --- a/src/borg/locking.py +++ b/src/borg/locking.py @@ -6,47 +6,11 @@ import sys import time from .helpers import Error, ErrorWithTraceback +from .platform import process_alive, get_process_id ADD, REMOVE = 'add', 'remove' SHARED, EXCLUSIVE = 'shared', 'exclusive' -# only determine the PID and hostname once. -# for FUSE mounts, we fork a child process that needs to release -# the lock made by the parent, so it needs to use the same PID for that. -_pid = os.getpid() -_hostname = socket.gethostname() - - -def get_id(): - """Get identification tuple for 'us'""" - - # If changing the thread_id to ever be non-zero, also revisit the check_lock_stale() below. - thread_id = 0 - return _hostname, _pid, thread_id - - -def check_lock_stale(host, pid, thread): - """Check if the host, pid, thread combination corresponds to a dead process on our local node or not.""" - if host != _hostname: - return False - - if thread != 0: - # Currently thread is always 0, if we ever decide to set this to a non-zero value, this code needs to be revisited too to do a sensible thing - return False - - try: - # This may not work in Windows. - # This does not kill anything, 0 means "see if we can send a signal to this process or not". - # Possible errors: No such process (== stale lock) or permission denied (not a stale lock) - # If the exception is not raised that means such a pid is valid and we can send a signal to it (== not a stale lock too). - os.kill(pid, 0) - return False - except OSError as err: - if err.errno != errno.ESRCH: - return False - pass - - return True class TimeoutTimer: @@ -141,7 +105,7 @@ class ExclusiveLock: self.timeout = timeout self.sleep = sleep self.path = os.path.abspath(path) - self.id = id or get_id() + self.id = id or get_process_id() self.unique_name = os.path.join(self.path, "%s.%d-%x" % self.id) self.ok_to_kill_stale_locks = kill_stale_locks self.stale_warning_printed = False @@ -194,7 +158,6 @@ class ExclusiveLock: def kill_stale_lock(self): for name in os.listdir(self.path): - try: host_pid, thread_str = name.rsplit('-', 1) host, pid_str = host_pid.rsplit('.', 1) @@ -205,7 +168,7 @@ class ExclusiveLock: # It's safer to just exit return False - if not check_lock_stale(host, pid, thread): + if not process_alive(host, pid, thread): return False if not self.ok_to_kill_stale_locks: @@ -249,7 +212,7 @@ class LockRoster: """ def __init__(self, path, id=None, kill_stale_locks=False): self.path = path - self.id = id or get_id() + self.id = id or get_process_id() self.ok_to_kill_zombie_locks = kill_stale_locks def load(self): @@ -264,7 +227,7 @@ class LockRoster: try: for e in data[key]: (host, pid, thread) = e - if not check_lock_stale(host, pid, thread): + if not process_alive(host, pid, thread): elements.add(tuple(e)) else: print(("Removed stale %s roster lock for pid %d." % (key, pid)), file=sys.stderr) @@ -330,7 +293,7 @@ class Lock: self.is_exclusive = exclusive self.sleep = sleep self.timeout = timeout - self.id = id or get_id() + self.id = id or get_process_id() # globally keeping track of shared and exclusive lockers: self._roster = LockRoster(path + '.roster', id=id, kill_stale_locks=kill_stale_locks) # an exclusive lock, used for: diff --git a/src/borg/platform/__init__.py b/src/borg/platform/__init__.py index e1772c5e..5a37cf1b 100644 --- a/src/borg/platform/__init__.py +++ b/src/borg/platform/__init__.py @@ -10,6 +10,7 @@ from .base import acl_get, acl_set from .base import set_flags, get_flags from .base import SaveFile, SyncFile, sync_dir, fdatasync from .base import swidth, API_VERSION +from .posix import process_alive, get_process_id, local_pid_alive if sys.platform.startswith('linux'): # pragma: linux only from .linux import acl_get, acl_set diff --git a/src/borg/platform/posix.pyx b/src/borg/platform/posix.pyx index c9726ea1..1351254a 100644 --- a/src/borg/platform/posix.pyx +++ b/src/borg/platform/posix.pyx @@ -1,3 +1,8 @@ + +import errno +import os +import socket + cdef extern from "wchar.h": cdef int wcswidth(const Py_UNICODE *str, size_t n) @@ -8,3 +13,53 @@ def swidth(s): return terminal_width else: return str_len + + +# only determine the PID and hostname once. +# for FUSE mounts, we fork a child process that needs to release +# the lock made by the parent, so it needs to use the same PID for that. +_pid = os.getpid() +# XXX this sometimes requires live internet access for issuing a DNS query in the background. +_hostname = socket.gethostname() + + +def get_process_id(): + """ + Return identification tuple (hostname, pid, thread_id) for 'us'. If this is a FUSE process, then the PID will be + that of the parent, not the forked FUSE child. + + Note: Currently thread_id is *always* zero. + """ + thread_id = 0 + return _hostname, _pid, thread_id + + +def process_alive(host, pid, thread): + """Check if the (host, pid, thread_id) combination corresponds to a dead process on our local node or not.""" + from . import local_pid_alive + + if host != _hostname: + return False + + if thread != 0: + # Currently thread is always 0, if we ever decide to set this to a non-zero value, + # this code needs to be revisited, too, to do a sensible thing + return False + + return local_pid_alive + +def local_pid_alive(pid): + """Return whether *pid* is alive.""" + try: + # This doesn't work on Windows. + # This does not kill anything, 0 means "see if we can send a signal to this process or not". + # Possible errors: No such process (== stale lock) or permission denied (not a stale lock) + # If the exception is not raised that means such a pid is valid and we can send a signal to it. + os.kill(pid, 0) + return True + except OSError as err: + if err.errno == errno.ESRCH: + # ESRCH = no such process + return False + # Any other error (eg. permissions) mean that the process ID refers to a live process + return True \ No newline at end of file From 8e1df7a36405b37c2debd2650c469d39ecb3f865 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 1 Oct 2016 18:33:51 +0200 Subject: [PATCH 0350/1387] Use logging instead of prints --- src/borg/locking.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/borg/locking.py b/src/borg/locking.py index 02c1c758..66d6a3bc 100644 --- a/src/borg/locking.py +++ b/src/borg/locking.py @@ -6,11 +6,13 @@ import sys import time from .helpers import Error, ErrorWithTraceback +from .logger import create_logger from .platform import process_alive, get_process_id ADD, REMOVE = 'add', 'remove' SHARED, EXCLUSIVE = 'shared', 'exclusive' +logger = create_logger(__name__) class TimeoutTimer: @@ -173,16 +175,18 @@ class ExclusiveLock: if not self.ok_to_kill_stale_locks: if not self.stale_warning_printed: - print(("Found stale lock %s, but not deleting because BORG_UNIQUE_HOSTNAME is not set." % name), file=sys.stderr) + # Log this at warning level to hint the user at the ability + logger.warning("Found stale lock %s, but not deleting because BORG_UNIQUE_HOSTNAME is not set.", name) self.stale_warning_printed = True return False try: os.unlink(os.path.join(self.path, name)) - print(("Killed stale lock %s." % name), file=sys.stderr) + logger.warning('Killed stale lock %s.', name) except OSError as err: if not self.stale_warning_printed: - print(("Found stale lock %s, but cannot delete due to %s" % (name, str(err))), file=sys.stderr) + # This error will bubble up and likely result in locking failure + logger.error('Found stale lock %s, but cannot delete due to %s', name, str(err)) self.stale_warning_printed = True return False @@ -230,7 +234,7 @@ class LockRoster: if not process_alive(host, pid, thread): elements.add(tuple(e)) else: - print(("Removed stale %s roster lock for pid %d." % (key, pid)), file=sys.stderr) + logger.warning('Removed stale %s roster lock for pid %d.', key, pid) data[key] = list(list(e) for e in elements) except KeyError: pass From 7930d055ecdbd4fb7251ae1063cf1c7ab3147ced Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 1 Oct 2016 18:35:06 +0200 Subject: [PATCH 0351/1387] import platform module instead of functions (testability) --- src/borg/locking.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/borg/locking.py b/src/borg/locking.py index 66d6a3bc..a90fbcfd 100644 --- a/src/borg/locking.py +++ b/src/borg/locking.py @@ -1,13 +1,10 @@ -import errno import json import os -import socket -import sys import time +from . import platform from .helpers import Error, ErrorWithTraceback from .logger import create_logger -from .platform import process_alive, get_process_id ADD, REMOVE = 'add', 'remove' SHARED, EXCLUSIVE = 'shared', 'exclusive' @@ -107,7 +104,7 @@ class ExclusiveLock: self.timeout = timeout self.sleep = sleep self.path = os.path.abspath(path) - self.id = id or get_process_id() + self.id = id or platform.get_process_id() self.unique_name = os.path.join(self.path, "%s.%d-%x" % self.id) self.ok_to_kill_stale_locks = kill_stale_locks self.stale_warning_printed = False @@ -170,7 +167,7 @@ class ExclusiveLock: # It's safer to just exit return False - if not process_alive(host, pid, thread): + if not platform.process_alive(host, pid, thread): return False if not self.ok_to_kill_stale_locks: @@ -216,7 +213,7 @@ class LockRoster: """ def __init__(self, path, id=None, kill_stale_locks=False): self.path = path - self.id = id or get_process_id() + self.id = id or platform.get_process_id() self.ok_to_kill_zombie_locks = kill_stale_locks def load(self): @@ -231,7 +228,7 @@ class LockRoster: try: for e in data[key]: (host, pid, thread) = e - if not process_alive(host, pid, thread): + if not platform.process_alive(host, pid, thread): elements.add(tuple(e)) else: logger.warning('Removed stale %s roster lock for pid %d.', key, pid) @@ -297,7 +294,7 @@ class Lock: self.is_exclusive = exclusive self.sleep = sleep self.timeout = timeout - self.id = id or get_process_id() + self.id = id or platform.get_process_id() # globally keeping track of shared and exclusive lockers: self._roster = LockRoster(path + '.roster', id=id, kill_stale_locks=kill_stale_locks) # an exclusive lock, used for: From 2bd8ac7762dc982bfd19c7950fa23d36fd2e539c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 2 Oct 2016 10:54:30 +0200 Subject: [PATCH 0352/1387] platform: bump API version (and check consistency) --- src/borg/helpers.py | 2 +- src/borg/platform/__init__.py | 11 ++++++++--- src/borg/platform/base.py | 2 +- src/borg/platform/darwin.pyx | 2 +- src/borg/platform/freebsd.pyx | 2 +- src/borg/platform/linux.pyx | 2 +- 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 875d708f..5535c28d 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -94,7 +94,7 @@ def check_extension_modules(): raise ExtensionModuleError if crypto.API_VERSION != 3: raise ExtensionModuleError - if platform.API_VERSION != 3: + if platform.API_VERSION != platform.OS_API_VERSION != 4: raise ExtensionModuleError diff --git a/src/borg/platform/__init__.py b/src/borg/platform/__init__.py index 5a37cf1b..1be09fe7 100644 --- a/src/borg/platform/__init__.py +++ b/src/borg/platform/__init__.py @@ -12,14 +12,19 @@ from .base import SaveFile, SyncFile, sync_dir, fdatasync from .base import swidth, API_VERSION from .posix import process_alive, get_process_id, local_pid_alive + +OS_API_VERSION = API_VERSION if sys.platform.startswith('linux'): # pragma: linux only + from .linux import API_VERSION as OS_API_VERSION from .linux import acl_get, acl_set from .linux import set_flags, get_flags from .linux import SyncFile - from .linux import swidth, API_VERSION + from .linux import swidth elif sys.platform.startswith('freebsd'): # pragma: freebsd only + from .freebsd import API_VERSION as OS_API_VERSION from .freebsd import acl_get, acl_set - from .freebsd import swidth, API_VERSION + from .freebsd import swidth elif sys.platform == 'darwin': # pragma: darwin only + from .darwin import API_VERSION as OS_API_VERSION from .darwin import acl_get, acl_set - from .darwin import swidth, API_VERSION + from .darwin import swidth diff --git a/src/borg/platform/base.py b/src/borg/platform/base.py index da8d3bc0..496a59b5 100644 --- a/src/borg/platform/base.py +++ b/src/borg/platform/base.py @@ -13,7 +13,7 @@ platform API: that way platform APIs provided by the platform-specific support m are correctly composed into the base functionality. """ -API_VERSION = 3 +API_VERSION = 4 fdatasync = getattr(os, 'fdatasync', os.fsync) diff --git a/src/borg/platform/darwin.pyx b/src/borg/platform/darwin.pyx index 188e5f4f..7f95f9e3 100644 --- a/src/borg/platform/darwin.pyx +++ b/src/borg/platform/darwin.pyx @@ -4,7 +4,7 @@ from ..helpers import user2uid, group2gid from ..helpers import safe_decode, safe_encode from .posix import swidth -API_VERSION = 3 +API_VERSION = 4 cdef extern from "sys/acl.h": ctypedef struct _acl_t: diff --git a/src/borg/platform/freebsd.pyx b/src/borg/platform/freebsd.pyx index 0a02ed8b..1aed05d0 100644 --- a/src/borg/platform/freebsd.pyx +++ b/src/borg/platform/freebsd.pyx @@ -4,7 +4,7 @@ from ..helpers import posix_acl_use_stored_uid_gid from ..helpers import safe_encode, safe_decode from .posix import swidth -API_VERSION = 3 +API_VERSION = 4 cdef extern from "errno.h": int errno diff --git a/src/borg/platform/linux.pyx b/src/borg/platform/linux.pyx index d35b28ac..7b043cf1 100644 --- a/src/borg/platform/linux.pyx +++ b/src/borg/platform/linux.pyx @@ -12,7 +12,7 @@ from .posix import swidth from libc cimport errno from libc.stdint cimport int64_t -API_VERSION = 3 +API_VERSION = 4 cdef extern from "sys/types.h": int ACL_TYPE_ACCESS From cc14975f2d25c9741fb95e98c4708311698f6b28 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 2 Oct 2016 10:54:36 +0200 Subject: [PATCH 0353/1387] Add tests for stale lock killing and platform.process_alive --- src/borg/locking.py | 23 +++++----- src/borg/platform/posix.pyx | 15 ++++-- src/borg/testsuite/locking.py | 83 ++++++++++++++++++++++++++++++---- src/borg/testsuite/platform.py | 22 +++++++++ 4 files changed, 116 insertions(+), 27 deletions(-) diff --git a/src/borg/locking.py b/src/borg/locking.py index a90fbcfd..b14b50ad 100644 --- a/src/borg/locking.py +++ b/src/borg/locking.py @@ -130,8 +130,7 @@ class ExclusiveLock: except FileExistsError: # already locked if self.by_me(): return self - if self.kill_stale_lock(): - pass + self.kill_stale_lock() if timer.timed_out_or_sleep(): raise LockTimeout(self.path) except OSError as err: @@ -167,7 +166,7 @@ class ExclusiveLock: # It's safer to just exit return False - if not platform.process_alive(host, pid, thread): + if platform.process_alive(host, pid, thread): return False if not self.ok_to_kill_stale_locks: @@ -224,17 +223,17 @@ class LockRoster: # Just nuke the stale locks early on load if self.ok_to_kill_zombie_locks: for key in (SHARED, EXCLUSIVE): - elements = set() try: - for e in data[key]: - (host, pid, thread) = e - if not platform.process_alive(host, pid, thread): - elements.add(tuple(e)) - else: - logger.warning('Removed stale %s roster lock for pid %d.', key, pid) - data[key] = list(list(e) for e in elements) + entries = data[key] except KeyError: - pass + continue + elements = set() + for host, pid, thread in entries: + if platform.process_alive(host, pid, thread): + elements.add((host, pid, thread)) + else: + logger.warning('Removed stale %s roster lock for pid %d.', key, pid) + data[key] = list(elements) except (FileNotFoundError, ValueError): # no or corrupt/empty roster file? data = {} diff --git a/src/borg/platform/posix.pyx b/src/borg/platform/posix.pyx index 1351254a..e64dc5d8 100644 --- a/src/borg/platform/posix.pyx +++ b/src/borg/platform/posix.pyx @@ -35,18 +35,23 @@ def get_process_id(): def process_alive(host, pid, thread): - """Check if the (host, pid, thread_id) combination corresponds to a dead process on our local node or not.""" + """ + Check if the (host, pid, thread_id) combination corresponds to a potentially alive process. + + If the process is local, then this will be accurate. If the process is not local, then this + returns always True, since there is no real way to check. + """ from . import local_pid_alive if host != _hostname: - return False + return True if thread != 0: # Currently thread is always 0, if we ever decide to set this to a non-zero value, # this code needs to be revisited, too, to do a sensible thing - return False + return True - return local_pid_alive + return local_pid_alive(pid) def local_pid_alive(pid): """Return whether *pid* is alive.""" @@ -62,4 +67,4 @@ def local_pid_alive(pid): # ESRCH = no such process return False # Any other error (eg. permissions) mean that the process ID refers to a live process - return True \ No newline at end of file + return True diff --git a/src/borg/testsuite/locking.py b/src/borg/testsuite/locking.py index 850c0ac5..fe8676d4 100644 --- a/src/borg/testsuite/locking.py +++ b/src/borg/testsuite/locking.py @@ -1,22 +1,25 @@ +import random import time import pytest -from ..locking import get_id, TimeoutTimer, ExclusiveLock, Lock, LockRoster, \ - ADD, REMOVE, SHARED, EXCLUSIVE, LockTimeout - +from ..platform import get_process_id, process_alive +from ..locking import TimeoutTimer, ExclusiveLock, Lock, LockRoster, \ + ADD, REMOVE, SHARED, EXCLUSIVE, LockTimeout, NotLocked, NotMyLock ID1 = "foo", 1, 1 ID2 = "bar", 2, 2 -def test_id(): - hostname, pid, tid = get_id() - assert isinstance(hostname, str) - assert isinstance(pid, int) - assert isinstance(tid, int) - assert len(hostname) > 0 - assert pid > 0 +@pytest.fixture() +def free_pid(): + """Return a free PID not used by any process (naturally this is racy)""" + host, pid, tid = get_process_id() + while True: + # PIDs are often restricted to a small range. On Linux the range >32k is by default not used. + pid = random.randint(33000, 65000) + if not process_alive(host, pid, tid): + return pid class TestTimeoutTimer: @@ -57,6 +60,22 @@ class TestExclusiveLock: with pytest.raises(LockTimeout): ExclusiveLock(lockpath, id=ID2, timeout=0.1).acquire() + def test_kill_stale(self, lockpath, free_pid): + host, pid, tid = our_id = get_process_id() + dead_id = host, free_pid, tid + cant_know_if_dead_id = 'foo.bar.example.net', 1, 2 + + dead_lock = ExclusiveLock(lockpath, id=dead_id).acquire() + with ExclusiveLock(lockpath, id=our_id, kill_stale_locks=True): + with pytest.raises(NotMyLock): + dead_lock.release() + with pytest.raises(NotLocked): + dead_lock.release() + + with ExclusiveLock(lockpath, id=cant_know_if_dead_id): + with pytest.raises(LockTimeout): + ExclusiveLock(lockpath, id=our_id, kill_stale_locks=True, timeout=0.1).acquire() + class TestLock: def test_shared(self, lockpath): @@ -117,6 +136,25 @@ class TestLock: with pytest.raises(LockTimeout): Lock(lockpath, exclusive=True, id=ID2, timeout=0.1).acquire() + def test_kill_stale(self, lockpath, free_pid): + host, pid, tid = our_id = get_process_id() + dead_id = host, free_pid, tid + cant_know_if_dead_id = 'foo.bar.example.net', 1, 2 + + dead_lock = Lock(lockpath, id=dead_id, exclusive=True).acquire() + roster = dead_lock._roster + with Lock(lockpath, id=our_id, kill_stale_locks=True): + assert roster.get(EXCLUSIVE) == set() + assert roster.get(SHARED) == {our_id} + assert roster.get(EXCLUSIVE) == set() + assert roster.get(SHARED) == set() + with pytest.raises(KeyError): + dead_lock.release() + + with Lock(lockpath, id=cant_know_if_dead_id, exclusive=True): + with pytest.raises(LockTimeout): + Lock(lockpath, id=our_id, kill_stale_locks=True, timeout=0.1).acquire() + @pytest.fixture() def rosterpath(tmpdir): @@ -144,3 +182,28 @@ class TestLockRoster: roster2 = LockRoster(rosterpath, id=ID2) roster2.modify(SHARED, REMOVE) assert roster2.get(SHARED) == set() + + def test_kill_stale(self, rosterpath, free_pid): + host, pid, tid = our_id = get_process_id() + dead_id = host, free_pid, tid + + roster1 = LockRoster(rosterpath, id=dead_id) + assert roster1.get(SHARED) == set() + roster1.modify(SHARED, ADD) + assert roster1.get(SHARED) == {dead_id} + + cant_know_if_dead_id = 'foo.bar.example.net', 1, 2 + roster1 = LockRoster(rosterpath, id=cant_know_if_dead_id) + assert roster1.get(SHARED) == {dead_id} + roster1.modify(SHARED, ADD) + assert roster1.get(SHARED) == {dead_id, cant_know_if_dead_id} + + killer_roster = LockRoster(rosterpath, kill_stale_locks=True) + # Did kill the dead processes lock (which was alive ... I guess?!) + assert killer_roster.get(SHARED) == {cant_know_if_dead_id} + killer_roster.modify(SHARED, ADD) + assert killer_roster.get(SHARED) == {our_id, cant_know_if_dead_id} + + other_killer_roster = LockRoster(rosterpath, kill_stale_locks=True) + # Did not kill us, since we're alive + assert other_killer_roster.get(SHARED) == {our_id, cant_know_if_dead_id} diff --git a/src/borg/testsuite/platform.py b/src/borg/testsuite/platform.py index 9bd81d2b..0ae1458a 100644 --- a/src/borg/testsuite/platform.py +++ b/src/borg/testsuite/platform.py @@ -1,5 +1,6 @@ import functools import os +import random import shutil import sys import tempfile @@ -7,7 +8,9 @@ import pwd import unittest from ..platform import acl_get, acl_set, swidth +from ..platform import get_process_id, process_alive from . import BaseTestCase, unopened_tempfile +from .locking import free_pid ACCESS_ACL = """ @@ -186,3 +189,22 @@ class PlatformPosixTestCase(BaseTestCase): def test_swidth_mixed(self): self.assert_equal(swidth("borgバックアップ"), 4 + 6 * 2) + + +def test_process_alive(free_pid): + id = get_process_id() + assert process_alive(*id) + host, pid, tid = id + assert process_alive(host + 'abc', pid, tid) + assert process_alive(host, pid, tid + 1) + assert not process_alive(host, free_pid, tid) + + +def test_process_id(): + hostname, pid, tid = get_process_id() + assert isinstance(hostname, str) + assert isinstance(pid, int) + assert isinstance(tid, int) + assert len(hostname) > 0 + assert pid > 0 + assert get_process_id() == (hostname, pid, tid) From 676e69cac4bbc6620fb6d58dce08b41f6414d255 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 4 Oct 2016 14:26:57 +0200 Subject: [PATCH 0354/1387] Parse & pass BORG_HOSTNAME_IS_UNIQUE env var to enable stale lock killing --- src/borg/cache.py | 6 ++++-- src/borg/helpers.py | 4 +++- src/borg/locking.py | 13 ++++++------- src/borg/platform/posix.pyx | 5 +++-- src/borg/remote.py | 8 ++++++-- src/borg/repository.py | 7 +++++-- 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 8a7ad4aa..3ed33d74 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -75,7 +75,9 @@ class Cache: self.key = key self.manifest = manifest self.path = path or os.path.join(get_cache_dir(), repository.id_str) - self.unique_hostname = bool(os.environ.get('BORG_UNIQUE_HOSTNAME')) + self.hostname_is_unique = yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', prompt=False, env_msg=None) + if self.hostname_is_unique: + logger.info('Enabled removal of stale cache locks') self.do_files = do_files # Warn user before sending data to a never seen before unencrypted repository if not os.path.exists(self.path): @@ -203,7 +205,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def open(self, lock_wait=None): if not os.path.isdir(self.path): raise Exception('%s Does not look like a Borg cache' % self.path) - self.lock = Lock(os.path.join(self.path, 'lock'), exclusive=True, timeout=lock_wait, kill_stale_locks=self.unique_hostname).acquire() + self.lock = Lock(os.path.join(self.path, 'lock'), exclusive=True, timeout=lock_wait, kill_stale_locks=self.hostname_is_unique).acquire() self.rollback() def close(self): diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 5535c28d..0b322685 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1116,7 +1116,7 @@ DEFAULTISH = ('Default', 'DEFAULT', 'default', 'D', 'd', '', ) def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, retry_msg=None, invalid_msg=None, env_msg='{} (from {})', falsish=FALSISH, truish=TRUISH, defaultish=DEFAULTISH, - default=False, retry=True, env_var_override=None, ofile=None, input=input): + default=False, retry=True, env_var_override=None, ofile=None, input=input, prompt=True): """Output (usually a question) and let user input an answer. Qualifies the answer according to falsish, truish and defaultish as True, False or . If it didn't qualify and retry is False (no retries wanted), return the default [which @@ -1161,6 +1161,8 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, if answer is not None and env_msg: print(env_msg.format(answer, env_var_override), file=ofile) if answer is None: + if not prompt: + return default try: answer = input() except EOFError: diff --git a/src/borg/locking.py b/src/borg/locking.py index b14b50ad..c3d69674 100644 --- a/src/borg/locking.py +++ b/src/borg/locking.py @@ -106,7 +106,7 @@ class ExclusiveLock: self.path = os.path.abspath(path) self.id = id or platform.get_process_id() self.unique_name = os.path.join(self.path, "%s.%d-%x" % self.id) - self.ok_to_kill_stale_locks = kill_stale_locks + self.kill_stale_locks = kill_stale_locks self.stale_warning_printed = False def __enter__(self): @@ -163,16 +163,16 @@ class ExclusiveLock: thread = int(thread_str) except ValueError: # Malformed lock name? Or just some new format we don't understand? - # It's safer to just exit + # It's safer to just exit. return False if platform.process_alive(host, pid, thread): return False - if not self.ok_to_kill_stale_locks: + if not self.kill_stale_locks: if not self.stale_warning_printed: # Log this at warning level to hint the user at the ability - logger.warning("Found stale lock %s, but not deleting because BORG_UNIQUE_HOSTNAME is not set.", name) + logger.warning("Found stale lock %s, but not deleting because BORG_HOSTNAME_IS_UNIQUE is not set.", name) self.stale_warning_printed = True return False @@ -213,7 +213,7 @@ class LockRoster: def __init__(self, path, id=None, kill_stale_locks=False): self.path = path self.id = id or platform.get_process_id() - self.ok_to_kill_zombie_locks = kill_stale_locks + self.kill_stale_locks = kill_stale_locks def load(self): try: @@ -221,7 +221,7 @@ class LockRoster: data = json.load(f) # Just nuke the stale locks early on load - if self.ok_to_kill_zombie_locks: + if self.kill_stale_locks: for key in (SHARED, EXCLUSIVE): try: entries = data[key] @@ -237,7 +237,6 @@ class LockRoster: except (FileNotFoundError, ValueError): # no or corrupt/empty roster file? data = {} - return data def save(self, data): diff --git a/src/borg/platform/posix.pyx b/src/borg/platform/posix.pyx index e64dc5d8..30b7e126 100644 --- a/src/borg/platform/posix.pyx +++ b/src/borg/platform/posix.pyx @@ -53,12 +53,13 @@ def process_alive(host, pid, thread): return local_pid_alive(pid) + def local_pid_alive(pid): """Return whether *pid* is alive.""" try: # This doesn't work on Windows. # This does not kill anything, 0 means "see if we can send a signal to this process or not". - # Possible errors: No such process (== stale lock) or permission denied (not a stale lock) + # Possible errors: No such process (== stale lock) or permission denied (not a stale lock). # If the exception is not raised that means such a pid is valid and we can send a signal to it. os.kill(pid, 0) return True @@ -66,5 +67,5 @@ def local_pid_alive(pid): if err.errno == errno.ESRCH: # ESRCH = no such process return False - # Any other error (eg. permissions) mean that the process ID refers to a live process + # Any other error (eg. permissions) means that the process ID refers to a live process. return True diff --git a/src/borg/remote.py b/src/borg/remote.py index a4988eb9..6264241c 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -18,6 +18,7 @@ from .helpers import get_home_dir from .helpers import sysinfo from .helpers import bin_to_hex from .helpers import replace_placeholders +from .helpers import yes from .repository import Repository RPC_PROTOCOL_VERSION = 2 @@ -326,12 +327,15 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. opts.append('--critical') else: raise ValueError('log level missing, fix this code') + env_vars = [] + if yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', env_msg=None, prompt=False): + env_vars.append('BORG_HOSTNAME_IS_UNIQUE=yes') if testing: - return [sys.executable, '-m', 'borg.archiver', 'serve'] + opts + self.extra_test_args + return env_vars + [sys.executable, '-m', 'borg.archiver', 'serve'] + opts + self.extra_test_args else: # pragma: no cover remote_path = args.remote_path or os.environ.get('BORG_REMOTE_PATH', 'borg') remote_path = replace_placeholders(remote_path) - return [remote_path, 'serve'] + opts + return env_vars + [remote_path, 'serve'] + opts def ssh_cmd(self, location): """return a ssh command line that can be prefixed to a borg command line""" diff --git a/src/borg/repository.py b/src/borg/repository.py index 106642d8..58c4af86 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -21,6 +21,7 @@ from .helpers import Error, ErrorWithTraceback, IntegrityError, format_file_size from .helpers import Location from .helpers import ProgressIndicatorPercent from .helpers import bin_to_hex +from .helpers import yes from .locking import Lock, LockError, LockErrorT from .logger import create_logger from .lrucache import LRUCache @@ -121,7 +122,9 @@ class Repository: self.do_create = create self.exclusive = exclusive self.append_only = append_only - self.unique_hostname = bool(os.environ.get('BORG_UNIQUE_HOSTNAME')) + self.hostname_is_unique = yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', env_msg=None, prompt=False) + if self.hostname_is_unique: + logger.info('Enabled removal of stale repository locks') def __del__(self): if self.lock: @@ -255,7 +258,7 @@ class Repository: if not os.path.isdir(path): raise self.DoesNotExist(path) if lock: - self.lock = Lock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait, kill_stale_locks=self.unique_hostname).acquire() + self.lock = Lock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait, kill_stale_locks=self.hostname_is_unique).acquire() else: self.lock = None self.config = ConfigParser(interpolation=None) From 09f470bd8548dfa1ccf4385e1098d63a343af1b0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 7 Nov 2016 22:08:11 +0100 Subject: [PATCH 0355/1387] Add crypto.blake2b_256 --- src/borg/blake2/COPYING | 122 +++++++++++ src/borg/blake2/README.md | 13 ++ src/borg/blake2/blake2-impl.h | 161 ++++++++++++++ src/borg/blake2/blake2.h | 196 ++++++++++++++++++ src/borg/blake2/blake2b-ref.c | 380 ++++++++++++++++++++++++++++++++++ src/borg/crypto.pyx | 38 ++++ src/borg/selftest.py | 2 +- src/borg/testsuite/crypto.py | 18 +- 8 files changed, 928 insertions(+), 2 deletions(-) create mode 100644 src/borg/blake2/COPYING create mode 100644 src/borg/blake2/README.md create mode 100644 src/borg/blake2/blake2-impl.h create mode 100644 src/borg/blake2/blake2.h create mode 100644 src/borg/blake2/blake2b-ref.c diff --git a/src/borg/blake2/COPYING b/src/borg/blake2/COPYING new file mode 100644 index 00000000..6ca207ef --- /dev/null +++ b/src/borg/blake2/COPYING @@ -0,0 +1,122 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. + diff --git a/src/borg/blake2/README.md b/src/borg/blake2/README.md new file mode 100644 index 00000000..696febaa --- /dev/null +++ b/src/borg/blake2/README.md @@ -0,0 +1,13 @@ +# BLAKE2 + +This is the reference source code package of BLAKE2. + +All code is triple-licensed under the [CC0](http://creativecommons.org/publicdomain/zero/1.0), +the [OpenSSL Licence](https://www.openssl.org/source/license.html), +or the [Apache Public License 2.0](http://www.apache.org/licenses/LICENSE-2.0), +at your choosing. + +More: [https://blake2.net](https://blake2.net). [GitHub repository](https://github.com/BLAKE2/BLAKE2). + +Contact: contact@blake2.net + diff --git a/src/borg/blake2/blake2-impl.h b/src/borg/blake2/blake2-impl.h new file mode 100644 index 00000000..ad9089ee --- /dev/null +++ b/src/borg/blake2/blake2-impl.h @@ -0,0 +1,161 @@ +/* + BLAKE2 reference source code package - reference C implementations + + Copyright 2012, Samuel Neves . You may use this under the + terms of the CC0, the OpenSSL Licence, or the Apache Public License 2.0, at + your option. The terms of these licenses can be found at: + + - CC0 1.0 Universal : http://creativecommons.org/publicdomain/zero/1.0 + - OpenSSL license : https://www.openssl.org/source/license.html + - Apache 2.0 : http://www.apache.org/licenses/LICENSE-2.0 + + More information about the BLAKE2 hash function can be found at + https://blake2.net. +*/ +#ifndef BLAKE2_IMPL_H +#define BLAKE2_IMPL_H + +#include +#include + +#if !defined(__cplusplus) && (!defined(__STDC_VERSION__) || __STDC_VERSION__ < 199901L) + #if defined(_MSC_VER) + #define BLAKE2_INLINE __inline + #elif defined(__GNUC__) + #define BLAKE2_INLINE __inline__ + #else + #define BLAKE2_INLINE + #endif +#else + #define BLAKE2_INLINE inline +#endif + +static BLAKE2_INLINE uint32_t load32( const void *src ) +{ +#if defined(NATIVE_LITTLE_ENDIAN) + uint32_t w; + memcpy(&w, src, sizeof w); + return w; +#else + const uint8_t *p = ( const uint8_t * )src; + return (( uint32_t )( p[0] ) << 0) | + (( uint32_t )( p[1] ) << 8) | + (( uint32_t )( p[2] ) << 16) | + (( uint32_t )( p[3] ) << 24) ; +#endif +} + +static BLAKE2_INLINE uint64_t load64( const void *src ) +{ +#if defined(NATIVE_LITTLE_ENDIAN) + uint64_t w; + memcpy(&w, src, sizeof w); + return w; +#else + const uint8_t *p = ( const uint8_t * )src; + return (( uint64_t )( p[0] ) << 0) | + (( uint64_t )( p[1] ) << 8) | + (( uint64_t )( p[2] ) << 16) | + (( uint64_t )( p[3] ) << 24) | + (( uint64_t )( p[4] ) << 32) | + (( uint64_t )( p[5] ) << 40) | + (( uint64_t )( p[6] ) << 48) | + (( uint64_t )( p[7] ) << 56) ; +#endif +} + +static BLAKE2_INLINE uint16_t load16( const void *src ) +{ +#if defined(NATIVE_LITTLE_ENDIAN) + uint16_t w; + memcpy(&w, src, sizeof w); + return w; +#else + const uint8_t *p = ( const uint8_t * )src; + return (( uint16_t )( p[0] ) << 0) | + (( uint16_t )( p[1] ) << 8) ; +#endif +} + +static BLAKE2_INLINE void store16( void *dst, uint16_t w ) +{ +#if defined(NATIVE_LITTLE_ENDIAN) + memcpy(dst, &w, sizeof w); +#else + uint8_t *p = ( uint8_t * )dst; + *p++ = ( uint8_t )w; w >>= 8; + *p++ = ( uint8_t )w; +#endif +} + +static BLAKE2_INLINE void store32( void *dst, uint32_t w ) +{ +#if defined(NATIVE_LITTLE_ENDIAN) + memcpy(dst, &w, sizeof w); +#else + uint8_t *p = ( uint8_t * )dst; + p[0] = (uint8_t)(w >> 0); + p[1] = (uint8_t)(w >> 8); + p[2] = (uint8_t)(w >> 16); + p[3] = (uint8_t)(w >> 24); +#endif +} + +static BLAKE2_INLINE void store64( void *dst, uint64_t w ) +{ +#if defined(NATIVE_LITTLE_ENDIAN) + memcpy(dst, &w, sizeof w); +#else + uint8_t *p = ( uint8_t * )dst; + p[0] = (uint8_t)(w >> 0); + p[1] = (uint8_t)(w >> 8); + p[2] = (uint8_t)(w >> 16); + p[3] = (uint8_t)(w >> 24); + p[4] = (uint8_t)(w >> 32); + p[5] = (uint8_t)(w >> 40); + p[6] = (uint8_t)(w >> 48); + p[7] = (uint8_t)(w >> 56); +#endif +} + +static BLAKE2_INLINE uint64_t load48( const void *src ) +{ + const uint8_t *p = ( const uint8_t * )src; + return (( uint64_t )( p[0] ) << 0) | + (( uint64_t )( p[1] ) << 8) | + (( uint64_t )( p[2] ) << 16) | + (( uint64_t )( p[3] ) << 24) | + (( uint64_t )( p[4] ) << 32) | + (( uint64_t )( p[5] ) << 40) ; +} + +static BLAKE2_INLINE void store48( void *dst, uint64_t w ) +{ + uint8_t *p = ( uint8_t * )dst; + p[0] = (uint8_t)(w >> 0); + p[1] = (uint8_t)(w >> 8); + p[2] = (uint8_t)(w >> 16); + p[3] = (uint8_t)(w >> 24); + p[4] = (uint8_t)(w >> 32); + p[5] = (uint8_t)(w >> 40); +} + +static BLAKE2_INLINE uint32_t rotr32( const uint32_t w, const unsigned c ) +{ + return ( w >> c ) | ( w << ( 32 - c ) ); +} + +static BLAKE2_INLINE uint64_t rotr64( const uint64_t w, const unsigned c ) +{ + return ( w >> c ) | ( w << ( 64 - c ) ); +} + +/* prevents compiler optimizing out memset() */ +static BLAKE2_INLINE void secure_zero_memory(void *v, size_t n) +{ + static void *(*const volatile memset_v)(void *, int, size_t) = &memset; + memset_v(v, 0, n); +} + +#endif + diff --git a/src/borg/blake2/blake2.h b/src/borg/blake2/blake2.h new file mode 100644 index 00000000..6420c536 --- /dev/null +++ b/src/borg/blake2/blake2.h @@ -0,0 +1,196 @@ +/* + BLAKE2 reference source code package - reference C implementations + + Copyright 2012, Samuel Neves . You may use this under the + terms of the CC0, the OpenSSL Licence, or the Apache Public License 2.0, at + your option. The terms of these licenses can be found at: + + - CC0 1.0 Universal : http://creativecommons.org/publicdomain/zero/1.0 + - OpenSSL license : https://www.openssl.org/source/license.html + - Apache 2.0 : http://www.apache.org/licenses/LICENSE-2.0 + + More information about the BLAKE2 hash function can be found at + https://blake2.net. +*/ +#ifndef BLAKE2_H +#define BLAKE2_H + +#include +#include + +#if defined(_MSC_VER) +#define BLAKE2_PACKED(x) __pragma(pack(push, 1)) x __pragma(pack(pop)) +#else +#define BLAKE2_PACKED(x) x __attribute__((packed)) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + + enum blake2s_constant + { + BLAKE2S_BLOCKBYTES = 64, + BLAKE2S_OUTBYTES = 32, + BLAKE2S_KEYBYTES = 32, + BLAKE2S_SALTBYTES = 8, + BLAKE2S_PERSONALBYTES = 8 + }; + + enum blake2b_constant + { + BLAKE2B_BLOCKBYTES = 128, + BLAKE2B_OUTBYTES = 64, + BLAKE2B_KEYBYTES = 64, + BLAKE2B_SALTBYTES = 16, + BLAKE2B_PERSONALBYTES = 16 + }; + + typedef struct blake2s_state__ + { + uint32_t h[8]; + uint32_t t[2]; + uint32_t f[2]; + uint8_t buf[BLAKE2S_BLOCKBYTES]; + size_t buflen; + size_t outlen; + uint8_t last_node; + } blake2s_state; + + typedef struct blake2b_state__ + { + uint64_t h[8]; + uint64_t t[2]; + uint64_t f[2]; + uint8_t buf[BLAKE2B_BLOCKBYTES]; + size_t buflen; + size_t outlen; + uint8_t last_node; + } blake2b_state; + + typedef struct blake2sp_state__ + { + blake2s_state S[8][1]; + blake2s_state R[1]; + uint8_t buf[8 * BLAKE2S_BLOCKBYTES]; + size_t buflen; + size_t outlen; + } blake2sp_state; + + typedef struct blake2bp_state__ + { + blake2b_state S[4][1]; + blake2b_state R[1]; + uint8_t buf[4 * BLAKE2B_BLOCKBYTES]; + size_t buflen; + size_t outlen; + } blake2bp_state; + + + BLAKE2_PACKED(struct blake2s_param__ + { + uint8_t digest_length; /* 1 */ + uint8_t key_length; /* 2 */ + uint8_t fanout; /* 3 */ + uint8_t depth; /* 4 */ + uint32_t leaf_length; /* 8 */ + uint32_t node_offset; /* 12 */ + uint16_t xof_length; /* 14 */ + uint8_t node_depth; /* 15 */ + uint8_t inner_length; /* 16 */ + /* uint8_t reserved[0]; */ + uint8_t salt[BLAKE2S_SALTBYTES]; /* 24 */ + uint8_t personal[BLAKE2S_PERSONALBYTES]; /* 32 */ + }); + + typedef struct blake2s_param__ blake2s_param; + + BLAKE2_PACKED(struct blake2b_param__ + { + uint8_t digest_length; /* 1 */ + uint8_t key_length; /* 2 */ + uint8_t fanout; /* 3 */ + uint8_t depth; /* 4 */ + uint32_t leaf_length; /* 8 */ + uint32_t node_offset; /* 12 */ + uint32_t xof_length; /* 16 */ + uint8_t node_depth; /* 17 */ + uint8_t inner_length; /* 18 */ + uint8_t reserved[14]; /* 32 */ + uint8_t salt[BLAKE2B_SALTBYTES]; /* 48 */ + uint8_t personal[BLAKE2B_PERSONALBYTES]; /* 64 */ + }); + + typedef struct blake2b_param__ blake2b_param; + + typedef struct blake2xs_state__ + { + blake2s_state S[1]; + blake2s_param P[1]; + } blake2xs_state; + + typedef struct blake2xb_state__ + { + blake2b_state S[1]; + blake2b_param P[1]; + } blake2xb_state; + + /* Padded structs result in a compile-time error */ + enum { + BLAKE2_DUMMY_1 = 1/(sizeof(blake2s_param) == BLAKE2S_OUTBYTES), + BLAKE2_DUMMY_2 = 1/(sizeof(blake2b_param) == BLAKE2B_OUTBYTES) + }; + + /* Streaming API */ + int blake2s_init( blake2s_state *S, size_t outlen ); + int blake2s_init_key( blake2s_state *S, size_t outlen, const void *key, size_t keylen ); + int blake2s_init_param( blake2s_state *S, const blake2s_param *P ); + int blake2s_update( blake2s_state *S, const void *in, size_t inlen ); + int blake2s_final( blake2s_state *S, void *out, size_t outlen ); + + int blake2b_init( blake2b_state *S, size_t outlen ); + int blake2b_init_key( blake2b_state *S, size_t outlen, const void *key, size_t keylen ); + int blake2b_init_param( blake2b_state *S, const blake2b_param *P ); + int blake2b_update( blake2b_state *S, const void *in, size_t inlen ); + int blake2b_final( blake2b_state *S, void *out, size_t outlen ); + + int blake2sp_init( blake2sp_state *S, size_t outlen ); + int blake2sp_init_key( blake2sp_state *S, size_t outlen, const void *key, size_t keylen ); + int blake2sp_update( blake2sp_state *S, const void *in, size_t inlen ); + int blake2sp_final( blake2sp_state *S, void *out, size_t outlen ); + + int blake2bp_init( blake2bp_state *S, size_t outlen ); + int blake2bp_init_key( blake2bp_state *S, size_t outlen, const void *key, size_t keylen ); + int blake2bp_update( blake2bp_state *S, const void *in, size_t inlen ); + int blake2bp_final( blake2bp_state *S, void *out, size_t outlen ); + + /* Variable output length API */ + int blake2xs_init( blake2xs_state *S, const size_t outlen ); + int blake2xs_init_key( blake2xs_state *S, const size_t outlen, const void *key, size_t keylen ); + int blake2xs_update( blake2xs_state *S, const void *in, size_t inlen ); + int blake2xs_final(blake2xs_state *S, void *out, size_t outlen); + + int blake2xb_init( blake2xb_state *S, const size_t outlen ); + int blake2xb_init_key( blake2xb_state *S, const size_t outlen, const void *key, size_t keylen ); + int blake2xb_update( blake2xb_state *S, const void *in, size_t inlen ); + int blake2xb_final(blake2xb_state *S, void *out, size_t outlen); + + /* Simple API */ + int blake2s( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + int blake2b( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + + int blake2sp( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + int blake2bp( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + + int blake2xs( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + int blake2xb( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + + /* This is simply an alias for blake2b */ + int blake2( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + +#if defined(__cplusplus) +} +#endif + +#endif + diff --git a/src/borg/blake2/blake2b-ref.c b/src/borg/blake2/blake2b-ref.c new file mode 100644 index 00000000..0d36fb0d --- /dev/null +++ b/src/borg/blake2/blake2b-ref.c @@ -0,0 +1,380 @@ +/* + BLAKE2 reference source code package - reference C implementations + + Copyright 2012, Samuel Neves . You may use this under the + terms of the CC0, the OpenSSL Licence, or the Apache Public License 2.0, at + your option. The terms of these licenses can be found at: + + - CC0 1.0 Universal : http://creativecommons.org/publicdomain/zero/1.0 + - OpenSSL license : https://www.openssl.org/source/license.html + - Apache 2.0 : http://www.apache.org/licenses/LICENSE-2.0 + + More information about the BLAKE2 hash function can be found at + https://blake2.net. +*/ + +#include +#include +#include + +#include "blake2.h" +#include "blake2-impl.h" + +static const uint64_t blake2b_IV[8] = +{ + 0x6a09e667f3bcc908ULL, 0xbb67ae8584caa73bULL, + 0x3c6ef372fe94f82bULL, 0xa54ff53a5f1d36f1ULL, + 0x510e527fade682d1ULL, 0x9b05688c2b3e6c1fULL, + 0x1f83d9abfb41bd6bULL, 0x5be0cd19137e2179ULL +}; + +static const uint8_t blake2b_sigma[12][16] = +{ + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 } , + { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 } , + { 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4 } , + { 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8 } , + { 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13 } , + { 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9 } , + { 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11 } , + { 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10 } , + { 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5 } , + { 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13 , 0 } , + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 } , + { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 } +}; + + +static void blake2b_set_lastnode( blake2b_state *S ) +{ + S->f[1] = (uint64_t)-1; +} + +/* Some helper functions, not necessarily useful */ +static int blake2b_is_lastblock( const blake2b_state *S ) +{ + return S->f[0] != 0; +} + +static void blake2b_set_lastblock( blake2b_state *S ) +{ + if( S->last_node ) blake2b_set_lastnode( S ); + + S->f[0] = (uint64_t)-1; +} + +static void blake2b_increment_counter( blake2b_state *S, const uint64_t inc ) +{ + S->t[0] += inc; + S->t[1] += ( S->t[0] < inc ); +} + +static void blake2b_init0( blake2b_state *S ) +{ + size_t i; + memset( S, 0, sizeof( blake2b_state ) ); + + for( i = 0; i < 8; ++i ) S->h[i] = blake2b_IV[i]; +} + +/* init xors IV with input parameter block */ +int blake2b_init_param( blake2b_state *S, const blake2b_param *P ) +{ + const uint8_t *p = ( const uint8_t * )( P ); + size_t i; + + blake2b_init0( S ); + + /* IV XOR ParamBlock */ + for( i = 0; i < 8; ++i ) + S->h[i] ^= load64( p + sizeof( S->h[i] ) * i ); + + S->outlen = P->digest_length; + return 0; +} + + + +int blake2b_init( blake2b_state *S, size_t outlen ) +{ + blake2b_param P[1]; + + if ( ( !outlen ) || ( outlen > BLAKE2B_OUTBYTES ) ) return -1; + + P->digest_length = (uint8_t)outlen; + P->key_length = 0; + P->fanout = 1; + P->depth = 1; + store32( &P->leaf_length, 0 ); + store32( &P->node_offset, 0 ); + store32( &P->xof_length, 0 ); + P->node_depth = 0; + P->inner_length = 0; + memset( P->reserved, 0, sizeof( P->reserved ) ); + memset( P->salt, 0, sizeof( P->salt ) ); + memset( P->personal, 0, sizeof( P->personal ) ); + return blake2b_init_param( S, P ); +} + + +int blake2b_init_key( blake2b_state *S, size_t outlen, const void *key, size_t keylen ) +{ + blake2b_param P[1]; + + if ( ( !outlen ) || ( outlen > BLAKE2B_OUTBYTES ) ) return -1; + + if ( !key || !keylen || keylen > BLAKE2B_KEYBYTES ) return -1; + + P->digest_length = (uint8_t)outlen; + P->key_length = (uint8_t)keylen; + P->fanout = 1; + P->depth = 1; + store32( &P->leaf_length, 0 ); + store32( &P->node_offset, 0 ); + store32( &P->xof_length, 0 ); + P->node_depth = 0; + P->inner_length = 0; + memset( P->reserved, 0, sizeof( P->reserved ) ); + memset( P->salt, 0, sizeof( P->salt ) ); + memset( P->personal, 0, sizeof( P->personal ) ); + + if( blake2b_init_param( S, P ) < 0 ) return -1; + + { + uint8_t block[BLAKE2B_BLOCKBYTES]; + memset( block, 0, BLAKE2B_BLOCKBYTES ); + memcpy( block, key, keylen ); + blake2b_update( S, block, BLAKE2B_BLOCKBYTES ); + secure_zero_memory( block, BLAKE2B_BLOCKBYTES ); /* Burn the key from stack */ + } + return 0; +} + +#define G(r,i,a,b,c,d) \ + do { \ + a = a + b + m[blake2b_sigma[r][2*i+0]]; \ + d = rotr64(d ^ a, 32); \ + c = c + d; \ + b = rotr64(b ^ c, 24); \ + a = a + b + m[blake2b_sigma[r][2*i+1]]; \ + d = rotr64(d ^ a, 16); \ + c = c + d; \ + b = rotr64(b ^ c, 63); \ + } while(0) + +#define ROUND(r) \ + do { \ + G(r,0,v[ 0],v[ 4],v[ 8],v[12]); \ + G(r,1,v[ 1],v[ 5],v[ 9],v[13]); \ + G(r,2,v[ 2],v[ 6],v[10],v[14]); \ + G(r,3,v[ 3],v[ 7],v[11],v[15]); \ + G(r,4,v[ 0],v[ 5],v[10],v[15]); \ + G(r,5,v[ 1],v[ 6],v[11],v[12]); \ + G(r,6,v[ 2],v[ 7],v[ 8],v[13]); \ + G(r,7,v[ 3],v[ 4],v[ 9],v[14]); \ + } while(0) + +static void blake2b_compress( blake2b_state *S, const uint8_t block[BLAKE2B_BLOCKBYTES] ) +{ + uint64_t m[16]; + uint64_t v[16]; + size_t i; + + for( i = 0; i < 16; ++i ) { + m[i] = load64( block + i * sizeof( m[i] ) ); + } + + for( i = 0; i < 8; ++i ) { + v[i] = S->h[i]; + } + + v[ 8] = blake2b_IV[0]; + v[ 9] = blake2b_IV[1]; + v[10] = blake2b_IV[2]; + v[11] = blake2b_IV[3]; + v[12] = blake2b_IV[4] ^ S->t[0]; + v[13] = blake2b_IV[5] ^ S->t[1]; + v[14] = blake2b_IV[6] ^ S->f[0]; + v[15] = blake2b_IV[7] ^ S->f[1]; + + ROUND( 0 ); + ROUND( 1 ); + ROUND( 2 ); + ROUND( 3 ); + ROUND( 4 ); + ROUND( 5 ); + ROUND( 6 ); + ROUND( 7 ); + ROUND( 8 ); + ROUND( 9 ); + ROUND( 10 ); + ROUND( 11 ); + + for( i = 0; i < 8; ++i ) { + S->h[i] = S->h[i] ^ v[i] ^ v[i + 8]; + } +} + +#undef G +#undef ROUND + +int blake2b_update( blake2b_state *S, const void *pin, size_t inlen ) +{ + const unsigned char * in = (const unsigned char *)pin; + if( inlen > 0 ) + { + size_t left = S->buflen; + size_t fill = BLAKE2B_BLOCKBYTES - left; + if( inlen > fill ) + { + S->buflen = 0; + memcpy( S->buf + left, in, fill ); /* Fill buffer */ + blake2b_increment_counter( S, BLAKE2B_BLOCKBYTES ); + blake2b_compress( S, S->buf ); /* Compress */ + in += fill; inlen -= fill; + while(inlen > BLAKE2B_BLOCKBYTES) { + blake2b_increment_counter(S, BLAKE2B_BLOCKBYTES); + blake2b_compress( S, in ); + in += BLAKE2B_BLOCKBYTES; + inlen -= BLAKE2B_BLOCKBYTES; + } + } + memcpy( S->buf + S->buflen, in, inlen ); + S->buflen += inlen; + } + return 0; +} + +int blake2b_final( blake2b_state *S, void *out, size_t outlen ) +{ + uint8_t buffer[BLAKE2B_OUTBYTES] = {0}; + size_t i; + + if( out == NULL || outlen < S->outlen ) + return -1; + + if( blake2b_is_lastblock( S ) ) + return -1; + + blake2b_increment_counter( S, S->buflen ); + blake2b_set_lastblock( S ); + memset( S->buf + S->buflen, 0, BLAKE2B_BLOCKBYTES - S->buflen ); /* Padding */ + blake2b_compress( S, S->buf ); + + for( i = 0; i < 8; ++i ) /* Output full hash to temp buffer */ + store64( buffer + sizeof( S->h[i] ) * i, S->h[i] ); + + memcpy( out, buffer, S->outlen ); + secure_zero_memory(buffer, sizeof(buffer)); + return 0; +} + +/* inlen, at least, should be uint64_t. Others can be size_t. */ +int blake2b( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ) +{ + blake2b_state S[1]; + + /* Verify parameters */ + if ( NULL == in && inlen > 0 ) return -1; + + if ( NULL == out ) return -1; + + if( NULL == key && keylen > 0 ) return -1; + + if( !outlen || outlen > BLAKE2B_OUTBYTES ) return -1; + + if( keylen > BLAKE2B_KEYBYTES ) return -1; + + if( keylen > 0 ) + { + if( blake2b_init_key( S, outlen, key, keylen ) < 0 ) return -1; + } + else + { + if( blake2b_init( S, outlen ) < 0 ) return -1; + } + + blake2b_update( S, ( const uint8_t * )in, inlen ); + blake2b_final( S, out, outlen ); + return 0; +} + +int blake2( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ) { + return blake2b(out, outlen, in, inlen, key, keylen); +} + +#if defined(SUPERCOP) +int crypto_hash( unsigned char *out, unsigned char *in, unsigned long long inlen ) +{ + return blake2b( out, BLAKE2B_OUTBYTES, in, inlen, NULL, 0 ); +} +#endif + +#if defined(BLAKE2B_SELFTEST) +#include +#include "blake2-kat.h" +int main( void ) +{ + uint8_t key[BLAKE2B_KEYBYTES]; + uint8_t buf[BLAKE2_KAT_LENGTH]; + size_t i, step; + + for( i = 0; i < BLAKE2B_KEYBYTES; ++i ) + key[i] = ( uint8_t )i; + + for( i = 0; i < BLAKE2_KAT_LENGTH; ++i ) + buf[i] = ( uint8_t )i; + + /* Test simple API */ + for( i = 0; i < BLAKE2_KAT_LENGTH; ++i ) + { + uint8_t hash[BLAKE2B_OUTBYTES]; + blake2b( hash, BLAKE2B_OUTBYTES, buf, i, key, BLAKE2B_KEYBYTES ); + + if( 0 != memcmp( hash, blake2b_keyed_kat[i], BLAKE2B_OUTBYTES ) ) + { + goto fail; + } + } + + /* Test streaming API */ + for(step = 1; step < BLAKE2B_BLOCKBYTES; ++step) { + for (i = 0; i < BLAKE2_KAT_LENGTH; ++i) { + uint8_t hash[BLAKE2B_OUTBYTES]; + blake2b_state S; + uint8_t * p = buf; + size_t mlen = i; + int err = 0; + + if( (err = blake2b_init_key(&S, BLAKE2B_OUTBYTES, key, BLAKE2B_KEYBYTES)) < 0 ) { + goto fail; + } + + while (mlen >= step) { + if ( (err = blake2b_update(&S, p, step)) < 0 ) { + goto fail; + } + mlen -= step; + p += step; + } + if ( (err = blake2b_update(&S, p, mlen)) < 0) { + goto fail; + } + if ( (err = blake2b_final(&S, hash, BLAKE2B_OUTBYTES)) < 0) { + goto fail; + } + + if (0 != memcmp(hash, blake2b_keyed_kat[i], BLAKE2B_OUTBYTES)) { + goto fail; + } + } + } + + puts( "ok" ); + return 0; +fail: + puts("error"); + return -1; +} +#endif + diff --git a/src/borg/crypto.pyx b/src/borg/crypto.pyx index 286d596b..4e19e530 100644 --- a/src/borg/crypto.pyx +++ b/src/borg/crypto.pyx @@ -6,6 +6,15 @@ from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release API_VERSION = 3 +cdef extern from "blake2/blake2b-ref.c": + ctypedef struct blake2b_state: + pass + + int blake2b_init(blake2b_state *S, size_t outlen) nogil + int blake2b_update(blake2b_state *S, const void *input, size_t inlen) nogil + int blake2b_final(blake2b_state *S, void *out, size_t outlen) nogil + + cdef extern from "openssl/evp.h": ctypedef struct EVP_MD: pass @@ -201,3 +210,32 @@ def hmac_sha256(key, data): finally: PyBuffer_Release(&data_buf) return md + + +cdef blake2b_update_from_buffer(blake2b_state *state, obj): + cdef Py_buffer buf = ro_buffer(obj) + try: + with nogil: + rc = blake2b_update(state, buf.buf, buf.len) + if rc == -1: + raise Exception('blake2b_update(key) failed') + finally: + PyBuffer_Release(&buf) + + +def blake2b_256(key, data): + cdef blake2b_state state + if blake2b_init(&state, 32) == -1: + raise Exception('blake2b_init() failed') + + md = bytes(32) + cdef unsigned char *md_ptr = md + + blake2b_update_from_buffer(&state, key) + blake2b_update_from_buffer(&state, data) + + rc = blake2b_final(&state, md_ptr, 32) + if rc == -1: + raise Exception('blake2b_final() failed') + + return md diff --git a/src/borg/selftest.py b/src/borg/selftest.py index 139ed7e8..40d8d06e 100644 --- a/src/borg/selftest.py +++ b/src/borg/selftest.py @@ -30,7 +30,7 @@ SELFTEST_CASES = [ ChunkerTestCase, ] -SELFTEST_COUNT = 29 +SELFTEST_COUNT = 30 class SelfTestResult(TestResult): diff --git a/src/borg/testsuite/crypto.py b/src/borg/testsuite/crypto.py index b79a5d83..aa138a76 100644 --- a/src/borg/testsuite/crypto.py +++ b/src/borg/testsuite/crypto.py @@ -1,6 +1,6 @@ from binascii import hexlify, unhexlify -from ..crypto import AES, bytes_to_long, bytes_to_int, long_to_bytes, hmac_sha256 +from ..crypto import AES, bytes_to_long, bytes_to_int, long_to_bytes, hmac_sha256, blake2b_256 from ..crypto import increment_iv, bytes16_to_int, int_to_bytes16 from . import BaseTestCase @@ -80,3 +80,19 @@ class CryptoTestCase(BaseTestCase): hmac = unhexlify('82558a389a443c0ea4cc819899f2083a' '85f0faa3e578f8077a2e3ff46729665b') assert hmac_sha256(key, data) == hmac + + def test_blake2b_256(self): + # In BLAKE2 the output length actually is part of the hashes personality - it is *not* simple truncation like in + # the SHA-2 family. Therefore we need to generate test vectors ourselves (as is true for most applications that + # are not precisely vanilla BLAKE2b-512 or BLAKE2s-256). + # + # Obtained via "b2sum" utility from the official BLAKE2 repository. It calculates the exact hash of a file's + # contents, no extras (like length) included. + assert blake2b_256(b'', b'abc') == unhexlify('bddd813c634239723171ef3fee98579b94964e3bb1cb3e427262c8c068d52319') + assert blake2b_256(b'a', b'bc') == unhexlify('bddd813c634239723171ef3fee98579b94964e3bb1cb3e427262c8c068d52319') + assert blake2b_256(b'ab', b'c') == unhexlify('bddd813c634239723171ef3fee98579b94964e3bb1cb3e427262c8c068d52319') + assert blake2b_256(b'abc', b'') == unhexlify('bddd813c634239723171ef3fee98579b94964e3bb1cb3e427262c8c068d52319') + + key = unhexlify('e944973af2256d4d670c12dd75304c319f58f4e40df6fb18ef996cb47e063676') + data = memoryview(b'1234567890' * 100) + assert blake2b_256(key, data) == unhexlify('97ede832378531dd0f4c668685d166e797da27b47d8cd441e885b60abd5e0cb2') From 79c77e7a682a0dca194de066a7d5bbe131b32126 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Thu, 10 Nov 2016 13:44:58 +0100 Subject: [PATCH 0356/1387] Repository: rollback's cleanup parameter is internal. --- src/borg/repository.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index f1c97e13..d67643ff 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -147,7 +147,7 @@ class Repository: cleanup = True else: cleanup = False - self.rollback(cleanup) + self._rollback(cleanup=cleanup) self.close() @property @@ -433,7 +433,7 @@ class Repository: free_space = st_vfs.f_bavail * st_vfs.f_bsize logger.debug('check_free_space: required bytes {}, free bytes {}'.format(required_free_space, free_space)) if free_space < required_free_space: - self.rollback(cleanup=True) + self._rollback(cleanup=True) formatted_required = format_file_size(required_free_space) formatted_free = format_file_size(free_space) raise self.InsufficientFreeSpaceError(formatted_required, formatted_free) @@ -731,7 +731,7 @@ class Repository: logger.info('Completed repository check, no problems found.') return not error_found or repair - def rollback(self, cleanup=False): + def _rollback(self, *, cleanup): """ """ if cleanup: @@ -739,6 +739,9 @@ class Repository: self.index = None self._active_txn = False + def rollback(self): + self._rollback(cleanup=False) + def __len__(self): if not self.index: self.index = self.open_index(self.get_transaction_id()) From 74ab7f1f612cbbaf040847a56b0059df651e3ba5 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Thu, 10 Nov 2016 16:48:35 +0100 Subject: [PATCH 0357/1387] .gitattributes: Set python diff mode for all *.py files. --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index a97e7297..9d00a690 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ borg/_version.py export-subst + +*.py diff=python From 5fecac63a929a710f9b6eb17b81a5b7e7911c64b Mon Sep 17 00:00:00 2001 From: enkore Date: Thu, 10 Nov 2016 17:35:59 +0100 Subject: [PATCH 0358/1387] testsuite/archiver: fix missing newline before RemoteArchiverTestCase --- borg/testsuite/archiver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index b0fa1a11..bc49bc66 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1444,6 +1444,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.cmd('check', self.repository_location, exit_code=0) self.cmd('extract', '--dry-run', self.repository_location + '::archive1', exit_code=0) + @pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteArchiverTestCase(ArchiverTestCase): prefix = '__testsuite__:' From 3ee019761aff9deecef8096b3eb2a1a57bf02a8a Mon Sep 17 00:00:00 2001 From: Johannes Wienke Date: Thu, 10 Nov 2016 19:40:08 +0100 Subject: [PATCH 0359/1387] Clarify prune behavior for different archive contents In the online help, explain that archives with different contents need to be separated via the prefix when pruning to achieve a desired retention policy per archive set. Relates to #1824. --- borg/archiver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/borg/archiver.py b/borg/archiver.py index f417c245..07086709 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1536,6 +1536,8 @@ class Archiver: considered for deletion and only those archives count towards the totals specified by the rules. Otherwise, *all* archives in the repository are candidates for deletion! + There is no automatic distinction between archives representing different + contents. These need to be distinguished by specifying matching prefixes. """) subparser = subparsers.add_parser('prune', parents=[common_parser], description=self.do_prune.__doc__, From 00ac7b14be2fb2d466989967fda45ce20353e7e5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 10 Nov 2016 00:41:27 +0100 Subject: [PATCH 0360/1387] Add BLAKE2b key types --- src/borg/archiver.py | 23 +++++++++-- src/borg/blake2/openssl-b2.c | 0 src/borg/crypto.pyx | 5 +++ src/borg/key.py | 77 +++++++++++++++++++++++++++++++----- src/borg/testsuite/key.py | 48 +++++++++++++++++++++- 5 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 src/borg/blake2/openssl-b2.c diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 785ef668..4999440a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -959,7 +959,7 @@ class Archiver: else: encrypted = 'Yes (%s)' % key.NAME print('Encrypted: %s' % encrypted) - if key.NAME == 'key file': + if key.NAME.startswith('key file'): print('Key file: %s' % key.find_key()) print('Cache: %s' % cache.path) print(DASHES) @@ -1556,6 +1556,7 @@ class Archiver: 'Access to all sub-directories is granted implicitly; PATH doesn\'t need to directly point to a repository.') subparser.add_argument('--append-only', dest='append_only', action='store_true', help='only allow appending to repository segment files') + init_epilog = textwrap.dedent(""" This command initializes an empty repository. A repository is a filesystem directory containing the deduplicated data from zero or more archives. @@ -1599,8 +1600,21 @@ class Archiver: You can change your passphrase for existing repos at any time, it won't affect the encryption/decryption key or other secrets. - When encrypting, AES-CTR-256 is used for encryption, and HMAC-SHA256 for - authentication. Hardware acceleration will be used automatically. + Encryption modes + ++++++++++++++++ + + repokey and keyfile use AES-CTR-256 for encryption and HMAC-SHA256 for + authentication in an encrypt-then-MAC (EtM) construction. The chunk ID hash + is HMAC-SHA256 as well (with a separate key). + + repokey-blake2 and keyfile-blake2 use the same authenticated encryption, but + use a keyed BLAKE2b-256 hash for the chunk ID hash. + + "authenticated" mode uses no encryption, but authenticates repository contents + through the same keyed BLAKE2b-256 hash as the other blake2 modes. + The key is stored like repokey. + + Hardware acceleration will be used automatically. """) subparser = subparsers.add_parser('init', parents=[common_parser], add_help=False, description=self.do_init.__doc__, epilog=init_epilog, @@ -1611,7 +1625,8 @@ class Archiver: type=location_validator(archive=False), help='repository to create') subparser.add_argument('-e', '--encryption', dest='encryption', - choices=('none', 'keyfile', 'repokey'), default='repokey', + choices=('none', 'keyfile', 'repokey', 'keyfile-blake2', 'repokey-blake2', 'authenticated'), + default='repokey', help='select encryption key mode (default: "%(default)s")') subparser.add_argument('-a', '--append-only', dest='append_only', action='store_true', help='create an append-only mode repository') diff --git a/src/borg/blake2/openssl-b2.c b/src/borg/blake2/openssl-b2.c new file mode 100644 index 00000000..e69de29b diff --git a/src/borg/crypto.pyx b/src/borg/crypto.pyx index 4e19e530..bd61955e 100644 --- a/src/borg/crypto.pyx +++ b/src/borg/crypto.pyx @@ -231,6 +231,11 @@ def blake2b_256(key, data): md = bytes(32) cdef unsigned char *md_ptr = md + # This is secure, because BLAKE2 is not vulnerable to length-extension attacks (unlike SHA-1/2, MD-5 and others). + # See the BLAKE2 paper section 2.9 "Keyed hashing (MAC and PRF)" for details. + # A nice benefit is that this simpler prefix-MAC mode has less overhead than the more complex HMAC mode. + # We don't use the BLAKE2 parameter block (via blake2s_init_key) for this to + # avoid incompatibility with the limited API of OpenSSL. blake2b_update_from_buffer(&state, key) blake2b_update_from_buffer(&state, data) diff --git a/src/borg/key.py b/src/borg/key.py index aa603c0c..f5636ba3 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -14,7 +14,7 @@ logger = create_logger() from .constants import * # NOQA from .compress import Compressor, get_compressor -from .crypto import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks, hmac_sha256 +from .crypto import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256 from .helpers import Chunk from .helpers import Error, IntegrityError from .helpers import yes @@ -62,6 +62,12 @@ def key_creator(repository, args): return KeyfileKey.create(repository, args) elif args.encryption == 'repokey': return RepoKey.create(repository, args) + elif args.encryption == 'keyfile-blake2': + return Blake2KeyfileKey.create(repository, args) + elif args.encryption == 'repokey-blake2': + return Blake2RepoKey.create(repository, args) + elif args.encryption == 'authenticated': + return AuthenticatedKey.create(repository, args) else: return PlaintextKey.create(repository, args) @@ -78,6 +84,12 @@ def key_factory(repository, manifest_data): return RepoKey.detect(repository, manifest_data) elif key_type == PlaintextKey.TYPE: return PlaintextKey.detect(repository, manifest_data) + elif key_type == Blake2KeyfileKey.TYPE: + return Blake2KeyfileKey.detect(repository, manifest_data) + elif key_type == Blake2RepoKey.TYPE: + return Blake2RepoKey.detect(repository, manifest_data) + elif key_type == AuthenticatedKey.TYPE: + return AuthenticatedKey.detect(repository, manifest_data) else: raise UnsupportedPayloadError(key_type) @@ -149,6 +161,28 @@ class PlaintextKey(KeyBase): return Chunk(data) +class ID_BLAKE2b_256: + """ + Key mix-in class for using BLAKE2b-256 for the id key. + + The id_key length must be 32 bytes. + """ + + def id_hash(self, data): + return blake2b_256(self.id_key, data) + + +class ID_HMAC_SHA_256: + """ + Key mix-in class for using HMAC-SHA-256 for the id key. + + The id_key length must be 32 bytes. + """ + + def id_hash(self, data): + return hmac_sha256(self.id_key, data) + + class AESKeyBase(KeyBase): """Common base class shared by KeyfileKey and PassphraseKey @@ -164,11 +198,6 @@ class AESKeyBase(KeyBase): PAYLOAD_OVERHEAD = 1 + 32 + 8 # TYPE + HMAC + NONCE - def id_hash(self, data): - """Return HMAC hash using the "id" HMAC key - """ - return hmac_sha256(self.id_key, data) - def encrypt(self, chunk): chunk = self.compress(chunk) self.nonce_manager.ensure_reservation(num_aes_blocks(len(chunk.data))) @@ -272,7 +301,7 @@ class Passphrase(str): return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length) -class PassphraseKey(AESKeyBase): +class PassphraseKey(ID_HMAC_SHA_256, AESKeyBase): # This mode was killed in borg 1.0, see: https://github.com/borgbackup/borg/issues/97 # Reasons: # - you can never ever change your passphrase for existing repos. @@ -432,7 +461,7 @@ class KeyfileKeyBase(AESKeyBase): raise NotImplementedError -class KeyfileKey(KeyfileKeyBase): +class KeyfileKey(ID_HMAC_SHA_256, KeyfileKeyBase): TYPE = 0x00 NAME = 'key file' FILE_ID = 'BORG_KEY' @@ -492,7 +521,7 @@ class KeyfileKey(KeyfileKeyBase): self.target = target -class RepoKey(KeyfileKeyBase): +class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase): TYPE = 0x03 NAME = 'repokey' @@ -522,3 +551,33 @@ class RepoKey(KeyfileKeyBase): key_data = key_data.encode('utf-8') # remote repo: msgpack issue #99, giving bytes target.save_key(key_data) self.target = target + + +class Blake2KeyfileKey(ID_BLAKE2b_256, KeyfileKey): + TYPE = 0x04 + NAME = 'key file BLAKE2b' + FILE_ID = 'BORG_KEY' + + +class Blake2RepoKey(ID_BLAKE2b_256, RepoKey): + TYPE = 0x05 + NAME = 'repokey BLAKE2b' + + +class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): + TYPE = 0x06 + NAME = 'authenticated BLAKE2b' + + def encrypt(self, chunk): + chunk = self.compress(chunk) + return b''.join([self.TYPE_STR, chunk.data]) + + def decrypt(self, id, data, decompress=True): + if data[0] != self.TYPE: + raise IntegrityError('Chunk %s: Invalid envelope' % bin_to_hex(id)) + payload = memoryview(data)[1:] + if not decompress: + return Chunk(payload) + data = self.compressor.decompress(payload) + self.assert_id(id, data) + return Chunk(data) diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index 95c74e36..5f456b0b 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -11,7 +11,8 @@ from ..helpers import Location from ..helpers import Chunk from ..helpers import IntegrityError from ..helpers import get_nonces_dir -from ..key import PlaintextKey, PassphraseKey, KeyfileKey, Passphrase, PasswordRetriesExceeded, bin_to_hex +from ..key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, AuthenticatedKey +from ..key import Passphrase, PasswordRetriesExceeded, bin_to_hex class TestKey: @@ -34,6 +35,24 @@ class TestKey: """)) keyfile2_id = unhexlify('c3fbf14bc001ebcc3cd86e696c13482ed071740927cd7cbe1b01b4bfcee49314') + keyfile_blake2_key_file = """ + BORG_KEY 0000000000000000000000000000000000000000000000000000000000000000 + hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaANAwo4EbUPF/kLQXhQnT4LxRc1advS8lUiegDa + q2Q6oOkP1Jc7MwBa7ZVMgoBG1sBeKYO6Sn6W6BBrHbMR8Dxv7xquaQIh8jIpnjLWpzyFIk + JlijFiTWI58Sxj+2D19b2ayFolnGkF9PJSARgfaieo0GkryqjcIgcXuKHO/H9NfaUDk5YJ + UqrJ9TUMohXSQzwF1pO4ak2BHPZKnbeJ7XL/8fFN8VFQZl27R0et4WlTFRBI1qQYyQaTiL + +/1ICMUpVsQM0mvyW6dc8/zGMsAlmZVApGhhc2jaACDdRF7uPv90UN3zsZy5Be89728RBl + zKvtzupDyTsfrJMqppdGVyYXRpb25zzgABhqCkc2FsdNoAIGTK3TR09UZqw1bPi17gyHOi + 7YtSp4BVK7XptWeKh6Vip3ZlcnNpb24B""".strip() + + keyfile_blake2_cdata = bytes.fromhex('04dd21cc91140ef009bc9e4dd634d075e39d39025ccce1289c' + '5536f9cb57f5f8130404040404040408ec852921309243b164') + # Verified against b2sum. Entire string passed to BLAKE2, including the 32 byte key contained in + # keyfile_blake2_key_file above is + # 037fb9b75b20d623f1d5a568050fccde4a1b7c5f5047432925e941a17c7a2d0d7061796c6f6164 + # p a y l o a d + keyfile_blake2_id = bytes.fromhex('a22d4fc81bb61c3846c334a09eaf28d22dd7df08c9a7a41e713ef28d80eebd45') + @pytest.fixture def keys_dir(self, request, monkeypatch, tmpdir): monkeypatch.setenv('BORG_KEYS_DIR', tmpdir) @@ -41,7 +60,11 @@ class TestKey: @pytest.fixture(params=( KeyfileKey, - PlaintextKey + PlaintextKey, + RepoKey, + Blake2KeyfileKey, + Blake2RepoKey, + AuthenticatedKey, )) def key(self, request, monkeypatch): monkeypatch.setenv('BORG_PASSPHRASE', 'test') @@ -61,6 +84,12 @@ class TestKey: def commit_nonce_reservation(self, next_unreserved, start_nonce): pass + def save_key(self, data): + self.key_data = data + + def load_key(self): + return self.key_data + def test_plaintext(self): key = PlaintextKey.create(None, None) chunk = Chunk(b'foo') @@ -128,6 +157,13 @@ class TestKey: key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata) assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata).data == b'payload' + def test_keyfile_blake2(self, monkeypatch, keys_dir): + with keys_dir.join('keyfile').open('w') as fd: + fd.write(self.keyfile_blake2_key_file) + monkeypatch.setenv('BORG_PASSPHRASE', 'passphrase') + key = Blake2KeyfileKey.detect(self.MockRepository(), self.keyfile_blake2_cdata) + assert key.decrypt(self.keyfile_blake2_id, self.keyfile_blake2_cdata).data == b'payload' + def test_passphrase(self, keys_dir, monkeypatch): monkeypatch.setenv('BORG_PASSPHRASE', 'test') key = PassphraseKey.create(self.MockRepository(), None) @@ -193,6 +229,14 @@ class TestKey: with pytest.raises(IntegrityError): key.assert_id(id, plaintext_changed) + def test_authenticated_encrypt(self, monkeypatch): + monkeypatch.setenv('BORG_PASSPHRASE', 'test') + key = AuthenticatedKey.create(self.MockRepository(), self.MockArgs()) + plaintext = Chunk(b'123456789') + authenticated = key.encrypt(plaintext) + # 0x06 is the key TYPE, 0x0000 identifies CNONE compression + assert authenticated == b'\x06\x00\x00' + plaintext.data + class TestPassphrase: def test_passphrase_new_verification(self, capsys, monkeypatch): From 76c93bb80bd204f9347400d919da6e9c197e3e14 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 10 Nov 2016 11:45:40 +0100 Subject: [PATCH 0361/1387] crypto blake2: don't release the GIL during key hashing --- src/borg/blake2/openssl-b2.c | 0 src/borg/crypto.pyx | 7 +++++-- 2 files changed, 5 insertions(+), 2 deletions(-) delete mode 100644 src/borg/blake2/openssl-b2.c diff --git a/src/borg/blake2/openssl-b2.c b/src/borg/blake2/openssl-b2.c deleted file mode 100644 index e69de29b..00000000 diff --git a/src/borg/crypto.pyx b/src/borg/crypto.pyx index bd61955e..eefd0a23 100644 --- a/src/borg/crypto.pyx +++ b/src/borg/crypto.pyx @@ -218,7 +218,7 @@ cdef blake2b_update_from_buffer(blake2b_state *state, obj): with nogil: rc = blake2b_update(state, buf.buf, buf.len) if rc == -1: - raise Exception('blake2b_update(key) failed') + raise Exception('blake2b_update() failed') finally: PyBuffer_Release(&buf) @@ -230,13 +230,16 @@ def blake2b_256(key, data): md = bytes(32) cdef unsigned char *md_ptr = md + cdef unsigned char *key_ptr = key # This is secure, because BLAKE2 is not vulnerable to length-extension attacks (unlike SHA-1/2, MD-5 and others). # See the BLAKE2 paper section 2.9 "Keyed hashing (MAC and PRF)" for details. # A nice benefit is that this simpler prefix-MAC mode has less overhead than the more complex HMAC mode. # We don't use the BLAKE2 parameter block (via blake2s_init_key) for this to # avoid incompatibility with the limited API of OpenSSL. - blake2b_update_from_buffer(&state, key) + rc = blake2b_update(&state, key_ptr, len(key)) + if rc == -1: + raise Exception('blake2b_update() failed') blake2b_update_from_buffer(&state, data) rc = blake2b_final(&state, md_ptr, 32) From 05ce8e0ff96b0d283cedfc0058a06234bb4d0630 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 10 Nov 2016 12:43:30 +0100 Subject: [PATCH 0362/1387] Add test script for blake2b_256 against CPython 3.6 hashlib --- scripts/py36-blake2.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 scripts/py36-blake2.py diff --git a/scripts/py36-blake2.py b/scripts/py36-blake2.py new file mode 100644 index 00000000..758a4f34 --- /dev/null +++ b/scripts/py36-blake2.py @@ -0,0 +1,36 @@ + +""" +This script checks compatibility of crypto.blake2b_256 against hashlib.blake2b in CPython 3.6. +""" + +import hashlib +import sys + + +def test_b2(b2_input, b2_output): + digest = hashlib.blake2b(b2_input, digest_size=32).digest() + identical = b2_output == digest + + print('Input: ', b2_input.hex()) + print('Expected: ', b2_output.hex()) + print('Calculated:', digest.hex()) + print('Identical: ', identical) + print() + if not identical: + sys.exit(1) + + +test_b2( + bytes.fromhex('037fb9b75b20d623f1d5a568050fccde4a1b7c5f5047432925e941a17c7a2d0d7061796c6f6164'), + bytes.fromhex('a22d4fc81bb61c3846c334a09eaf28d22dd7df08c9a7a41e713ef28d80eebd45') +) + +test_b2( + b'abc', + bytes.fromhex('bddd813c634239723171ef3fee98579b94964e3bb1cb3e427262c8c068d52319') +) + +test_b2( + bytes.fromhex('e944973af2256d4d670c12dd75304c319f58f4e40df6fb18ef996cb47e063676') + b'1234567890' * 100, + bytes.fromhex('97ede832378531dd0f4c668685d166e797da27b47d8cd441e885b60abd5e0cb2'), +) From a31ca4f022b76bcb4d679bf9be86aa9d0236bdd5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 10 Nov 2016 22:14:09 +0100 Subject: [PATCH 0363/1387] crypto: link against system libb2, if possible --- docs/global.rst.inc | 1 + docs/installation.rst | 1 + docs/usage.rst | 3 +++ setup.py | 26 ++++++++++++++++++++++++-- src/borg/blake2-libselect.h | 5 +++++ src/borg/crypto.pyx | 2 +- 6 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src/borg/blake2-libselect.h diff --git a/docs/global.rst.inc b/docs/global.rst.inc index d34f0965..88c659a3 100644 --- a/docs/global.rst.inc +++ b/docs/global.rst.inc @@ -15,6 +15,7 @@ .. _libacl: https://savannah.nongnu.org/projects/acl/ .. _libattr: https://savannah.nongnu.org/projects/attr/ .. _liblz4: https://github.com/Cyan4973/lz4 +.. _libb2: https://github.com/BLAKE2/libb2 .. _OpenSSL: https://www.openssl.org/ .. _`Python 3`: https://www.python.org/ .. _Buzhash: https://en.wikipedia.org/wiki/Buzhash diff --git a/docs/installation.rst b/docs/installation.rst index 44ba02e3..abd700c2 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -210,6 +210,7 @@ following dependencies first: * some Python dependencies, pip will automatically install them for you * optionally, the llfuse_ Python package is required if you wish to mount an archive as a FUSE filesystem. See setup.py about the version requirements. +* optionally libb2_. If it is not found a bundled implementation is used instead. If you have troubles finding the right package names, have a look at the distribution specific sections below and also at the Vagrantfile in our repo. diff --git a/docs/usage.rst b/docs/usage.rst index b05b81c4..bfeb6ef0 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -194,6 +194,9 @@ Building: Adds given OpenSSL header file directory to the default locations (setup.py). BORG_LZ4_PREFIX Adds given LZ4 header file directory to the default locations (setup.py). + BORG_LIBB2_PREFIX + Adds given prefix directory to the default locations. If a 'include/blake2.h' is found Borg + will be linked against the system libb2 instead of a bundled implementation. (setup.py) Please note: diff --git a/setup.py b/setup.py index 02dd1886..92588ef7 100644 --- a/setup.py +++ b/setup.py @@ -128,8 +128,18 @@ def detect_lz4(prefixes): return prefix +def detect_libb2(prefixes): + for prefix in prefixes: + filename = os.path.join(prefix, 'include', 'blake2.h') + if os.path.exists(filename): + with open(filename, 'r') as fd: + if 'blake2b_init' in fd.read(): + return prefix + include_dirs = [] library_dirs = [] +define_macros = [] +crypto_libraries = ['crypto'] possible_openssl_prefixes = ['/usr', '/usr/local', '/usr/local/opt/openssl', '/usr/local/ssl', '/usr/local/openssl', '/usr/local/borg', '/opt/local', '/opt/pkg', ] @@ -153,6 +163,18 @@ if lz4_prefix: elif not on_rtd: raise Exception('Unable to find LZ4 headers. (Looked here: {})'.format(', '.join(possible_lz4_prefixes))) +possible_libb2_prefixes = ['/usr', '/usr/local', '/usr/local/opt/libb2', '/usr/local/libb2', + '/usr/local/borg', '/opt/local', '/opt/pkg', ] +if os.environ.get('BORG_LIBB2_PREFIX'): + possible_libb2_prefixes.insert(0, os.environ.get('BORG_LIBB2_PREFIX')) +libb2_prefix = detect_libb2(possible_libb2_prefixes) +if libb2_prefix: + print('Detected and preferring libb2 over bundled BLAKE2') + include_dirs.append(os.path.join(libb2_prefix, 'include')) + library_dirs.append(os.path.join(libb2_prefix, 'lib')) + crypto_libraries.append('b2') + define_macros.append(('BORG_USE_LIBB2', 'YES')) + with open('README.rst', 'r') as fd: long_description = fd.read() @@ -326,8 +348,8 @@ cmdclass = { ext_modules = [] if not on_rtd: ext_modules += [ - Extension('borg.compress', [compress_source], libraries=['lz4'], include_dirs=include_dirs, library_dirs=library_dirs), - Extension('borg.crypto', [crypto_source], libraries=['crypto'], include_dirs=include_dirs, library_dirs=library_dirs), + Extension('borg.compress', [compress_source], libraries=['lz4'], include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), + Extension('borg.crypto', [crypto_source], libraries=crypto_libraries, include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), Extension('borg.chunker', [chunker_source]), Extension('borg.hashindex', [hashindex_source]) ] diff --git a/src/borg/blake2-libselect.h b/src/borg/blake2-libselect.h new file mode 100644 index 00000000..5486400e --- /dev/null +++ b/src/borg/blake2-libselect.h @@ -0,0 +1,5 @@ +#ifdef BORG_USE_LIBB2 +#include +#else +#include "blake2/blake2b-ref.c" +#endif diff --git a/src/borg/crypto.pyx b/src/borg/crypto.pyx index eefd0a23..7fa8b891 100644 --- a/src/borg/crypto.pyx +++ b/src/borg/crypto.pyx @@ -6,7 +6,7 @@ from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release API_VERSION = 3 -cdef extern from "blake2/blake2b-ref.c": +cdef extern from "blake2-libselect.h": ctypedef struct blake2b_state: pass From 5b31a2cd61eeea6714061d9ef58bb97c7fb3d574 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 11 Nov 2016 16:17:51 +0100 Subject: [PATCH 0364/1387] borg init: fix free space check crashing if disk is full --- src/borg/repository.py | 8 +++++++- src/borg/testsuite/repository.py | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 89374d6f..fce6d068 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -120,6 +120,7 @@ class Repository: self.lock_wait = lock_wait self.do_lock = lock self.do_create = create + self.created = False self.exclusive = exclusive self.append_only = append_only self.hostname_is_unique = yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', env_msg=None, prompt=False) @@ -138,6 +139,7 @@ class Repository: if self.do_create: self.do_create = False self.create(self.path) + self.created = True self.open(self.path, bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock) return self @@ -437,7 +439,11 @@ class Repository: free_space = st_vfs.f_bavail * st_vfs.f_bsize logger.debug('check_free_space: required bytes {}, free bytes {}'.format(required_free_space, free_space)) if free_space < required_free_space: - self._rollback(cleanup=True) + if self.created: + logger.error('Not enough free space to initialize repository at this location.') + self.destroy() + else: + self._rollback(cleanup=True) formatted_required = format_file_size(required_free_space) formatted_free = format_file_size(free_space) raise self.InsufficientFreeSpaceError(formatted_required, formatted_free) diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index a4683a62..58472371 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -407,6 +407,12 @@ class RepositoryFreeSpaceTestCase(RepositoryTestCaseBase): with pytest.raises(Repository.InsufficientFreeSpaceError): self.repository.commit() + def test_create_free_space(self): + self.repository.additional_free_space = 1e20 + with pytest.raises(Repository.InsufficientFreeSpaceError): + self.add_keys() + assert not os.path.exists(self.repository.path) + class NonceReservation(RepositoryTestCaseBase): def test_get_free_nonce_asserts(self): From 6c955579a83c8cec6962c8e5344dd45677fe654d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 11 Nov 2016 16:39:11 +0100 Subject: [PATCH 0365/1387] repository: test_additional_free_space, assert it still exists --- src/borg/testsuite/repository.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 58472371..6d6f00a7 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -406,6 +406,7 @@ class RepositoryFreeSpaceTestCase(RepositoryTestCaseBase): self.repository.put(H(0), b'foobar') with pytest.raises(Repository.InsufficientFreeSpaceError): self.repository.commit() + assert os.path.exists(self.repository.path) def test_create_free_space(self): self.repository.additional_free_space = 1e20 From ac5f9b61ecc41cf82051c51da74f74085111145b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 11 Nov 2016 19:54:05 +0100 Subject: [PATCH 0366/1387] debug, key: enable --help A small merge mishap in 2a864be8; 1.0-maint has it correctly. --- src/borg/archiver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 4999440a..6f3fa61d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1721,7 +1721,7 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False)) - subparser = subparsers.add_parser('key', add_help=False, + subparser = subparsers.add_parser('key', add_help=True, description="Manage a keyfile or repokey of a repository", epilog="", formatter_class=argparse.RawDescriptionHelpFormatter, @@ -2491,7 +2491,7 @@ class Archiver: in case you ever run into some severe malfunction. Use them only if you know what you are doing or if a trusted developer tells you what to do.""") - subparser = subparsers.add_parser('debug', add_help=False, + subparser = subparsers.add_parser('debug', add_help=True, description='debugging command (not intended for normal use)', epilog=debug_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, From bd4a0fe23b50ddb1e5383255f301db06208d7972 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 11 Nov 2016 21:24:16 +0100 Subject: [PATCH 0367/1387] Clarify cache/repository README file --- src/borg/cache.py | 3 ++- src/borg/constants.py | 9 +++++++++ src/borg/repository.py | 2 +- src/borg/upgrader.py | 3 ++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 3ed33d74..e2ce3651 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -10,6 +10,7 @@ import msgpack from .logger import create_logger logger = create_logger() +from .constants import CACHE_README from .hashindex import ChunkIndex, ChunkIndexEntry from .helpers import Location from .helpers import Error @@ -151,7 +152,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" """ os.makedirs(self.path) with open(os.path.join(self.path, 'README'), 'w') as fd: - fd.write('This is a Borg cache') + fd.write(CACHE_README) config = configparser.ConfigParser(interpolation=None) config.add_section('cache') config.set('cache', 'version', '1') diff --git a/src/borg/constants.py b/src/borg/constants.py index d6f26d11..27ad8c29 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -50,3 +50,12 @@ EXIT_ERROR = 2 # terminated abruptly, did not reach end of operation DASHES = '-' * 78 PBKDF2_ITERATIONS = 100000 + + +REPOSITORY_README = """This is a Borg Backup repository. +See https://borgbackup.readthedocs.io/ +""" + +CACHE_README = """This is a Borg Backup cache. +See https://borgbackup.readthedocs.io/ +""" diff --git a/src/borg/repository.py b/src/borg/repository.py index 89374d6f..a4366f77 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -166,7 +166,7 @@ class Repository: if not os.path.exists(path): os.mkdir(path) with open(os.path.join(path, 'README'), 'w') as fd: - fd.write('This is a Borg repository\n') + fd.write(REPOSITORY_README) os.mkdir(os.path.join(path, 'data')) config = ConfigParser(interpolation=None) config.add_section('repository') diff --git a/src/borg/upgrader.py b/src/borg/upgrader.py index 69712832..78da849a 100644 --- a/src/borg/upgrader.py +++ b/src/borg/upgrader.py @@ -6,6 +6,7 @@ import time import logging logger = logging.getLogger(__name__) +from .constants import REPOSITORY_README from .helpers import get_home_dir, get_keys_dir, get_cache_dir from .helpers import ProgressIndicatorPercent from .key import KeyfileKey, KeyfileNotFoundError @@ -64,7 +65,7 @@ class AtticRepositoryUpgrader(Repository): readme = os.path.join(self.path, 'README') os.remove(readme) with open(readme, 'w') as fd: - fd.write('This is a Borg repository\n') + fd.write(REPOSITORY_README) @staticmethod def convert_segments(segments, dryrun=True, inplace=False, progress=False): From 00fae3f3e3daca23ea227dc49fae255bbca76c1c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 12 Nov 2016 14:42:11 +0100 Subject: [PATCH 0368/1387] repository: check_free_space, special case small repos --- src/borg/repository.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 40ebb64d..08aea535 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -428,8 +428,18 @@ class Repository: required_free_space += self.additional_free_space if not self.append_only: - # Keep one full worst-case segment free in non-append-only mode - required_free_space += self.max_segment_size + MAX_OBJECT_SIZE + full_segment_size = self.max_segment_size + MAX_OBJECT_SIZE + if len(self.compact) < 10: + # This is mostly for the test suite to avoid overestimated free space needs. This can be annoying + # if TMP is a small-ish tmpfs. + compact_working_space = sum(self.io.segment_size(segment) - free for segment, free in self.compact.items()) + logger.debug('check_free_space: few segments, not requiring a full free segment') + compact_working_space = min(compact_working_space, full_segment_size) + logger.debug('check_free_space: calculated working space for compact as %d bytes', compact_working_space) + required_free_space += compact_working_space + else: + # Keep one full worst-case segment free in non-append-only mode + required_free_space += full_segment_size try: st_vfs = os.statvfs(self.path) except OSError as os_error: From cd8dfda31873bc3528a43f82e932f5bcfa77120f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 12 Nov 2016 22:55:05 +0100 Subject: [PATCH 0369/1387] caskroom osxfuse-beta gone, it's osxfuse now (3.5.3) --- .travis/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis/install.sh b/.travis/install.sh index 64ccd5a2..92f14e21 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -17,7 +17,7 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then brew install lz4 brew outdated pyenv || brew upgrade pyenv brew install pkg-config - brew install Caskroom/versions/osxfuse-beta + brew install Caskroom/versions/osxfuse case "${TOXENV}" in py34) From 3237db46213bd4e6e93144580fa826df9cc73f15 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 12 Nov 2016 23:10:40 +0100 Subject: [PATCH 0370/1387] at xattr module import time, loggers are not initialized yet --- borg/xattr.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/borg/xattr.py b/borg/xattr.py index 10f99ae6..50d1b0b8 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -12,9 +12,6 @@ from distutils.version import LooseVersion from .helpers import Buffer -from .logger import create_logger -logger = create_logger() - try: ENOATTR = errno.ENOATTR @@ -68,7 +65,7 @@ if libc_name is None: libc_name = 'libc.dylib' else: msg = "Can't find C library. No fallback known. Try installing ldconfig, gcc/cc or objdump." - logger.error(msg) + print(msg, file=sys.stderr) # logger isn't initialized at this stage raise Exception(msg) # If we are running with fakeroot on Linux, then use the xattr functions of fakeroot. This is needed by From 960c42193acdab214217dd8a472f7f28d7ca5153 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 13 Nov 2016 11:19:58 +0100 Subject: [PATCH 0371/1387] fix tox build for environment-python != containing-python in yet-another instance this instance: the repository worktree is *not* named borg. Cherry pick of 4f1157c into 1.0-maint due to f3efcdb TODO removed since we already did that after 1.0-maint, but 1.0-maint will never receive the change. --- conftest.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/conftest.py b/conftest.py index d80aed4c..d5423660 100644 --- a/conftest.py +++ b/conftest.py @@ -1,4 +1,5 @@ import os +import os.path import sys import pytest @@ -10,12 +11,10 @@ import pytest # The workaround is to remove entries pointing there from the path and check whether "borg" # is still importable. If it is not, then it has not been installed in the environment # and the entries are put back. -# -# TODO: After moving the package to src/: remove this. original_path = list(sys.path) for entry in original_path: - if entry == '' or entry.endswith('/borg'): + if entry == '' or entry == os.path.dirname(__file__): sys.path.remove(entry) try: From 64a3fa8e737a35f3044e8bf654e932fe0bd84d96 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 13 Nov 2016 11:40:19 +0100 Subject: [PATCH 0372/1387] check: bail out early if repository is *completely* empty --- borg/repository.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/borg/repository.py b/borg/repository.py index fa6458c6..690e7770 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -438,6 +438,9 @@ class Repository: transaction_id = self.get_index_transaction_id() if transaction_id is None: transaction_id = self.io.get_latest_segment() + if transaction_id is None: + report_error('This repository contains no valid data.') + return False if repair: self.io.cleanup(transaction_id) segments_transaction_id = self.io.get_segments_transaction_id() From 2261709e78b34330ed439a64001cc1bb661828cf Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 13 Nov 2016 11:45:35 +0100 Subject: [PATCH 0373/1387] check: handle repo w/o objects gracefully normal check would complete, --repair would crash when trying to write the rebuilt (empty) manifest out, since self.key was None --- borg/archive.py | 3 +++ borg/testsuite/archiver.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/borg/archive.py b/borg/archive.py index e725857e..654a1da9 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -833,6 +833,9 @@ class ArchiveChecker: self.repair = repair self.repository = repository self.init_chunks() + if not self.chunks: + logger.error('Repository contains no apparent data at all, cannot continue check/repair.') + return False self.key = self.identify_key(repository) if Manifest.MANIFEST_ID not in self.chunks: logger.error("Repository manifest not found!") diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index bc49bc66..b50304b5 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1444,6 +1444,13 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.cmd('check', self.repository_location, exit_code=0) self.cmd('extract', '--dry-run', self.repository_location + '::archive1', exit_code=0) + def test_empty_repository(self): + with Repository(self.repository_location, exclusive=True) as repository: + for id_ in repository.list(): + repository.delete(id_) + repository.commit() + self.cmd('check', self.repository_location, exit_code=1) + @pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteArchiverTestCase(ArchiverTestCase): From 639eba16354008ac7d97d966084eac2fb930a262 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 13 Nov 2016 15:22:51 +0100 Subject: [PATCH 0374/1387] Fix check incorrectly reporting attic 0.13 and earlier archives as corrupt --- borg/archive.py | 3 +++ borg/testsuite/archiver.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/borg/archive.py b/borg/archive.py index 654a1da9..b2f9df70 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -1020,6 +1020,9 @@ class ArchiveChecker: def valid_item(obj): if not isinstance(obj, StableDict): return False + # A bug in Attic up to and including release 0.13 added a (meaningless) b'acl' key to every item. + # We ignore it here, should it exist. See test_attic013_acl_bug for details. + obj.pop(b'acl', None) keys = set(obj) return REQUIRED_ITEM_KEYS.issubset(keys) and keys.issubset(item_keys) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index b50304b5..a7d8ff3e 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1451,6 +1451,25 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): repository.commit() self.cmd('check', self.repository_location, exit_code=1) + def test_attic013_acl_bug(self): + # Attic up to release 0.13 contained a bug where every item unintentionally received + # a b'acl'=None key-value pair. + # This bug can still live on in Borg repositories (through borg upgrade). + archive, repository = self.open_archive('archive1') + with repository: + manifest, key = Manifest.load(repository) + with Cache(repository, key, manifest) as cache: + archive = Archive(repository, key, manifest, '0.13', cache=cache, create=True) + archive.items_buffer.add({ + # path and mtime are required. + b'path': '1234', + b'mtime': 0, + # acl is the offending key. + b'acl': None + }) + archive.save() + self.cmd('check', self.repository_location, exit_code=0) + @pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteArchiverTestCase(ArchiverTestCase): From a898297669213fb95eab4f71a838121993775e53 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 13 Nov 2016 02:00:39 +0100 Subject: [PATCH 0375/1387] check: improve "did not get expected metadata dict" diagnostic --- borg/archive.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index b2f9df70..da8a0e70 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -1017,14 +1017,21 @@ class ArchiveChecker: self.error_found = True logger.error(msg) + def list_keys_safe(keys): + return ', '.join((k.decode() if isinstance(k, bytes) else str(k) for k in keys)) + def valid_item(obj): if not isinstance(obj, StableDict): - return False + return False, 'not a dictionary' # A bug in Attic up to and including release 0.13 added a (meaningless) b'acl' key to every item. # We ignore it here, should it exist. See test_attic013_acl_bug for details. obj.pop(b'acl', None) keys = set(obj) - return REQUIRED_ITEM_KEYS.issubset(keys) and keys.issubset(item_keys) + if not REQUIRED_ITEM_KEYS.issubset(keys): + return False, 'missing required keys: ' + list_keys_safe(REQUIRED_ITEM_KEYS - keys) + if not keys.issubset(item_keys): + return False, 'invalid keys: ' + list_keys_safe(keys - item_keys) + return True, '' i = 0 for state, items in groupby(archive[b'items'], missing_chunk_detector): @@ -1040,10 +1047,11 @@ class ArchiveChecker: unpacker.feed(self.key.decrypt(chunk_id, cdata)) try: for item in unpacker: - if valid_item(item): + valid, reason = valid_item(item) + if valid: yield item else: - report('Did not get expected metadata dict when unpacking item metadata', chunk_id, i) + report('Did not get expected metadata dict when unpacking item metadata (%s)' % reason, chunk_id, i) except RobustUnpacker.UnpackerCrashed as err: report('Unpacker crashed while unpacking item metadata, trying to resync...', chunk_id, i) unpacker.resync() From c2eb2539b96336e39c3eed486d3c3a11dae429ee Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 13 Nov 2016 16:07:01 +0100 Subject: [PATCH 0376/1387] Document maintenance merge command --- docs/development.rst | 10 ++++++++++ src/borg/item.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/development.rst b/docs/development.rst index 8fa3ba20..50e4f059 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -214,6 +214,16 @@ If you encounter issues, see also our `Vagrantfile` for details. without external dependencies. +Merging maintenance branches +---------------------------- + +As mentioned above bug fixes will usually be merged into a maintenance branch (x.y-maint) and then +merged back into the master branch. Large diffs between these branches can make automatic merges troublesome, +therefore we recommend to use these merge parameters:: + + git merge 1.0-maint -s recursive -X rename-threshold=20% + + Creating a new release ---------------------- diff --git a/src/borg/item.py b/src/borg/item.py index 84d5085e..e44e4367 100644 --- a/src/borg/item.py +++ b/src/borg/item.py @@ -20,7 +20,7 @@ class PropDict: When "packing" a dict, ie. you have a dict with some data and want to convert it into an instance, then use eg. Item({'a': 1, ...}). This way all keys in your dictionary are validated. - When "unpacking", that is you've read a dictionary with some data from somewhere (eg mspack), + When "unpacking", that is you've read a dictionary with some data from somewhere (eg. msgpack), then use eg. Item(internal_dict={...}). This does not validate the keys, therefore unknown keys are ignored instead of causing an error. """ From dfd748689c459d648f7861c1cf8bc2ea1044f941 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 13 Nov 2016 17:30:42 +0100 Subject: [PATCH 0377/1387] test_attic013_acl_bug: use def instead of lambda --- src/borg/testsuite/archiver.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 49008650..d59d05f1 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2166,18 +2166,19 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): # a b'acl'=None key-value pair. # This bug can still live on in Borg repositories (through borg upgrade). class Attic013Item: - as_dict = lambda: { - # These are required - b'path': '1234', - b'mtime': 0, - b'mode': 0, - b'user': b'0', - b'group': b'0', - b'uid': 0, - b'gid': 0, - # acl is the offending key. - b'acl': None, - } + def as_dict(): + return { + # These are required + b'path': '1234', + b'mtime': 0, + b'mode': 0, + b'user': b'0', + b'group': b'0', + b'uid': 0, + b'gid': 0, + # acl is the offending key. + b'acl': None, + } archive, repository = self.open_archive('archive1') with repository: From b73786690587a77e93411d57fa9dcb64e38cd891 Mon Sep 17 00:00:00 2001 From: Abogical Date: Sun, 13 Nov 2016 23:34:15 +0200 Subject: [PATCH 0378/1387] Improve extract progress display and ProgressIndicatorPercent --- src/borg/archive.py | 12 ++++-------- src/borg/archiver.py | 4 +++- src/borg/helpers.py | 31 +++++++++++++++++++++++++++---- src/borg/testsuite/archiver.py | 2 +- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index e831be22..5cff0456 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -31,7 +31,7 @@ from .helpers import format_time, format_timedelta, format_file_size, file_statu from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates, swidth_slice from .helpers import decode_dict, StableDict from .helpers import int_to_bigint, bigint_to_int, bin_to_hex -from .helpers import ProgressIndicatorPercent, log_multi +from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi from .helpers import PathPrefixPattern, FnmatchPattern from .helpers import consume, chunkit from .helpers import CompressionDecider1, CompressionDecider2, CompressionSpec @@ -93,11 +93,7 @@ class Statistics: msg = '' space = columns - swidth(msg) if space >= 8: - if space < swidth('...') + swidth(path): - path = '%s...%s' % (swidth_slice(path, space // 2 - swidth('...')), - swidth_slice(path, -space // 2)) - space -= swidth(path) - msg += path + ' ' * space + msg += ellipsis_truncate(path, space) else: msg = ' ' * columns print(msg, file=stream or sys.stderr, end="\r", flush=True) @@ -448,7 +444,7 @@ Number of files: {0.stats.nfiles}'''.format( if 'chunks' in item: for _, data in self.pipeline.fetch_many([c.id for c in item.chunks], is_preloaded=True): if pi: - pi.show(increase=len(data)) + pi.show(increase=len(data), info=[remove_surrogates(item.path)]) if stdout: sys.stdout.buffer.write(data) if stdout: @@ -501,7 +497,7 @@ Number of files: {0.stats.nfiles}'''.format( ids = [c.id for c in item.chunks] for _, data in self.pipeline.fetch_many(ids, is_preloaded=True): if pi: - pi.show(increase=len(data)) + pi.show(increase=len(data), info=[remove_surrogates(item.path)]) with backup_io(): if sparse and self.zeros.startswith(data): # all-zero chunk: create a hole in a sparse file diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 6f3fa61d..00db79b3 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -501,7 +501,7 @@ class Archiver: filter = self.build_filter(matcher, peek_and_store_hardlink_masters, strip_components) if progress: - pi = ProgressIndicatorPercent(msg='Extracting files %5.1f%%', step=0.1) + pi = ProgressIndicatorPercent(msg='%5.1f%% Extracting: %s', step=0.1) pi.output('Calculating size') extracted_size = sum(item.file_size(hardlink_masters) for item in archive.iter_items(filter)) pi.total = extracted_size @@ -546,6 +546,8 @@ class Archiver: for pattern in include_patterns: if pattern.match_count == 0: self.print_warning("Include pattern '%s' never matched.", pattern) + if pi: + pi.finish() return self.exit_code @with_repository() diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 0b322685..a0562b13 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -26,6 +26,7 @@ from functools import wraps, partial, lru_cache from itertools import islice from operator import attrgetter from string import Formatter +from shutil import get_terminal_size import msgpack import msgpack.fallback @@ -1191,6 +1192,20 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, env_var_override = None +def ellipsis_truncate(msg, space): + from .platform import swidth + """ + shorten a long string by adding ellipsis between it and return it, example: + this_is_a_very_long_string -------> this_is..._string + """ + ellipsis_width = swidth('...') + msg_width = swidth(msg) + if space < ellipsis_width + msg_width: + return '%s...%s' % (swidth_slice(msg, space // 2 - ellipsis_width), + swidth_slice(msg, -space // 2)) + return msg + ' ' * (space - msg_width) + + class ProgressIndicatorPercent: LOGGER = 'borg.output.progress' @@ -1208,7 +1223,6 @@ class ProgressIndicatorPercent: self.trigger_at = start # output next percentage value when reaching (at least) this self.step = step self.msg = msg - self.output_len = len(self.msg % 100.0) self.handler = None self.logger = logging.getLogger(self.LOGGER) @@ -1239,14 +1253,23 @@ class ProgressIndicatorPercent: self.trigger_at += self.step return pct - def show(self, current=None, increase=1): + def show(self, current=None, increase=1, info=[]): pct = self.progress(current, increase) if pct is not None: + # truncate the last argument, if space is available + if info != []: + msg = self.msg % (pct, *info[:-1], '') + space = get_terminal_size()[0] - len(msg) + if space < 8: + info[-1] = '' + else: + info[-1] = ellipsis_truncate(info[-1], space) + return self.output(self.msg % (pct, *info)) + return self.output(self.msg % pct) def output(self, message): - self.output_len = max(len(message), self.output_len) - message = message.ljust(self.output_len) + message = message.ljust(get_terminal_size(fallback=(4, 1))[0]) self.logger.info(message) def finish(self): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index a978fa0f..d720af46 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -774,7 +774,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with changedir('output'): output = self.cmd('extract', self.repository_location + '::test', '--progress') - assert 'Extracting files' in output + assert 'Extracting:' in output def _create_test_caches(self): self.cmd('init', self.repository_location) From 467fe38b1589bb9bc7b33c3e68eb82498f18fade Mon Sep 17 00:00:00 2001 From: Abogical Date: Mon, 14 Nov 2016 00:05:42 +0200 Subject: [PATCH 0379/1387] Compatibility with python 3.4 --- src/borg/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index a0562b13..e70db723 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1258,13 +1258,13 @@ class ProgressIndicatorPercent: if pct is not None: # truncate the last argument, if space is available if info != []: - msg = self.msg % (pct, *info[:-1], '') + msg = self.msg % tuple([pct] + info[:-1] + ['']) space = get_terminal_size()[0] - len(msg) if space < 8: info[-1] = '' else: info[-1] = ellipsis_truncate(info[-1], space) - return self.output(self.msg % (pct, *info)) + return self.output(self.msg % tuple([pct] + info)) return self.output(self.msg % pct) From 1362d2e90f48465396212abbc4b006acac64f304 Mon Sep 17 00:00:00 2001 From: Abogical Date: Mon, 14 Nov 2016 00:45:54 +0200 Subject: [PATCH 0380/1387] Set COLUMNS & LINES as if it was a terminal --- src/borg/testsuite/helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index bd4865f0..db90b1fc 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -904,6 +904,8 @@ def test_yes_env_output(capfd, monkeypatch): def test_progress_percentage_sameline(capfd): + os.environ['COLUMNS'] = '4' + os.environ['LINES'] = '1' pi = ProgressIndicatorPercent(1000, step=5, start=0, msg="%3.0f%%") pi.logger.setLevel('INFO') pi.show(0) From 372ebb4e55e2443557d30ab0a23b7a9162dbaed1 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 14 Nov 2016 02:08:11 +0100 Subject: [PATCH 0381/1387] travis: allow OS X failures until the brew cask osxfuse issue is fixed --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index c46fc6f5..36c38d63 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,8 @@ matrix: os: osx osx_image: xcode6.4 env: TOXENV=py35 + allow_failures: + - os: osx install: - ./.travis/install.sh From c81f861ba661675859451f3fc60e8c9ac03a64db Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 5 Nov 2016 03:05:15 +0100 Subject: [PATCH 0382/1387] developers docs: clarify how to choose PR target branch --- docs/development.rst | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/development.rst b/docs/development.rst index 513bb4ec..a0bbb6cd 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -21,9 +21,16 @@ Some guidance for contributors: - choose the branch you base your changesets on wisely: - - choose x.y-maint for stuff that should go into next x.y release - (it usually gets merged into master branch later also) - - choose master if that does not apply + - choose x.y-maint for stuff that should go into next x.y.z release + (it usually gets merged into master branch later also), like: + + - bug fixes (code or docs) + - missing *important* (and preferably small) features + - docs rearrangements (so stuff stays in-sync to avoid merge + troubles in future) + - choose master if that does not apply, like for: + + - developping new features - do clean changesets: From 3232769ffc815241d2560ca71017ef9c9328f51a Mon Sep 17 00:00:00 2001 From: Abogical Date: Mon, 14 Nov 2016 11:43:46 +0200 Subject: [PATCH 0383/1387] Respond to feedback --- src/borg/helpers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index e70db723..f65ebc05 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1193,11 +1193,11 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, def ellipsis_truncate(msg, space): - from .platform import swidth """ shorten a long string by adding ellipsis between it and return it, example: this_is_a_very_long_string -------> this_is..._string """ + from .platform import swidth ellipsis_width = swidth('...') msg_width = swidth(msg) if space < ellipsis_width + msg_width: @@ -1253,11 +1253,11 @@ class ProgressIndicatorPercent: self.trigger_at += self.step return pct - def show(self, current=None, increase=1, info=[]): + def show(self, current=None, increase=1, info=None): pct = self.progress(current, increase) if pct is not None: - # truncate the last argument, if space is available - if info != []: + # truncate the last argument, if no space is available + if info is not None: msg = self.msg % tuple([pct] + info[:-1] + ['']) space = get_terminal_size()[0] - len(msg) if space < 8: From af925d272312ede6abe1a61e9e786a95bdfe359b Mon Sep 17 00:00:00 2001 From: Abogical Date: Mon, 14 Nov 2016 14:49:24 +0200 Subject: [PATCH 0384/1387] do not justify if we're not outputing to a terminal --- src/borg/helpers.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index f65ebc05..853d3625 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1258,18 +1258,24 @@ class ProgressIndicatorPercent: if pct is not None: # truncate the last argument, if no space is available if info is not None: - msg = self.msg % tuple([pct] + info[:-1] + ['']) - space = get_terminal_size()[0] - len(msg) - if space < 8: - info[-1] = '' - else: - info[-1] = ellipsis_truncate(info[-1], space) - return self.output(self.msg % tuple([pct] + info)) + # no need to truncate if we're not outputing to a terminal + terminal_space = get_terminal_size(fallback=(-1, -1))[0] + if terminal_space != -1: + space = terminal_space - len(self.msg % tuple([pct] + info[:-1] + [''])) + if space < 8: + info[-1] = ' ' * space + else: + info[-1] = ellipsis_truncate(info[-1], space) + return self.output(self.msg % tuple([pct] + info), justify=False) return self.output(self.msg % pct) - def output(self, message): - message = message.ljust(get_terminal_size(fallback=(4, 1))[0]) + def output(self, message, justify=True): + if justify: + terminal_space = get_terminal_size(fallback=(-1, -1))[0] + # no need to ljust if we're not outputing to a terminal + if terminal_space != -1: + message = message.ljust(terminal_space) self.logger.info(message) def finish(self): From 76638d0562794c571b085e775b2ed5442494073e Mon Sep 17 00:00:00 2001 From: Abogical Date: Mon, 14 Nov 2016 14:56:34 +0200 Subject: [PATCH 0385/1387] If there is a small space for ellipsis_truncate, show '...' only --- src/borg/helpers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 853d3625..f2dd59ad 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1200,6 +1200,8 @@ def ellipsis_truncate(msg, space): from .platform import swidth ellipsis_width = swidth('...') msg_width = swidth(msg) + if space < 8: + return '...' + ' ' * (space - ellipsis_width) if space < ellipsis_width + msg_width: return '%s...%s' % (swidth_slice(msg, space // 2 - ellipsis_width), swidth_slice(msg, -space // 2)) @@ -1262,10 +1264,7 @@ class ProgressIndicatorPercent: terminal_space = get_terminal_size(fallback=(-1, -1))[0] if terminal_space != -1: space = terminal_space - len(self.msg % tuple([pct] + info[:-1] + [''])) - if space < 8: - info[-1] = ' ' * space - else: - info[-1] = ellipsis_truncate(info[-1], space) + info[-1] = ellipsis_truncate(info[-1], space) return self.output(self.msg % tuple([pct] + info), justify=False) return self.output(self.msg % pct) From 3896f26ab24bbfaf47656ec582558076b08dfa96 Mon Sep 17 00:00:00 2001 From: Abogical Date: Mon, 14 Nov 2016 15:10:01 +0200 Subject: [PATCH 0386/1387] use monkeypatch --- src/borg/testsuite/helpers.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index db90b1fc..f41cb442 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -903,9 +903,10 @@ def test_yes_env_output(capfd, monkeypatch): assert 'yes' in err -def test_progress_percentage_sameline(capfd): - os.environ['COLUMNS'] = '4' - os.environ['LINES'] = '1' +def test_progress_percentage_sameline(capfd, monkeypatch): + # run the test as if it was in a 4x1 terminal + monkeypatch.setenv('COLUMNS', '4') + monkeypatch.setenv('LINES', '1') pi = ProgressIndicatorPercent(1000, step=5, start=0, msg="%3.0f%%") pi.logger.setLevel('INFO') pi.show(0) @@ -923,7 +924,10 @@ def test_progress_percentage_sameline(capfd): assert err == ' ' * 4 + '\r' -def test_progress_percentage_step(capfd): +def test_progress_percentage_step(capfd, monkeypatch): + # run the test as if it was in a 4x1 terminal + monkeypatch.setenv('COLUMNS', '4') + monkeypatch.setenv('LINES', '1') pi = ProgressIndicatorPercent(100, step=2, start=0, msg="%3.0f%%") pi.logger.setLevel('INFO') pi.show() From 34f529c7df63b52a30da8cb639d02c1d9004d810 Mon Sep 17 00:00:00 2001 From: Abogical Date: Mon, 14 Nov 2016 17:37:49 +0200 Subject: [PATCH 0387/1387] satisfy codecov --- src/borg/archiver.py | 1 + src/borg/helpers.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 00db79b3..5e764344 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -547,6 +547,7 @@ class Archiver: if pattern.match_count == 0: self.print_warning("Include pattern '%s' never matched.", pattern) if pi: + # clear progress output pi.finish() return self.exit_code diff --git a/src/borg/helpers.py b/src/borg/helpers.py index f2dd59ad..ab2e1271 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1201,6 +1201,7 @@ def ellipsis_truncate(msg, space): ellipsis_width = swidth('...') msg_width = swidth(msg) if space < 8: + # if there is very little space, just show ... return '...' + ' ' * (space - ellipsis_width) if space < ellipsis_width + msg_width: return '%s...%s' % (swidth_slice(msg, space // 2 - ellipsis_width), @@ -1256,6 +1257,13 @@ class ProgressIndicatorPercent: return pct def show(self, current=None, increase=1, info=None): + """ + Show and output the progress message + + :param current: set the current percentage [None] + :param increase: increase the current percentage [None] + :param info: array of strings to be formatted with msg [None] + """ pct = self.progress(current, increase) if pct is not None: # truncate the last argument, if no space is available From c380d918052574091a007cd07e571edc0469762e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 14 Nov 2016 21:50:20 +0100 Subject: [PATCH 0388/1387] fixes for flake 3.1.1 --- setup.py | 1 + src/borg/helpers.py | 2 ++ src/borg/platform/base.py | 1 + tox.ini | 2 +- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 92588ef7..64882457 100644 --- a/setup.py +++ b/setup.py @@ -136,6 +136,7 @@ def detect_libb2(prefixes): if 'blake2b_init' in fd.read(): return prefix + include_dirs = [] library_dirs = [] define_macros = [] diff --git a/src/borg/helpers.py b/src/borg/helpers.py index ab2e1271..0898a454 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -672,6 +672,7 @@ def replace_placeholders(text): } return format_line(text, data) + PrefixSpec = replace_placeholders @@ -1669,6 +1670,7 @@ def scandir_generic(path='.'): for name in sorted(os.listdir(path)): yield GenericDirEntry(path, name) + try: from os import scandir except ImportError: diff --git a/src/borg/platform/base.py b/src/borg/platform/base.py index 496a59b5..d024ce76 100644 --- a/src/borg/platform/base.py +++ b/src/borg/platform/base.py @@ -34,6 +34,7 @@ def acl_set(path, item, numeric_owner=False): of the user/group names """ + try: from os import lchflags diff --git a/tox.ini b/tox.ini index bcd560c5..5aa14a50 100644 --- a/tox.ini +++ b/tox.ini @@ -16,4 +16,4 @@ passenv = * [testenv:flake8] changedir = deps = flake8 -commands = flake8 +commands = flake8 src scripts conftest.py From 9cf4846c270a9420f4578bdb049f3860e4ba33b6 Mon Sep 17 00:00:00 2001 From: Abdel-Rahman Date: Tue, 15 Nov 2016 00:11:32 +0200 Subject: [PATCH 0389/1387] Typo --- docs/development.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development.rst b/docs/development.rst index a0bbb6cd..cf8e25bd 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -30,7 +30,7 @@ Some guidance for contributors: troubles in future) - choose master if that does not apply, like for: - - developping new features + - developing new features - do clean changesets: From c580d9c173162b0c632a70f94e3c1ca422ab5cce Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Thu, 10 Nov 2016 08:55:19 +0100 Subject: [PATCH 0390/1387] version: Add version parsing and formating --- src/borg/version.py | 49 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/borg/version.py diff --git a/src/borg/version.py b/src/borg/version.py new file mode 100644 index 00000000..4eb0c77d --- /dev/null +++ b/src/borg/version.py @@ -0,0 +1,49 @@ +import re + + +def parse_version(version): + """ + simplistic parser for setuptools_scm versions + + supports final versions and alpha ('a'), beta ('b') and rc versions. It just discards commits since last tag + and git revision hash. + + Output is a version tuple containing integers. It ends with one or two elements that ensure that relational + operators yield correct relations for alpha, beta and rc versions too. For final versions the last element + is a -1, for prerelease versions the last two elements are a smaller negative number and the number of e.g. + the beta. + + Note, this sorts version 1.0 before 1.0.0. + + This version format is part of the remote protocol, don‘t change in breaking ways. + """ + + parts = version.split('+')[0].split('.') + if parts[-1].startswith('dev'): + del parts[-1] + version = [int(segment) for segment in parts[:-1]] + + prerelease = re.fullmatch('([0-9]+)(a|b|rc)([0-9]+)', parts[-1]) + if prerelease: + version_type = {'a': -4, 'b': -3, 'rc': -2}[prerelease.group(2)] + version += [int(prerelease.group(1)), version_type, int(prerelease.group(3))] + else: + version += [int(parts[-1]), -1] + + return tuple(version) + + +def format_version(version): + """a reverse for parse_version (obviously without the dropped information)""" + f = [] + it = iter(version) + while True: + part = next(it) + if part >= 0: + f += str(part) + elif part == -1: + break + else: + f[-1] = f[-1] + {-2: 'rc', -3: 'b', -4: 'a'}[part] + str(next(it)) + break + return '.'.join(f) From 0da913f8ed9a3e4feea91c6894e25aa93e61a4c2 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Thu, 10 Nov 2016 09:44:00 +0100 Subject: [PATCH 0391/1387] remote: Decode method name as utf-8 instead of ascii for consistency. --- src/borg/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 6264241c..d94354ec 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -112,7 +112,7 @@ class RepositoryServer: # pragma: no cover self.repository.close() raise UnexpectedRPCDataFormatFromClient(__version__) type, msgid, method, args = unpacked - method = method.decode('ascii') + method = method.decode() try: if method not in self.rpc_methods: raise InvalidRPCMethod(method) From d25e9aa4f0ce9724e409e8ee4d525998d6cb5511 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Thu, 10 Nov 2016 09:41:33 +0100 Subject: [PATCH 0392/1387] remote: Use single quotes --- src/borg/remote.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index d94354ec..d326ce92 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -102,7 +102,7 @@ class RepositoryServer: # pragma: no cover if self.repository is not None: self.repository.close() else: - os.write(stderr_fd, "Borg {}: Got connection close before repository was opened.\n" + os.write(stderr_fd, 'Borg {}: Got connection close before repository was opened.\n' .format(__version__).encode()) return unpacker.feed(data) @@ -137,7 +137,7 @@ class RepositoryServer: # pragma: no cover tb = '%s\n%s' % (traceback.format_exc(), sysinfo()) logging.error(msg) logging.log(tb_log_level, tb) - exc = "Remote Exception (see remote log for the traceback)" + exc = 'Remote Exception (see remote log for the traceback)' os.write(stdout_fd, msgpack.packb((1, msgid, e.__class__.__name__, exc))) else: os.write(stdout_fd, msgpack.packb((1, msgid, None, res))) @@ -272,7 +272,7 @@ This TypeError is a cosmetic side effect of the compatibility code borg clients >= 1.0.7 have to support older borg servers. This problem will go away as soon as the server has been upgraded to 1.0.7+. """ - # emit this msg in the same way as the "Remote: ..." lines that show the remote TypeError + # emit this msg in the same way as the 'Remote: ...' lines that show the remote TypeError sys.stderr.write(msg) if append_only: raise self.NoAppendOnlyOnServer() @@ -283,10 +283,10 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. def __del__(self): if len(self.responses): - logging.debug("still %d cached responses left in RemoteRepository" % (len(self.responses),)) + logging.debug('still %d cached responses left in RemoteRepository' % (len(self.responses),)) if self.p: self.close() - assert False, "cleanup happened in Repository.__del__" + assert False, 'cleanup happened in Repository.__del__' def __repr__(self): return '<%s %s>' % (self.__class__.__name__, self.location.canonical_path()) @@ -310,7 +310,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. def borg_cmd(self, args, testing): """return a borg serve command line""" - # give some args/options to "borg serve" process as they were given to us + # give some args/options to 'borg serve' process as they were given to us opts = [] if args is not None: opts.append('--umask=%03o' % args.umask) @@ -430,7 +430,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. while not self.to_send and (calls or self.preload_ids) and len(waiting_for) < MAX_INFLIGHT: if calls: if is_preloaded: - assert cmd == "get", "is_preload is only supported for 'get'" + assert cmd == 'get', "is_preload is only supported for 'get'" if calls[0][0] in self.chunkid_to_msgids: waiting_for.append(pop_preload_msgid(calls.pop(0)[0])) else: @@ -531,7 +531,7 @@ def handle_remote_line(line): logname, msg = msg.split(' ', 1) logging.getLogger(logname).log(level, msg.rstrip()) else: - sys.stderr.write("Remote: " + line) + sys.stderr.write('Remote: ' + line) class RepositoryNoCache: From 6c1b337ce2ac0c0b058608f252f33165fb23fdfc Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Thu, 10 Nov 2016 10:04:15 +0100 Subject: [PATCH 0393/1387] remote: Replace broken exception argument restoration with code that uses a fixed value. --- src/borg/remote.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index d326ce92..b071a07b 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -371,13 +371,13 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. elif error == 'CheckNeeded': raise Repository.CheckNeeded(self.location.orig) elif error == 'IntegrityError': - raise IntegrityError(res) + raise IntegrityError('(not available)') elif error == 'PathNotAllowed': - raise PathNotAllowed(*res) + raise PathNotAllowed() elif error == 'ObjectNotFound': - raise Repository.ObjectNotFound(res[0], self.location.orig) + raise Repository.ObjectNotFound('(not available)', self.location.orig) elif error == 'InvalidRPCMethod': - raise InvalidRPCMethod(*res) + raise InvalidRPCMethod('(not available)') else: raise self.RPCError(res.decode('utf-8'), error) From ba553ec628b84cfa2f2f92898627b711244ea1d6 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Thu, 10 Nov 2016 09:56:18 +0100 Subject: [PATCH 0394/1387] remote: Introduce rpc protocol with named parameters. --- src/borg/remote.py | 286 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 236 insertions(+), 50 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index b071a07b..b4e1b3d2 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -1,5 +1,7 @@ import errno import fcntl +import functools +import inspect import logging import os import select @@ -20,8 +22,11 @@ from .helpers import bin_to_hex from .helpers import replace_placeholders from .helpers import yes from .repository import Repository +from .version import parse_version, format_version RPC_PROTOCOL_VERSION = 2 +BORG_VERSION = parse_version(__version__) +MSGID, MSG, ARGS, RESULT = b'i', b'm', b'a', b'r' BUFSIZE = 10 * 1024 * 1024 @@ -54,6 +59,51 @@ class UnexpectedRPCDataFormatFromServer(Error): """Got unexpected RPC data format from server.""" +# Protocol compatibility: +# In general the server is responsible for rejecting too old clients and the client it responsible for rejecting +# too old servers. This ensures that the knowledge what is compatible is always held by the newer component. +# +# The server can do checks for the client version in RepositoryServer.negotiate. If the client_data is 2 then +# client is in the version range [0.29.0, 1.0.x] inclusive. For newer clients client_data is a dict which contains +# client_version. +# +# For the client the return of the negotiate method is either 2 if the server is in the version range [0.29.0, 1.0.x] +# inclusive, or it is a dict which includes the server version. +# +# All method calls on the remote repository object must be whitelisted in RepositoryServer.rpc_methods and have api +# stubs in RemoteRepository. The @api decorator on these stubs is used to set server version requirements. +# +# Method parameters are identified only by name and never by position. Unknown parameters are ignored by the server side. +# If a new parameter is important and may not be ignored, on the client a parameter specific version requirement needs +# to be added. +# When parameters are removed, they need to be preserved as defaulted parameters on the client stubs so that older +# servers still get compatible input. + + +compatMap = { + 'check': ('repair', 'save_space', ), + 'commit': ('save_space', ), + 'rollback': (), + 'destroy': (), + '__len__': (), + 'list': ('limit', 'marker', ), + 'put': ('id', 'data', ), + 'get': ('id_', ), + 'delete': ('id', ), + 'save_key': ('keydata', ), + 'load_key': (), + 'break_lock': (), + 'negotiate': ('client_data', ), + 'open': ('path', 'create', 'lock_wait', 'lock', 'exclusive', 'append_only', ), + 'get_free_nonce': (), + 'commit_nonce_reservation': ('next_unreserved', 'start_nonce', ), +} + + +def decode_keys(d): + return {k.decode(): d[k] for k in d} + + class RepositoryServer: # pragma: no cover rpc_methods = ( '__len__', @@ -79,6 +129,16 @@ class RepositoryServer: # pragma: no cover self.repository = None self.restrict_to_paths = restrict_to_paths self.append_only = append_only + self.client_version = parse_version('1.0.8') # fallback version if client is too old to send version information + + def positional_to_named(self, method, argv): + """Translate from positional protocol to named protocol.""" + return {name: argv[pos] for pos, name in enumerate(compatMap[method])} + + def filter_args(self, f, kwargs): + """Remove unknown named parameters from call, because client did (implicitly) say it's ok.""" + known = set(inspect.signature(f).parameters) + return {name: kwargs[name] for name in kwargs if name in known} def serve(self): stdin_fd = sys.stdin.fileno() @@ -107,12 +167,20 @@ class RepositoryServer: # pragma: no cover return unpacker.feed(data) for unpacked in unpacker: - if not (isinstance(unpacked, tuple) and len(unpacked) == 4): + if isinstance(unpacked, dict): + dictFormat = True + msgid = unpacked[MSGID] + method = unpacked[MSG].decode() + args = decode_keys(unpacked[ARGS]) + elif isinstance(unpacked, tuple) and len(unpacked) == 4: + dictFormat = False + type, msgid, method, args = unpacked + method = method.decode() + args = self.positional_to_named(method, args) + else: if self.repository is not None: self.repository.close() raise UnexpectedRPCDataFormatFromClient(__version__) - type, msgid, method, args = unpacked - method = method.decode() try: if method not in self.rpc_methods: raise InvalidRPCMethod(method) @@ -120,7 +188,8 @@ class RepositoryServer: # pragma: no cover f = getattr(self, method) except AttributeError: f = getattr(self.repository, method) - res = f(*args) + args = self.filter_args(f, args) + res = f(**args) except BaseException as e: if isinstance(e, (Repository.DoesNotExist, Repository.AlreadyExists, PathNotAllowed)): # These exceptions are reconstructed on the client end in RemoteRepository.call_many(), @@ -138,18 +207,35 @@ class RepositoryServer: # pragma: no cover logging.error(msg) logging.log(tb_log_level, tb) exc = 'Remote Exception (see remote log for the traceback)' - os.write(stdout_fd, msgpack.packb((1, msgid, e.__class__.__name__, exc))) + if dictFormat: + os.write(stdout_fd, msgpack.packb({MSGID: msgid, b'exception_class': e.__class__.__name__})) + else: + os.write(stdout_fd, msgpack.packb((1, msgid, e.__class__.__name__, exc))) else: - os.write(stdout_fd, msgpack.packb((1, msgid, None, res))) + if dictFormat: + os.write(stdout_fd, msgpack.packb({MSGID: msgid, RESULT: res})) + else: + os.write(stdout_fd, msgpack.packb((1, msgid, None, res))) if es: self.repository.close() return - def negotiate(self, versions): - return RPC_PROTOCOL_VERSION + def negotiate(self, client_data): + # old format used in 1.0.x + if client_data == RPC_PROTOCOL_VERSION: + return RPC_PROTOCOL_VERSION + # clients since 1.1.0b3 use a dict as client_data + if isinstance(client_data, dict): + self.client_version = client_data[b'client_version'] + else: + self.client_version = BORG_VERSION # seems to be newer than current version (no known old format) + + # not a known old format, send newest negotiate this version knows + return {'server_version': BORG_VERSION} def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False): - path = os.fsdecode(path) + if isinstance(path, bytes): + path = os.fsdecode(path) if path.startswith('/~'): # /~/x = path x relative to home dir, /~username/x = relative to "user" home dir path = os.path.join(get_home_dir(), path[2:]) # XXX check this (see also 1.0-maint), is it correct for ~u? elif path.startswith('/./'): # /./x = path x relative to cwd @@ -203,6 +289,54 @@ class SleepingBandwidthLimiter: return written +def api(*, since, **kwargs_decorator): + """Check version requirements and use self.call to do the remote method call. + + specifies the version in which borg introduced this method, + calling this method when connected to an older version will fail without transmiting + anything to the server. + + Further kwargs can be used to encode version specific restrictions. + If a previous hardcoded behaviour is parameterized in a version, this allows calls that + use the previously hardcoded behaviour to pass through and generates an error if another + behaviour is requested by the client. + + e.g. when 'append_only' was introduced in 1.0.7 the previous behaviour was what now is append_only=False. + Thus @api(..., append_only={'since': parse_version('1.0.7'), 'previously': False}) allows calls + with append_only=False for all version but rejects calls using append_only=True on versions older than 1.0.7. + """ + def decorator(f): + @functools.wraps(f) + def do_rpc(self, *args, **kwargs): + sig = inspect.signature(f) + bound_args = sig.bind(self, *args, **kwargs) + named = {} + for name, param in sig.parameters.items(): + if name == 'self': + continue + if name in bound_args.arguments: + named[name] = bound_args.arguments[name] + else: + if param.default is not param.empty: + named[name] = param.default + + if self.server_version < since: + raise self.RPCServerOutdated(f.__name__, format_version(since)) + + for name, restriction in kwargs_decorator.items(): + if restriction['since'] <= self.server_version: + continue + if 'previously' in restriction and named[name] == restriction['previously']: + continue + + raise self.RPCServerOutdated("{0} {1}={2!s}".format(f.__name__, name, named[name]), + format_version(restriction['since'])) + + return self.call(f.__name__, named) + return do_rpc + return decorator + + class RemoteRepository: extra_test_args = [] @@ -214,6 +348,17 @@ class RemoteRepository: class NoAppendOnlyOnServer(Error): """Server does not support --append-only.""" + class RPCServerOutdated(Error): + """Borg server is too old for {}. Required version {}""" + + @property + def method(self): + return self.args[0] + + @property + def required_version(self): + return self.args[1] + def __init__(self, location, create=False, exclusive=False, lock_wait=None, lock=True, append_only=False, args=None): self.location = self._location = location self.preload_ids = [] @@ -225,6 +370,8 @@ class RemoteRepository: self.ratelimit = SleepingBandwidthLimiter(args.remote_ratelimit * 1024 if args and args.remote_ratelimit else 0) self.unpacker = msgpack.Unpacker(use_list=False) + self.dictFormat = False + self.server_version = parse_version('1.0.8') # fallback version if server is too old to send version information self.p = None testing = location.host == '__testsuite__' borg_cmd = self.borg_cmd(args, testing) @@ -254,15 +401,22 @@ class RemoteRepository: try: try: - version = self.call('negotiate', RPC_PROTOCOL_VERSION) + version = self.call('negotiate', {'client_data': {b'client_version': BORG_VERSION}}) except ConnectionClosed: raise ConnectionClosedWithHint('Is borg working on the server?') from None - if version != RPC_PROTOCOL_VERSION: - raise Exception('Server insisted on using unsupported protocol version %d' % version) + if version == RPC_PROTOCOL_VERSION: + self.dictFormat = False + elif isinstance(version, dict) and b'server_version' in version: + self.dictFormat = True + self.server_version = version[b'server_version'] + else: + raise Exception('Server insisted on using unsupported protocol version %s' % version) + try: - self.id = self.call('open', self.location.path, create, lock_wait, lock, exclusive, append_only) + self.id = self.call('open', {'path': self.location.path, 'create': create, 'lock_wait': lock_wait, + 'lock': lock, 'exclusive': exclusive, 'append_only': append_only}) except self.RPCError as err: - if err.remote_type != 'TypeError': + if self.dictFormat or err.remote_type != 'TypeError': raise msg = """\ Please note: @@ -276,7 +430,9 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. sys.stderr.write(msg) if append_only: raise self.NoAppendOnlyOnServer() - self.id = self.call('open', self.location.path, create, lock_wait, lock) + compatMap['open'] = ('path', 'create', 'lock_wait', 'lock', ) + self.id = self.call('open', {'path': self.location.path, 'create': create, 'lock_wait': lock_wait, + 'lock': lock, 'exclusive': exclusive, 'append_only': append_only}) except Exception: self.close() raise @@ -348,7 +504,10 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. args.append('%s' % location.host) return args - def call(self, cmd, *args, **kw): + def named_to_positional(self, method, kwargs): + return [kwargs[name] for name in compatMap[method]] + + def call(self, cmd, args, **kw): for resp in self.call_many(cmd, [args], **kw): return resp @@ -386,12 +545,12 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. while wait or calls: while waiting_for: try: - error, res = self.responses.pop(waiting_for[0]) + unpacked = self.responses.pop(waiting_for[0]) waiting_for.pop(0) - if error: - handle_error(error, res) + if b'exception_class' in unpacked: + handle_error(unpacked[b'exception_class'], None) else: - yield res + yield unpacked[RESULT] if not waiting_for and not calls: return except KeyError: @@ -410,15 +569,22 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. raise ConnectionClosed() self.unpacker.feed(data) for unpacked in self.unpacker: - if not (isinstance(unpacked, tuple) and len(unpacked) == 4): + if isinstance(unpacked, dict): + msgid = unpacked[MSGID] + elif isinstance(unpacked, tuple) and len(unpacked) == 4: + type, msgid, error, res = unpacked + if error: + unpacked = {MSGID: msgid, b'exception_class': error} + else: + unpacked = {MSGID: msgid, RESULT: res} + else: raise UnexpectedRPCDataFormatFromServer() - type, msgid, error, res = unpacked if msgid in self.ignore_responses: self.ignore_responses.remove(msgid) - if error: - handle_error(error, res) + if b'exception_class' in unpacked: + handle_error(unpacked[b'exception_class'], None) else: - self.responses[msgid] = error, res + self.responses[msgid] = unpacked elif fd is self.stderr_fd: data = os.read(fd, 32768) if not data: @@ -431,22 +597,28 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. if calls: if is_preloaded: assert cmd == 'get', "is_preload is only supported for 'get'" - if calls[0][0] in self.chunkid_to_msgids: - waiting_for.append(pop_preload_msgid(calls.pop(0)[0])) + if calls[0]['id_'] in self.chunkid_to_msgids: + waiting_for.append(pop_preload_msgid(calls.pop(0)['id_'])) else: args = calls.pop(0) - if cmd == 'get' and args[0] in self.chunkid_to_msgids: - waiting_for.append(pop_preload_msgid(args[0])) + if cmd == 'get' and args['id_'] in self.chunkid_to_msgids: + waiting_for.append(pop_preload_msgid(args['id_'])) else: self.msgid += 1 waiting_for.append(self.msgid) - self.to_send = msgpack.packb((1, self.msgid, cmd, args)) + if self.dictFormat: + self.to_send = msgpack.packb({MSGID: self.msgid, MSG: cmd, ARGS: args}) + else: + self.to_send = msgpack.packb((1, self.msgid, cmd, self.named_to_positional(cmd, args))) if not self.to_send and self.preload_ids: chunk_id = self.preload_ids.pop(0) - args = (chunk_id,) + args = {'id_': chunk_id} self.msgid += 1 self.chunkid_to_msgids.setdefault(chunk_id, []).append(self.msgid) - self.to_send = msgpack.packb((1, self.msgid, 'get', args)) + if self.dictFormat: + self.to_send = msgpack.packb({MSGID: self.msgid, MSG: 'get', ARGS: args}) + else: + self.to_send = msgpack.packb((1, self.msgid, 'get', self.named_to_positional(cmd, args))) if self.to_send: try: @@ -458,55 +630,69 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. raise self.ignore_responses |= set(waiting_for) + @api(since=parse_version('1.0.0')) def check(self, repair=False, save_space=False): - return self.call('check', repair, save_space) + """actual remoting is done via self.call in the @api decorator""" + @api(since=parse_version('1.0.0')) def commit(self, save_space=False): - return self.call('commit', save_space) + """actual remoting is done via self.call in the @api decorator""" - def rollback(self, *args): - return self.call('rollback') + @api(since=parse_version('1.0.0')) + def rollback(self): + """actual remoting is done via self.call in the @api decorator""" + @api(since=parse_version('1.0.0')) def destroy(self): - return self.call('destroy') + """actual remoting is done via self.call in the @api decorator""" + @api(since=parse_version('1.0.0')) def __len__(self): - return self.call('__len__') + """actual remoting is done via self.call in the @api decorator""" + @api(since=parse_version('1.0.0')) def list(self, limit=None, marker=None): - return self.call('list', limit, marker) + """actual remoting is done via self.call in the @api decorator""" + @api(since=parse_version('1.1.0b3')) def scan(self, limit=None, marker=None): - return self.call('scan', limit, marker) + """actual remoting is done via self.call in the @api decorator""" def get(self, id_): for resp in self.get_many([id_]): return resp def get_many(self, ids, is_preloaded=False): - for resp in self.call_many('get', [(id_,) for id_ in ids], is_preloaded=is_preloaded): + for resp in self.call_many('get', [{'id_': id_} for id_ in ids], is_preloaded=is_preloaded): yield resp - def put(self, id_, data, wait=True): - return self.call('put', id_, data, wait=wait) + @api(since=parse_version('1.0.0')) + def put(self, id, data, wait=True): + """actual remoting is done via self.call in the @api decorator""" - def delete(self, id_, wait=True): - return self.call('delete', id_, wait=wait) + @api(since=parse_version('1.0.0')) + def delete(self, id, wait=True): + """actual remoting is done via self.call in the @api decorator""" + @api(since=parse_version('1.0.0')) def save_key(self, keydata): - return self.call('save_key', keydata) + """actual remoting is done via self.call in the @api decorator""" + @api(since=parse_version('1.0.0')) def load_key(self): - return self.call('load_key') + """actual remoting is done via self.call in the @api decorator""" + @api(since=parse_version('1.0.0')) def get_free_nonce(self): - return self.call('get_free_nonce') + """actual remoting is done via self.call in the @api decorator""" + @api(since=parse_version('1.0.0')) def commit_nonce_reservation(self, next_unreserved, start_nonce): - return self.call('commit_nonce_reservation', next_unreserved, start_nonce) + """actual remoting is done via self.call in the @api decorator""" + @api(since=parse_version('1.0.0')) def break_lock(self): - return self.call('break_lock') + """actual remoting is done via self.call in the @api decorator""" def close(self): if self.p: From 4854fcef2e80ae3e5d1e012facb222ae358f94fc Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 31 Jul 2016 21:19:30 +0200 Subject: [PATCH 0395/1387] remote: Move open to a normal api stub. --- src/borg/remote.py | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index b4e1b3d2..5e536935 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -345,9 +345,6 @@ class RemoteRepository: self.name = name self.remote_type = remote_type - class NoAppendOnlyOnServer(Error): - """Server does not support --append-only.""" - class RPCServerOutdated(Error): """Borg server is too old for {}. Required version {}""" @@ -412,13 +409,20 @@ class RemoteRepository: else: raise Exception('Server insisted on using unsupported protocol version %s' % version) - try: - self.id = self.call('open', {'path': self.location.path, 'create': create, 'lock_wait': lock_wait, - 'lock': lock, 'exclusive': exclusive, 'append_only': append_only}) - except self.RPCError as err: - if self.dictFormat or err.remote_type != 'TypeError': - raise - msg = """\ + def do_open(): + self.id = self.open(path=self.location.path, create=create, lock_wait=lock_wait, + lock=lock, exclusive=exclusive, append_only=append_only) + + if self.dictFormat: + do_open() + else: + # Ugly detection of versions prior to 1.0.7: If open throws it has to be 1.0.6 or lower + try: + do_open() + except self.RPCError as err: + if err.remote_type != 'TypeError': + raise + msg = """\ Please note: If you see a TypeError complaining about the number of positional arguments given to open(), you can ignore it if it comes from a borg version < 1.0.7. @@ -426,13 +430,12 @@ This TypeError is a cosmetic side effect of the compatibility code borg clients >= 1.0.7 have to support older borg servers. This problem will go away as soon as the server has been upgraded to 1.0.7+. """ - # emit this msg in the same way as the 'Remote: ...' lines that show the remote TypeError - sys.stderr.write(msg) - if append_only: - raise self.NoAppendOnlyOnServer() - compatMap['open'] = ('path', 'create', 'lock_wait', 'lock', ) - self.id = self.call('open', {'path': self.location.path, 'create': create, 'lock_wait': lock_wait, - 'lock': lock, 'exclusive': exclusive, 'append_only': append_only}) + # emit this msg in the same way as the 'Remote: ...' lines that show the remote TypeError + sys.stderr.write(msg) + self.server_version = parse_version('1.0.6') + compatMap['open'] = ('path', 'create', 'lock_wait', 'lock', ), + # try again with corrected version and compatMap + do_open() except Exception: self.close() raise @@ -630,6 +633,11 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. raise self.ignore_responses |= set(waiting_for) + @api(since=parse_version('1.0.0'), + append_only={'since': parse_version('1.0.7'), 'previously': False}) + def open(self, path, create=False, lock_wait=None, lock=True, exclusive=False, append_only=False): + """actual remoting is done via self.call in the @api decorator""" + @api(since=parse_version('1.0.0')) def check(self, repair=False, save_space=False): """actual remoting is done via self.call in the @api decorator""" From e14406fdbfdb3074e021f58933ff3f6c0be38eb8 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Tue, 8 Nov 2016 23:18:18 +0100 Subject: [PATCH 0396/1387] remote: Redo exception handling --- src/borg/archiver.py | 10 ++-- src/borg/remote.py | 126 ++++++++++++++++++++++++++++++++----------- 2 files changed, 103 insertions(+), 33 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 6df3284c..59f4e17b 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2753,10 +2753,14 @@ def main(): # pragma: no cover tb = "%s\n%s" % (traceback.format_exc(), sysinfo()) exit_code = e.exit_code except RemoteRepository.RPCError as e: - msg = "%s %s" % (e.remote_type, e.name) - important = e.remote_type not in ('LockTimeout', ) + important = e.exception_class not in ('LockTimeout', ) tb_log_level = logging.ERROR if important else logging.DEBUG - tb = sysinfo() + if important: + msg = e.exception_full + else: + msg = e.get_message() + tb = '\n'.join('Borg server: ' + l for l in e.sysinfo.splitlines()) + tb += "\n" + sysinfo() exit_code = EXIT_ERROR except Exception: msg = 'Local Exception' diff --git a/src/borg/remote.py b/src/borg/remote.py index 5e536935..7d1a11b4 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -191,25 +191,53 @@ class RepositoryServer: # pragma: no cover args = self.filter_args(f, args) res = f(**args) except BaseException as e: - if isinstance(e, (Repository.DoesNotExist, Repository.AlreadyExists, PathNotAllowed)): - # These exceptions are reconstructed on the client end in RemoteRepository.call_many(), - # and will be handled just like locally raised exceptions. Suppress the remote traceback - # for these, except ErrorWithTraceback, which should always display a traceback. - pass - else: - if isinstance(e, Error): - tb_log_level = logging.ERROR if e.traceback else logging.DEBUG - msg = e.get_message() - else: - tb_log_level = logging.ERROR - msg = '%s Exception in RPC call' % e.__class__.__name__ - tb = '%s\n%s' % (traceback.format_exc(), sysinfo()) - logging.error(msg) - logging.log(tb_log_level, tb) - exc = 'Remote Exception (see remote log for the traceback)' if dictFormat: - os.write(stdout_fd, msgpack.packb({MSGID: msgid, b'exception_class': e.__class__.__name__})) + ex_short = traceback.format_exception_only(e.__class__, e) + ex_full = traceback.format_exception(*sys.exc_info()) + if isinstance(e, Error): + ex_short = e.get_message() + if isinstance(e, (Repository.DoesNotExist, Repository.AlreadyExists, PathNotAllowed)): + # These exceptions are reconstructed on the client end in RemoteRepository.call_many(), + # and will be handled just like locally raised exceptions. Suppress the remote traceback + # for these, except ErrorWithTraceback, which should always display a traceback. + pass + else: + logging.debug('\n'.join(ex_full)) + + try: + msg = msgpack.packb({MSGID: msgid, + b'exception_class': e.__class__.__name__, + b'exception_args': e.args, + b'exception_full': ex_full, + b'exception_short': ex_short, + b'sysinfo': sysinfo()}) + except TypeError: + msg = msgpack.packb({MSGID: msgid, + b'exception_class': e.__class__.__name__, + b'exception_args': [x if isinstance(x, (str, bytes, int)) else None + for x in e.args], + b'exception_full': ex_full, + b'exception_short': ex_short, + b'sysinfo': sysinfo()}) + + os.write(stdout_fd, msg) else: + if isinstance(e, (Repository.DoesNotExist, Repository.AlreadyExists, PathNotAllowed)): + # These exceptions are reconstructed on the client end in RemoteRepository.call_many(), + # and will be handled just like locally raised exceptions. Suppress the remote traceback + # for these, except ErrorWithTraceback, which should always display a traceback. + pass + else: + if isinstance(e, Error): + tb_log_level = logging.ERROR if e.traceback else logging.DEBUG + msg = e.get_message() + else: + tb_log_level = logging.ERROR + msg = '%s Exception in RPC call' % e.__class__.__name__ + tb = '%s\n%s' % (traceback.format_exc(), sysinfo()) + logging.error(msg) + logging.log(tb_log_level, tb) + exc = 'Remote Exception (see remote log for the traceback)' os.write(stdout_fd, msgpack.packb((1, msgid, e.__class__.__name__, exc))) else: if dictFormat: @@ -341,9 +369,34 @@ class RemoteRepository: extra_test_args = [] class RPCError(Exception): - def __init__(self, name, remote_type): - self.name = name - self.remote_type = remote_type + def __init__(self, unpacked): + # for borg < 1.1: unpacked only has b'exception_class' as key + # for borg 1.1+: unpacked has keys: b'exception_args', b'exception_full', b'exception_short', b'sysinfo' + self.unpacked = unpacked + + def get_message(self): + if b'exception_short' in self.unpacked: + return b'\n'.join(self.unpacked[b'exception_short']).decode() + else: + return self.exception_class + + @property + def exception_class(self): + return self.unpacked[b'exception_class'].decode() + + @property + def exception_full(self): + if b'exception_full' in self.unpacked: + return b'\n'.join(self.unpacked[b'exception_full']).decode() + else: + return self.get_message() + '\nRemote Exception (see remote log for the traceback)' + + @property + def sysinfo(self): + if b'sysinfo' in self.unpacked: + return self.unpacked[b'sysinfo'].decode() + else: + return '' class RPCServerOutdated(Error): """Borg server is too old for {}. Required version {}""" @@ -411,7 +464,7 @@ class RemoteRepository: def do_open(): self.id = self.open(path=self.location.path, create=create, lock_wait=lock_wait, - lock=lock, exclusive=exclusive, append_only=append_only) + lock=lock, exclusive=exclusive, append_only=append_only) if self.dictFormat: do_open() @@ -420,7 +473,7 @@ class RemoteRepository: try: do_open() except self.RPCError as err: - if err.remote_type != 'TypeError': + if err.exception_class != 'TypeError': raise msg = """\ Please note: @@ -524,8 +577,11 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. del self.chunkid_to_msgids[chunkid] return msgid - def handle_error(error, res): - error = error.decode('utf-8') + def handle_error(unpacked): + error = unpacked[b'exception_class'].decode() + old_server = b'exception_args' not in unpacked + args = unpacked.get(b'exception_args') + if error == 'DoesNotExist': raise Repository.DoesNotExist(self.location.orig) elif error == 'AlreadyExists': @@ -533,15 +589,24 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. elif error == 'CheckNeeded': raise Repository.CheckNeeded(self.location.orig) elif error == 'IntegrityError': - raise IntegrityError('(not available)') + if old_server: + raise IntegrityError('(not available)') + else: + raise IntegrityError(args[0].decode()) elif error == 'PathNotAllowed': raise PathNotAllowed() elif error == 'ObjectNotFound': - raise Repository.ObjectNotFound('(not available)', self.location.orig) + if old_server: + raise Repository.ObjectNotFound('(not available)', self.location.orig) + else: + raise Repository.ObjectNotFound(args[0].decode(), self.location.orig) elif error == 'InvalidRPCMethod': - raise InvalidRPCMethod('(not available)') + if old_server: + raise InvalidRPCMethod('(not available)') + else: + raise InvalidRPCMethod(args[0].decode()) else: - raise self.RPCError(res.decode('utf-8'), error) + raise self.RPCError(unpacked) calls = list(calls) waiting_for = [] @@ -551,7 +616,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. unpacked = self.responses.pop(waiting_for[0]) waiting_for.pop(0) if b'exception_class' in unpacked: - handle_error(unpacked[b'exception_class'], None) + handle_error(unpacked) else: yield unpacked[RESULT] if not waiting_for and not calls: @@ -577,6 +642,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. elif isinstance(unpacked, tuple) and len(unpacked) == 4: type, msgid, error, res = unpacked if error: + # ignore res, because it is only a fixed string anyway. unpacked = {MSGID: msgid, b'exception_class': error} else: unpacked = {MSGID: msgid, RESULT: res} @@ -585,7 +651,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. if msgid in self.ignore_responses: self.ignore_responses.remove(msgid) if b'exception_class' in unpacked: - handle_error(unpacked[b'exception_class'], None) + handle_error(unpacked) else: self.responses[msgid] = unpacked elif fd is self.stderr_fd: From bd3a4a2f9258a3354a2d73f5db1776f1c985f49c Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Tue, 8 Nov 2016 23:19:06 +0100 Subject: [PATCH 0397/1387] Add testing for exception transport. --- src/borg/remote.py | 24 +++++++++++++- src/borg/testsuite/repository.py | 57 ++++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 7d1a11b4..face64c5 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -122,7 +122,8 @@ class RepositoryServer: # pragma: no cover 'load_key', 'break_lock', 'get_free_nonce', - 'commit_nonce_reservation' + 'commit_nonce_reservation', + 'inject_exception', ) def __init__(self, restrict_to_paths, append_only): @@ -286,6 +287,27 @@ class RepositoryServer: # pragma: no cover self.repository.__enter__() # clean exit handled by serve() method return self.repository.id + def inject_exception(self, kind): + kind = kind.decode() + s1 = 'test string' + s2 = 'test string2' + if kind == 'DoesNotExist': + raise Repository.DoesNotExist(s1) + elif kind == 'AlreadyExists': + raise Repository.AlreadyExists(s1) + elif kind == 'CheckNeeded': + raise Repository.CheckNeeded(s1) + elif kind == 'IntegrityError': + raise IntegrityError(s1) + elif kind == 'PathNotAllowed': + raise PathNotAllowed() + elif kind == 'ObjectNotFound': + raise Repository.ObjectNotFound(s1, s2) + elif kind == 'InvalidRPCMethod': + raise InvalidRPCMethod(s1) + elif kind == 'divide': + 0 // 0 + class SleepingBandwidthLimiter: def __init__(self, limit): diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 6d6f00a7..6b66fb73 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -12,7 +12,7 @@ from ..hashindex import NSIndex from ..helpers import Location from ..helpers import IntegrityError from ..locking import Lock, LockFailed -from ..remote import RemoteRepository, InvalidRPCMethod, ConnectionClosedWithHint, handle_remote_line +from ..remote import RemoteRepository, InvalidRPCMethod, PathNotAllowed, ConnectionClosedWithHint, handle_remote_line from ..repository import Repository, LoggedIO, MAGIC, MAX_DATA_SIZE, TAG_DELETE from . import BaseTestCase from .hashindex import H @@ -647,7 +647,60 @@ class RemoteRepositoryTestCase(RepositoryTestCase): exclusive=True, create=create) def test_invalid_rpc(self): - self.assert_raises(InvalidRPCMethod, lambda: self.repository.call('__init__', None)) + self.assert_raises(InvalidRPCMethod, lambda: self.repository.call('__init__', {})) + + def test_rpc_exception_transport(self): + s1 = 'test string' + + try: + self.repository.call('inject_exception', {'kind': 'DoesNotExist'}) + except Repository.DoesNotExist as e: + assert len(e.args) == 1 + assert e.args[0] == self.repository.location.orig + + try: + self.repository.call('inject_exception', {'kind': 'AlreadyExists'}) + except Repository.AlreadyExists as e: + assert len(e.args) == 1 + assert e.args[0] == self.repository.location.orig + + try: + self.repository.call('inject_exception', {'kind': 'CheckNeeded'}) + except Repository.CheckNeeded as e: + assert len(e.args) == 1 + assert e.args[0] == self.repository.location.orig + + try: + self.repository.call('inject_exception', {'kind': 'IntegrityError'}) + except IntegrityError as e: + assert len(e.args) == 1 + assert e.args[0] == s1 + + try: + self.repository.call('inject_exception', {'kind': 'PathNotAllowed'}) + except PathNotAllowed as e: + assert len(e.args) == 0 + + try: + self.repository.call('inject_exception', {'kind': 'ObjectNotFound'}) + except Repository.ObjectNotFound as e: + assert len(e.args) == 2 + assert e.args[0] == s1 + assert e.args[1] == self.repository.location.orig + + try: + self.repository.call('inject_exception', {'kind': 'InvalidRPCMethod'}) + except InvalidRPCMethod as e: + assert len(e.args) == 1 + assert e.args[0] == s1 + + try: + self.repository.call('inject_exception', {'kind': 'divide'}) + except RemoteRepository.RPCError as e: + assert e.unpacked + assert e.get_message() == 'ZeroDivisionError: integer division or modulo by zero\n' + assert e.exception_class == 'ZeroDivisionError' + assert len(e.exception_full) > 0 def test_ssh_cmd(self): assert self.repository.ssh_cmd(Location('example.com:foo')) == ['ssh', 'example.com'] From 8955d8bb2ae56d66050f130ae26f4d10cc597438 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Thu, 10 Nov 2016 12:12:32 +0100 Subject: [PATCH 0398/1387] remote: Test that the legacy free rpc bootstrap works. --- src/borg/remote.py | 4 +++- src/borg/testsuite/repository.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index face64c5..0b15428d 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -431,6 +431,9 @@ class RemoteRepository: def required_version(self): return self.args[1] + # If compatibility with 1.0.x is not longer needed, replace all checks of this with True and simplify the code + dictFormat = False # outside of __init__ for testing of legacy free protocol + def __init__(self, location, create=False, exclusive=False, lock_wait=None, lock=True, append_only=False, args=None): self.location = self._location = location self.preload_ids = [] @@ -442,7 +445,6 @@ class RemoteRepository: self.ratelimit = SleepingBandwidthLimiter(args.remote_ratelimit * 1024 if args and args.remote_ratelimit else 0) self.unpacker = msgpack.Unpacker(use_list=False) - self.dictFormat = False self.server_version = parse_version('1.0.8') # fallback version if server is too old to send version information self.p = None testing = location.host == '__testsuite__' diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 6b66fb73..8d77e120 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -723,6 +723,31 @@ class RemoteRepositoryTestCase(RepositoryTestCase): assert self.repository.borg_cmd(args, testing=False) == ['borg-0.28.2', 'serve', '--umask=077', '--info'] +class RemoteLegacyFree(RepositoryTestCaseBase): + # Keep testing this so we can someday safely remove the legacy tuple format. + + def open(self, create=False): + with patch.object(RemoteRepository, 'dictFormat', True): + return RemoteRepository(Location('__testsuite__:' + os.path.join(self.tmppath, 'repository')), + exclusive=True, create=create) + + def test_legacy_free(self): + # put + self.repository.put(H(0), b'foo') + self.repository.commit() + self.repository.close() + # replace + self.repository = self.open() + with self.repository: + self.repository.put(H(0), b'bar') + self.repository.commit() + # delete + self.repository = self.open() + with self.repository: + self.repository.delete(H(0)) + self.repository.commit() + + @pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteRepositoryCheckTestCase(RepositoryCheckTestCase): From 1edff44b3d032826e8cda983f3fdf464ddab1679 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Thu, 10 Nov 2016 11:08:45 +0100 Subject: [PATCH 0399/1387] Repository,remote: Rename argument of get to 'id'. --- src/borg/remote.py | 18 +++++++++--------- src/borg/repository.py | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 0b15428d..6894a729 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -88,7 +88,7 @@ compatMap = { '__len__': (), 'list': ('limit', 'marker', ), 'put': ('id', 'data', ), - 'get': ('id_', ), + 'get': ('id', ), 'delete': ('id', ), 'save_key': ('keydata', ), 'load_key': (), @@ -690,12 +690,12 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. if calls: if is_preloaded: assert cmd == 'get', "is_preload is only supported for 'get'" - if calls[0]['id_'] in self.chunkid_to_msgids: - waiting_for.append(pop_preload_msgid(calls.pop(0)['id_'])) + if calls[0]['id'] in self.chunkid_to_msgids: + waiting_for.append(pop_preload_msgid(calls.pop(0)['id'])) else: args = calls.pop(0) - if cmd == 'get' and args['id_'] in self.chunkid_to_msgids: - waiting_for.append(pop_preload_msgid(args['id_'])) + if cmd == 'get' and args['id'] in self.chunkid_to_msgids: + waiting_for.append(pop_preload_msgid(args['id'])) else: self.msgid += 1 waiting_for.append(self.msgid) @@ -705,7 +705,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. self.to_send = msgpack.packb((1, self.msgid, cmd, self.named_to_positional(cmd, args))) if not self.to_send and self.preload_ids: chunk_id = self.preload_ids.pop(0) - args = {'id_': chunk_id} + args = {'id': chunk_id} self.msgid += 1 self.chunkid_to_msgids.setdefault(chunk_id, []).append(self.msgid) if self.dictFormat: @@ -756,12 +756,12 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. def scan(self, limit=None, marker=None): """actual remoting is done via self.call in the @api decorator""" - def get(self, id_): - for resp in self.get_many([id_]): + def get(self, id): + for resp in self.get_many([id]): return resp def get_many(self, ids, is_preloaded=False): - for resp in self.call_many('get', [{'id_': id_} for id_ in ids], is_preloaded=is_preloaded): + for resp in self.call_many('get', [{'id': id} for id in ids], is_preloaded=is_preloaded): yield resp @api(since=parse_version('1.0.0')) diff --git a/src/borg/repository.py b/src/borg/repository.py index 7d1bf829..fda085a6 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -825,14 +825,14 @@ class Repository: return result return result - def get(self, id_): + def get(self, id): if not self.index: self.index = self.open_index(self.get_transaction_id()) try: - segment, offset = self.index[id_] - return self.io.read(segment, offset, id_) + segment, offset = self.index[id] + return self.io.read(segment, offset, id) except KeyError: - raise self.ObjectNotFound(id_, self.path) from None + raise self.ObjectNotFound(id, self.path) from None def get_many(self, ids, is_preloaded=False): for id_ in ids: From f37109848f884b7e1cc7b0e9a2f8dd871b3be3ed Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Tue, 15 Nov 2016 23:18:21 +0100 Subject: [PATCH 0400/1387] remote: Remove unused type variable. --- src/borg/remote.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 6894a729..9a5221bf 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -175,7 +175,8 @@ class RepositoryServer: # pragma: no cover args = decode_keys(unpacked[ARGS]) elif isinstance(unpacked, tuple) and len(unpacked) == 4: dictFormat = False - type, msgid, method, args = unpacked + # The first field 'type' was always 1 and has always been ignored + _, msgid, method, args = unpacked method = method.decode() args = self.positional_to_named(method, args) else: @@ -664,7 +665,8 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. if isinstance(unpacked, dict): msgid = unpacked[MSGID] elif isinstance(unpacked, tuple) and len(unpacked) == 4: - type, msgid, error, res = unpacked + # The first field 'type' was always 1 and has always been ignored + _, msgid, error, res = unpacked if error: # ignore res, because it is only a fixed string anyway. unpacked = {MSGID: msgid, b'exception_class': error} From e17fe2b295d1d1e97a1e553413759890c7b9ace4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 17 Nov 2016 20:16:28 +0100 Subject: [PATCH 0401/1387] borg umount, fixes #1855 this refactors umount code we already used for the testsuite into the platform module's namespace. also, it exposes that functionality via the cli api, so users can use it via "borg umount ", which is more consistent than using borg to mount and fusermount -u (or umount) to un-mount. --- borg/archiver.py | 20 +++++++++++++++++++ borg/helpers.py | 2 +- borg/platform.py | 10 ++++++++-- borg/platform_darwin.pyx | 2 +- borg/platform_freebsd.pyx | 2 +- borg/platform_linux.pyx | 7 ++++++- borg/testsuite/__init__.py | 7 ++----- docs/usage.rst | 4 +++- docs/usage/umount.rst.inc | 40 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 docs/usage/umount.rst.inc diff --git a/borg/archiver.py b/borg/archiver.py index 07086709..ecc44be7 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -34,6 +34,7 @@ from .key import key_creator, RepoKey, PassphraseKey from .keymanager import KeyManager from .archive import backup_io, BackupOSError, Archive, ArchiveChecker, CHUNKER_PARAMS, is_special from .remote import RepositoryServer, RemoteRepository, cache_if_remote +from .platform import umount has_lchflags = hasattr(os, 'lchflags') @@ -539,6 +540,10 @@ class Archiver: self.exit_code = EXIT_ERROR return self.exit_code + def do_umount(self, args): + """un-mount the FUSE filesystem""" + return umount(args.mountpoint) + @with_repository() def do_list(self, args, repository, manifest, key): """List archive or repository contents""" @@ -1476,6 +1481,21 @@ class Archiver: subparser.add_argument('-o', dest='options', type=str, help='Extra mount options') + umount_epilog = textwrap.dedent(""" + This command un-mounts a FUSE filesystem that was mounted with ``borg mount``. + + This is a convenience wrapper that just calls the platform-specific shell + command - usually this is either umount or fusermount -u. + """) + subparser = subparsers.add_parser('umount', parents=[common_parser], + description=self.do_umount.__doc__, + epilog=umount_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='umount repository') + subparser.set_defaults(func=self.do_umount) + subparser.add_argument('mountpoint', metavar='MOUNTPOINT', type=str, + help='mountpoint of the filesystem to umount') + info_epilog = textwrap.dedent(""" This command displays some detailed information about the specified archive. diff --git a/borg/helpers.py b/borg/helpers.py index a4469c99..72129f08 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -88,7 +88,7 @@ def check_extension_modules(): raise ExtensionModuleError if crypto.API_VERSION != 2: raise ExtensionModuleError - if platform.API_VERSION != 2: + if platform.API_VERSION != 3: raise ExtensionModuleError diff --git a/borg/platform.py b/borg/platform.py index e1cffd87..be7a2bcd 100644 --- a/borg/platform.py +++ b/borg/platform.py @@ -1,5 +1,6 @@ import errno import os +import subprocess import sys @@ -17,14 +18,19 @@ def sync_dir(path): os.close(fd) +# most POSIX platforms (but not Linux), see also borg 1.1 platform.base +def umount(mountpoint): + return subprocess.call(['umount', mountpoint]) + + if sys.platform.startswith('linux'): # pragma: linux only - from .platform_linux import acl_get, acl_set, API_VERSION + from .platform_linux import acl_get, acl_set, umount, API_VERSION elif sys.platform.startswith('freebsd'): # pragma: freebsd only from .platform_freebsd import acl_get, acl_set, API_VERSION elif sys.platform == 'darwin': # pragma: darwin only from .platform_darwin import acl_get, acl_set, API_VERSION else: # pragma: unknown platform only - API_VERSION = 2 + API_VERSION = 3 def acl_get(path, item, st, numeric_owner=False): pass diff --git a/borg/platform_darwin.pyx b/borg/platform_darwin.pyx index edb41f71..4dc25b83 100644 --- a/borg/platform_darwin.pyx +++ b/borg/platform_darwin.pyx @@ -1,7 +1,7 @@ import os from .helpers import user2uid, group2gid, safe_decode, safe_encode -API_VERSION = 2 +API_VERSION = 3 cdef extern from "sys/acl.h": ctypedef struct _acl_t: diff --git a/borg/platform_freebsd.pyx b/borg/platform_freebsd.pyx index 27d63626..ae69af68 100644 --- a/borg/platform_freebsd.pyx +++ b/borg/platform_freebsd.pyx @@ -1,7 +1,7 @@ import os from .helpers import posix_acl_use_stored_uid_gid, safe_encode, safe_decode -API_VERSION = 2 +API_VERSION = 3 cdef extern from "errno.h": int errno diff --git a/borg/platform_linux.pyx b/borg/platform_linux.pyx index f9ed4241..0185268c 100644 --- a/borg/platform_linux.pyx +++ b/borg/platform_linux.pyx @@ -1,9 +1,10 @@ import os import re +import subprocess from stat import S_ISLNK from .helpers import posix_acl_use_stored_uid_gid, user2uid, group2gid, safe_decode, safe_encode -API_VERSION = 2 +API_VERSION = 3 cdef extern from "sys/types.h": int ACL_TYPE_ACCESS @@ -141,3 +142,7 @@ def acl_set(path, item, numeric_owner=False): acl_set_file(p, ACL_TYPE_DEFAULT, default_acl) finally: acl_free(default_acl) + + +def umount(mountpoint): + return subprocess.call(['fusermount', '-u', mountpoint]) diff --git a/borg/testsuite/__init__.py b/borg/testsuite/__init__.py index 42b935d1..8d757d2b 100644 --- a/borg/testsuite/__init__.py +++ b/borg/testsuite/__init__.py @@ -9,6 +9,7 @@ import time import unittest from ..xattr import get_all from ..logger import setup_logging +from ..platform import umount try: import llfuse @@ -110,11 +111,7 @@ class BaseTestCase(unittest.TestCase): self.cmd(*args, fork=True) self.wait_for_mount(mountpoint) yield - if sys.platform.startswith('linux'): - cmd = 'fusermount -u %s' % mountpoint - else: - cmd = 'umount %s' % mountpoint - os.system(cmd) + umount(mountpoint) os.rmdir(mountpoint) # Give the daemon some time to exit time.sleep(.2) diff --git a/docs/usage.rst b/docs/usage.rst index 8743f6ac..2a3538b2 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -545,6 +545,8 @@ Examples .. include:: usage/mount.rst.inc +.. include:: usage/umount.rst.inc + Examples ~~~~~~~~ :: @@ -552,7 +554,7 @@ Examples $ borg mount /path/to/repo::root-2016-02-15 /tmp/mymountpoint $ ls /tmp/mymountpoint bin boot etc home lib lib64 lost+found media mnt opt root sbin srv tmp usr var - $ fusermount -u /tmp/mymountpoint + $ borg umount /tmp/mymountpoint .. include:: usage/key_export.rst.inc diff --git a/docs/usage/umount.rst.inc b/docs/usage/umount.rst.inc new file mode 100644 index 00000000..febacda0 --- /dev/null +++ b/docs/usage/umount.rst.inc @@ -0,0 +1,40 @@ +.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! + +.. _borg_umount: + +borg umount +----------- +:: + + usage: borg umount [-h] [--critical] [--error] [--warning] [--info] [--debug] + [--lock-wait N] [--show-rc] [--no-files-cache] [--umask M] + [--remote-path PATH] + MOUNTPOINT + + un-mount the FUSE filesystem + + positional arguments: + MOUNTPOINT mountpoint of the filesystem to umount + + optional arguments: + -h, --help show this help message and exit + --critical work on log level CRITICAL + --error work on log level ERROR + --warning work on log level WARNING (default) + --info, -v, --verbose + work on log level INFO + --debug work on log level DEBUG + --lock-wait N wait for the lock, but max. N seconds (default: 1). + --show-rc show/log the return code (rc) + --no-files-cache do not load/update the file metadata cache used to + detect unchanged files + --umask M set umask to M (local and remote, default: 0077) + --remote-path PATH set remote path to executable (default: "borg") + +Description +~~~~~~~~~~~ + +This command un-mounts a FUSE filesystem that was mounted with ``borg mount``. + +This is a convenience wrapper that just calls the platform-specific shell +command - usually this is either umount or fusermount -u. From 44935aa8eacf2742c3ed7edd7b7de409ddad89ad Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 19 Nov 2016 16:49:20 +0100 Subject: [PATCH 0402/1387] recreate: remove interruption blah, autocommit blah, resuming blah --- src/borg/archive.py | 136 +++------------------------------ src/borg/archiver.py | 50 +++++------- src/borg/testsuite/archiver.py | 100 ------------------------ 3 files changed, 29 insertions(+), 257 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 263536f2..269263f9 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1371,9 +1371,6 @@ class ArchiveChecker: class ArchiveRecreater: - AUTOCOMMIT_THRESHOLD = 512 * 1024 * 1024 - """Commit (compact segments) after this many (or 1 % of repository size, whichever is greater) bytes.""" - class FakeTargetArchive: def __init__(self): self.stats = Statistics() @@ -1409,9 +1406,6 @@ class ArchiveRecreater: compression_files or []) key.compression_decider2 = CompressionDecider2(compression or CompressionSpec('none')) - self.autocommit_threshold = max(self.AUTOCOMMIT_THRESHOLD, self.cache.chunks_stored_size() / 100) - logger.debug("Autocommit threshold: %s", format_file_size(self.autocommit_threshold)) - self.dry_run = dry_run self.stats = stats self.progress = progress @@ -1423,20 +1417,17 @@ class ArchiveRecreater: def recreate(self, archive_name, comment=None, target_name=None): assert not self.is_temporary_archive(archive_name) archive = self.open_archive(archive_name) - target, resume_from = self.create_target_or_resume(archive, target_name) + target = self.create_target(archive, target_name) if self.exclude_if_present or self.exclude_caches: self.matcher_add_tagged_dirs(archive) if self.matcher.empty() and not self.recompress and not target.recreate_rechunkify and comment is None: logger.info("Skipping archive %s, nothing to do", archive_name) return True - try: - self.process_items(archive, target, resume_from) - except self.Interrupted as e: - return self.save(archive, target, completed=False, metadata=e.metadata) + self.process_items(archive, target) replace_original = target_name is None return self.save(archive, target, comment, replace_original=replace_original) - def process_items(self, archive, target, resume_from=None): + def process_items(self, archive, target): matcher = self.matcher target_is_subset = not matcher.empty() hardlink_masters = {} if target_is_subset else None @@ -1450,15 +1441,8 @@ class ArchiveRecreater: for item in archive.iter_items(): if item_is_hardlink_master(item): - # Re-visit all of these items in the archive even when fast-forwarding to rebuild hardlink_masters hardlink_masters[item.path] = (item.get('chunks'), None) continue - if resume_from: - # Fast forward to after the last processed file - if item.path == resume_from: - logger.info('Fast-forwarded to %s', remove_surrogates(item.path)) - resume_from = None - continue if not matcher.match(item.path): self.print_file_status('x', item.path) continue @@ -1476,12 +1460,7 @@ class ArchiveRecreater: if self.dry_run: self.print_file_status('-', item.path) else: - try: - self.process_item(archive, target, item) - except self.Interrupted: - if self.progress: - target.stats.show_progress(final=True) - raise + self.process_item(archive, target, item) if self.progress: target.stats.show_progress(final=True) @@ -1491,8 +1470,6 @@ class ArchiveRecreater: target.stats.nfiles += 1 target.add_item(item) self.print_file_status(file_status(item.mode), item.path) - if self.interrupt: - raise self.Interrupted def process_chunks(self, archive, target, item): """Return new chunk ID list for 'item'.""" @@ -1500,9 +1477,8 @@ class ArchiveRecreater: for chunk_id, size, csize in item.chunks: self.cache.chunk_incref(chunk_id, target.stats) return item.chunks - new_chunks = self.process_partial_chunks(target) + new_chunks = [] chunk_iterator = self.create_chunk_iterator(archive, target, item) - consume(chunk_iterator, len(new_chunks)) compress = self.compression_decider1.decide(item.path) for chunk in chunk_iterator: chunk.meta['compress'] = compress @@ -1521,20 +1497,8 @@ class ArchiveRecreater: chunk_id, size, csize = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite) new_chunks.append((chunk_id, size, csize)) self.seen_chunks.add(chunk_id) - if self.recompress and self.cache.seen_chunk(chunk_id) == 1: - # This tracks how many bytes are uncommitted but compactable, since we are recompressing - # existing chunks. - target.recreate_uncomitted_bytes += csize - if target.recreate_uncomitted_bytes >= self.autocommit_threshold: - # Issue commits to limit additional space usage when recompressing chunks - target.recreate_uncomitted_bytes = 0 - self.repository.commit() if self.progress: target.stats.show_progress(item=item, dt=0.2) - if self.interrupt: - raise self.Interrupted({ - 'recreate_partial_chunks': new_chunks, - }) return new_chunks def create_chunk_iterator(self, archive, target, item): @@ -1552,19 +1516,6 @@ class ArchiveRecreater: chunk_iterator = _chunk_iterator() return chunk_iterator - def process_partial_chunks(self, target): - """Return chunks from a previous run for archive 'target' (if any) or an empty list.""" - if not target.recreate_partial_chunks: - return [] - # No incref, create_target_or_resume already did that before to deleting the old target archive - # So just copy these over - partial_chunks = target.recreate_partial_chunks - target.recreate_partial_chunks = None - for chunk_id, size, csize in partial_chunks: - self.seen_chunks.add(chunk_id) - logger.debug('Copied %d chunks from a partially processed item', len(partial_chunks)) - return partial_chunks - def save(self, archive, target, comment=None, completed=True, metadata=None, replace_original=True): """Save target archive. If completed, replace source. If not, save temporary with additional 'metadata' dict.""" if self.dry_run: @@ -1631,84 +1582,15 @@ class ArchiveRecreater: matcher.add(tag_files, True) matcher.add(tagged_dirs, False) - def create_target_or_resume(self, archive, target_name=None): - """Create new target archive or resume from temporary archive, if it exists. Return archive, resume from path""" + def create_target(self, archive, target_name=None): + """Create target archive.""" if self.dry_run: return self.FakeTargetArchive(), None target_name = target_name or archive.name + '.recreate' - resume = target_name in self.manifest.archives - target, resume_from = None, None - if resume: - target, resume_from = self.try_resume(archive, target_name) - if not target: - target = self.create_target_archive(target_name) + target = self.create_target_archive(target_name) # If the archives use the same chunker params, then don't rechunkify target.recreate_rechunkify = tuple(archive.metadata.get('chunker_params', [])) != self.chunker_params - return target, resume_from - - def try_resume(self, archive, target_name): - """Try to resume from temporary archive. Return (target archive, resume from path) if successful.""" - logger.info('Found %s, will resume interrupted operation', target_name) - old_target = self.open_archive(target_name) - if not self.can_resume(archive, old_target, target_name): - return None, None - target = self.create_target_archive(target_name + '.temp') - logger.info('Replaying items from interrupted operation...') - last_old_item = self.copy_items(old_target, target) - resume_from = getattr(last_old_item, 'path', None) - self.incref_partial_chunks(old_target, target) - old_target.delete(Statistics(), progress=self.progress) - logger.info('Done replaying items') - return target, resume_from - - def incref_partial_chunks(self, source_archive, target_archive): - target_archive.recreate_partial_chunks = source_archive.metadata.get('recreate_partial_chunks', []) - for chunk_id, size, csize in target_archive.recreate_partial_chunks: - if not self.cache.seen_chunk(chunk_id): - try: - # Repository has __contains__, RemoteRepository doesn't - # `chunk_id in repo` doesn't read the data though, so we try to use that if possible. - get_or_in = getattr(self.repository, '__contains__', self.repository.get) - if get_or_in(chunk_id) is False: - raise Repository.ObjectNotFound(chunk_id, self.repository) - except Repository.ObjectNotFound: - # delete/prune/check between invocations: these chunks are gone. - target_archive.recreate_partial_chunks = None - break - # fast-lane insert into chunks cache - self.cache.chunks[chunk_id] = (1, size, csize) - target_archive.stats.update(size, csize, True) - continue - # incref now, otherwise a source_archive.delete() might delete these chunks - self.cache.chunk_incref(chunk_id, target_archive.stats) - - def copy_items(self, source_archive, target_archive): - item = None - for item in source_archive.iter_items(): - if 'chunks' in item: - for chunk in item.chunks: - self.cache.chunk_incref(chunk.id, target_archive.stats) - target_archive.stats.nfiles += 1 - target_archive.add_item(item) - if self.progress: - target_archive.stats.show_progress(final=True) - return item - - def can_resume(self, archive, old_target, target_name): - resume_id = old_target.metadata.recreate_source_id - resume_args = [safe_decode(arg) for arg in old_target.metadata.recreate_args] - if resume_id != archive.id: - logger.warning('Source archive changed, will discard %s and start over', target_name) - logger.warning('Saved fingerprint: %s', bin_to_hex(resume_id)) - logger.warning('Current fingerprint: %s', archive.fpr) - old_target.delete(Statistics(), progress=self.progress) - return False - if resume_args != sys.argv[1:]: - logger.warning('Command line changed, this might lead to inconsistencies') - logger.warning('Saved: %s', repr(resume_args)) - logger.warning('Current: %s', repr(sys.argv[1:])) - # Just warn in this case, don't start over - return True + return target def create_target_archive(self, name): target = Archive(self.repository, self.key, self.manifest, name, create=True, diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 59f4e17b..3c63e37e 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1058,13 +1058,6 @@ class Archiver: @with_repository(cache=True, exclusive=True) def do_recreate(self, args, repository, manifest, key, cache): """Re-create archives""" - def interrupt(signal_num, stack_frame): - if recreater.interrupt: - print("\nReceived signal, again. I'm not deaf.", file=sys.stderr) - else: - print("\nReceived signal, will exit cleanly.", file=sys.stderr) - recreater.interrupt = True - msg = ("recreate is an experimental feature.\n" "Type 'YES' if you understand this and want to continue: ") if not yes(msg, false_msg="Aborting.", truish=('YES',), @@ -1084,30 +1077,27 @@ class Archiver: file_status_printer=self.print_file_status, dry_run=args.dry_run) - with signal_handler(signal.SIGTERM, interrupt), \ - signal_handler(signal.SIGINT, interrupt), \ - signal_handler(signal.SIGHUP, interrupt): - if args.location.archive: - name = args.location.archive + if args.location.archive: + name = args.location.archive + if recreater.is_temporary_archive(name): + self.print_error('Refusing to work on temporary archive of prior recreate: %s', name) + return self.exit_code + recreater.recreate(name, args.comment, args.target) + else: + if args.target is not None: + self.print_error('--target: Need to specify single archive') + return self.exit_code + for archive in manifest.archives.list(sort_by=['ts']): + name = archive.name if recreater.is_temporary_archive(name): - self.print_error('Refusing to work on temporary archive of prior recreate: %s', name) - return self.exit_code - recreater.recreate(name, args.comment, args.target) - else: - if args.target is not None: - self.print_error('--target: Need to specify single archive') - return self.exit_code - for archive in manifest.archives.list(sort_by=['ts']): - name = archive.name - if recreater.is_temporary_archive(name): - continue - print('Processing', name) - if not recreater.recreate(name, args.comment): - break - manifest.write() - repository.commit() - cache.commit() - return self.exit_code + continue + print('Processing', name) + if not recreater.recreate(name, args.comment): + break + manifest.write() + repository.commit() + cache.commit() + return self.exit_code @with_repository(manifest=False, exclusive=True) def do_with_lock(self, args, repository): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index ffa7cccd..da372056 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1717,106 +1717,6 @@ class ArchiverTestCase(ArchiverTestCaseBase): archives_after = self.cmd('list', self.repository_location + '::test') assert archives_after == archives_before - def _recreate_interrupt_patch(self, interrupt_after_n_1_files): - def interrupt(self, *args): - if interrupt_after_n_1_files: - self.interrupt = True - pi_save(self, *args) - else: - raise ArchiveRecreater.Interrupted - - def process_item_patch(*args): - return pi_call.pop(0)(*args) - - pi_save = ArchiveRecreater.process_item - pi_call = [pi_save] * interrupt_after_n_1_files + [interrupt] - return process_item_patch - - def _test_recreate_interrupt(self, change_args, interrupt_early): - self.create_test_files() - self.create_regular_file('dir2/abcdef', size=1024 * 80) - self.cmd('init', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') - process_files = 1 - if interrupt_early: - process_files = 0 - with patch.object(ArchiveRecreater, 'process_item', self._recreate_interrupt_patch(process_files)): - self.cmd('recreate', self.repository_location, 'input/dir2') - assert 'test.recreate' in self.cmd('list', self.repository_location) - if change_args: - with patch.object(sys, 'argv', sys.argv + ['non-forking tests don\'t use sys.argv']): - output = self.cmd('recreate', '-sv', '--list', '-pC', 'lz4', self.repository_location, 'input/dir2') - else: - output = self.cmd('recreate', '-sv', '--list', self.repository_location, 'input/dir2') - assert 'Found test.recreate, will resume' in output - assert change_args == ('Command line changed' in output) - if not interrupt_early: - assert 'Fast-forwarded to input/dir2/abcdef' in output - assert 'A input/dir2/abcdef' not in output - assert 'A input/dir2/file2' in output - archives = self.cmd('list', self.repository_location) - assert 'test.recreate' not in archives - assert 'test' in archives - files = self.cmd('list', self.repository_location + '::test') - assert 'dir2/file2' in files - assert 'dir2/abcdef' in files - assert 'file1' not in files - - # The _test_create_interrupt requires a deterministic (alphabetic) order of the files to easily check if - # resumption works correctly. Patch scandir_inorder to work in alphabetic order. - - def test_recreate_interrupt(self): - with patch.object(helpers, 'scandir_inorder', helpers.scandir_generic): - self._test_recreate_interrupt(False, True) - - def test_recreate_interrupt2(self): - with patch.object(helpers, 'scandir_inorder', helpers.scandir_generic): - self._test_recreate_interrupt(True, False) - - def _test_recreate_chunker_interrupt_patch(self): - real_add_chunk = Cache.add_chunk - - def add_chunk(*args, **kwargs): - frame = inspect.stack()[2] - try: - caller_self = frame[0].f_locals['self'] - if isinstance(caller_self, ArchiveRecreater): - caller_self.interrupt = True - finally: - del frame - return real_add_chunk(*args, **kwargs) - return add_chunk - - def test_recreate_rechunkify_interrupt(self): - self.create_regular_file('file1', size=1024 * 80) - self.cmd('init', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') - archive_before = self.cmd('list', self.repository_location + '::test', '--format', '{sha512}') - with patch.object(Cache, 'add_chunk', self._test_recreate_chunker_interrupt_patch()): - self.cmd('recreate', '-pv', '--chunker-params', '10,13,11,4095', self.repository_location) - assert 'test.recreate' in self.cmd('list', self.repository_location) - output = self.cmd('recreate', '-svp', '--debug', '--chunker-params', '10,13,11,4095', self.repository_location) - assert 'Found test.recreate, will resume' in output - assert 'Copied 1 chunks from a partially processed item' in output - archive_after = self.cmd('list', self.repository_location + '::test', '--format', '{sha512}') - assert archive_after == archive_before - - def test_recreate_changed_source(self): - self.create_test_files() - self.cmd('init', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') - with patch.object(ArchiveRecreater, 'process_item', self._recreate_interrupt_patch(1)): - self.cmd('recreate', self.repository_location, 'input/dir2') - assert 'test.recreate' in self.cmd('list', self.repository_location) - self.cmd('delete', self.repository_location + '::test') - self.cmd('create', self.repository_location + '::test', 'input') - output = self.cmd('recreate', self.repository_location, 'input/dir2') - assert 'Source archive changed, will discard test.recreate and start over' in output - - def test_recreate_refuses_temporary(self): - self.cmd('init', self.repository_location) - self.cmd('recreate', self.repository_location + '::cba.recreate', exit_code=2) - def test_recreate_skips_nothing_to_do(self): self.create_regular_file('file1', size=1024 * 80) self.cmd('init', self.repository_location) From d7284a92f02a9058042823417055cef602ea5ec9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 19 Nov 2016 21:38:13 +0100 Subject: [PATCH 0403/1387] docs: clarify --umask usage, fixes #1859 --- docs/usage.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index 2a3538b2..09d1e2b4 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -746,6 +746,17 @@ a new repository when changing chunker params. For more details, see :ref:`chunker_details`. + +--umask +~~~~~~~ + +If you use ``--umask``, make sure that all repository-modifying borg commands +(create, delete, prune) that access the repository in question use the same +``--umask`` value. + +If multiple machines access the same repository, this should hold true for all +of them. + --read-special ~~~~~~~~~~~~~~ From 3d174aef3517a385a2f5cd71a37f7f7e10e0a677 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 19 Nov 2016 21:52:11 +0100 Subject: [PATCH 0404/1387] docs: clarify passphrase mode attic repo upgrade, fixes #1854 --- docs/usage.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index 09d1e2b4..14ecfbb8 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -629,6 +629,20 @@ Examples no key file found for repository +Upgrading a passphrase encrypted attic repo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +attic offered a "passphrase" encryption mode, but this was removed in borg 1.0 +and replaced by the "repokey" mode (which stores the passphrase-protected +encryption key into the repository config). + +Thus, to upgrade a "passphrase" attic repo to a "repokey" borg repo, 2 steps +are needed, in this order: + +- borg upgrade repo +- borg migrate-to-repokey repo + + .. include:: usage/break-lock.rst.inc From d8e806aac10d18ae03da18214523257096cb01d9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 19 Nov 2016 22:04:13 +0100 Subject: [PATCH 0405/1387] docs: datetime formatting examples for {now} placeholder, fixes #1822 --- docs/usage.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/usage.rst b/docs/usage.rst index 14ecfbb8..9a0dc710 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -382,7 +382,10 @@ Examples # Use short hostname, user name and current time in archive name $ borg create /path/to/repo::{hostname}-{user}-{now} ~ - $ borg create /path/to/repo::{hostname}-{user}-{now:%Y-%m-%d_%H:%M:%S} ~ + # Similar, use the same datetime format as borg 1.1 will have as default + $ borg create /path/to/repo::{hostname}-{user}-{now:%Y-%m-%dT%H:%M:%S} ~ + # As above, but add nanoseconds + $ borg create /path/to/repo::{hostname}-{user}-{now:%Y-%m-%dT%H:%M:%S.%f} ~ .. include:: usage/extract.rst.inc From 5fe5b6b339d20b5c19fdd7fbd7d12747b7f07ecb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 19 Nov 2016 22:26:07 +0100 Subject: [PATCH 0406/1387] docs: different pattern matching for --exclude, fixes #1779 --- docs/usage.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index 9a0dc710..61b469b1 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -387,6 +387,15 @@ Examples # As above, but add nanoseconds $ borg create /path/to/repo::{hostname}-{user}-{now:%Y-%m-%dT%H:%M:%S.%f} ~ +Notes +~~~~~ + +- the --exclude patterns are not like tar. In tar --exclude .bundler/gems will + exclude foo/.bundler/gems. In borg it will not, you need to use --exclude + '*/.bundler/gems' to get the same effect. See ``borg help patterns`` for + more information. + + .. include:: usage/extract.rst.inc Examples From cabcbc58872876fe8ff1ab4416a7d56e3a9f13fd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 20 Nov 2016 00:08:33 +0100 Subject: [PATCH 0407/1387] fix determination of newest mtime, fixes #1860 bug: if no files were added/modified, _newest_mtime stayed at its initial 0 value. when saving the files cache, this killed all age 0 entries. Now using None as initial value, so we can spot that circumstance. The 2 ** 63 - 1 value is just so it is MAX_INT on a 64bit platform, for better performance. It can be easily increased when y2262 is coming. --- borg/cache.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/borg/cache.py b/borg/cache.py index 53fdae7c..2c09d93a 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -191,7 +191,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def _read_files(self): self.files = {} - self._newest_mtime = 0 + self._newest_mtime = None logger.debug('Reading files cache ...') with open(os.path.join(self.path, 'files'), 'rb') as fd: u = msgpack.Unpacker(use_list=True) @@ -222,6 +222,9 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if not self.txn_active: return if self.files is not None: + if self._newest_mtime is None: + # was never set because no files were modified/added + self._newest_mtime = 2 ** 63 - 1 # nanoseconds, good until y2262 ttl = int(os.environ.get('BORG_FILES_CACHE_TTL', 20)) with open(os.path.join(self.path, 'files'), 'wb') as fd: for path_hash, item in self.files.items(): @@ -451,4 +454,4 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" # Entry: Age, inode, size, mtime, chunk ids mtime_ns = st.st_mtime_ns self.files[path_hash] = msgpack.packb((0, st.st_ino, st.st_size, int_to_bigint(mtime_ns), ids)) - self._newest_mtime = max(self._newest_mtime, mtime_ns) + self._newest_mtime = max(self._newest_mtime or 0, mtime_ns) From 93b03ea23109004d8876c1af1690e8c9ec33bcfc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 19 Nov 2016 19:09:47 +0100 Subject: [PATCH 0408/1387] recreate: re-use existing checkpoint functionality --- src/borg/archive.py | 161 ++++++++++++++++++++----------------------- src/borg/archiver.py | 6 +- 2 files changed, 79 insertions(+), 88 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 269263f9..cc90f165 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -6,6 +6,7 @@ import sys import time from contextlib import contextmanager from datetime import datetime, timezone +from functools import partial from getpass import getuser from io import BytesIO from itertools import groupby @@ -741,28 +742,32 @@ Number of files: {0.stats.nfiles}'''.format( self.add_item(item) return 's' # symlink - def chunk_file(self, item, cache, stats, fd, fh=-1, **chunk_kw): - def write_part(item, from_chunk, number): - item = Item(internal_dict=item.as_dict()) - length = len(item.chunks) - # the item should only have the *additional* chunks we processed after the last partial item: - item.chunks = item.chunks[from_chunk:] - item.path += '.borg_part_%d' % number - item.part = number - number += 1 - self.add_item(item, show_progress=False) - self.write_checkpoint() - return length, number + def write_part_file(self, item, from_chunk, number): + item = Item(internal_dict=item.as_dict()) + length = len(item.chunks) + # the item should only have the *additional* chunks we processed after the last partial item: + item.chunks = item.chunks[from_chunk:] + item.path += '.borg_part_%d' % number + item.part = number + number += 1 + self.add_item(item, show_progress=False) + self.write_checkpoint() + return length, number + + def chunk_file(self, item, cache, stats, chunk_iter, chunk_processor=None, **chunk_kw): + if not chunk_processor: + def chunk_processor(data): + return cache.add_chunk(self.key.id_hash(data), Chunk(data, **chunk_kw), stats) item.chunks = [] from_chunk = 0 part_number = 1 - for data in backup_io_iter(self.chunker.chunkify(fd, fh)): - item.chunks.append(cache.add_chunk(self.key.id_hash(data), Chunk(data, **chunk_kw), stats)) + for data in chunk_iter: + item.chunks.append(chunk_processor(data)) if self.show_progress: self.stats.show_progress(item=item, dt=0.2) if self.checkpoint_interval and time.time() - self.last_checkpoint > self.checkpoint_interval: - from_chunk, part_number = write_part(item, from_chunk, part_number) + from_chunk, part_number = self.write_part_file(item, from_chunk, part_number) self.last_checkpoint = time.time() else: if part_number > 1: @@ -770,7 +775,7 @@ Number of files: {0.stats.nfiles}'''.format( # if we already have created a part item inside this file, we want to put the final # chunks (if any) into a part item also (so all parts can be concatenated to get # the complete file): - from_chunk, part_number = write_part(item, from_chunk, part_number) + from_chunk, part_number = self.write_part_file(item, from_chunk, part_number) self.last_checkpoint = time.time() # if we created part files, we have referenced all chunks from the part files, @@ -789,7 +794,7 @@ Number of files: {0.stats.nfiles}'''.format( mtime=t, atime=t, ctime=t, ) fd = sys.stdin.buffer # binary - self.chunk_file(item, cache, self.stats, fd) + self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd))) self.stats.nfiles += 1 self.add_item(item) return 'i' # stdin @@ -845,7 +850,7 @@ Number of files: {0.stats.nfiles}'''.format( with backup_io(): fh = Archive._open_rb(path) with os.fdopen(fh, 'rb') as fd: - self.chunk_file(item, cache, self.stats, fd, fh, compress=compress) + self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd, fh)), compress=compress) if not is_special_file: # we must not memorize special files, because the contents of e.g. a # block or char device will change without its mtime/size/inode changing. @@ -1386,7 +1391,8 @@ class ArchiveRecreater: def __init__(self, repository, manifest, key, cache, matcher, exclude_caches=False, exclude_if_present=None, keep_tag_files=False, chunker_params=None, compression=None, compression_files=None, always_recompress=False, - dry_run=False, stats=False, progress=False, file_status_printer=None): + dry_run=False, stats=False, progress=False, file_status_printer=None, + checkpoint_interval=1800): self.repository = repository self.key = key self.manifest = manifest @@ -1410,9 +1416,7 @@ class ArchiveRecreater: self.stats = stats self.progress = progress self.print_file_status = file_status_printer or (lambda *args: None) - - self.interrupt = False - self.errors = False + self.checkpoint_interval = checkpoint_interval def recreate(self, archive_name, comment=None, target_name=None): assert not self.is_temporary_archive(archive_name) @@ -1466,7 +1470,7 @@ class ArchiveRecreater: def process_item(self, archive, target, item): if 'chunks' in item: - item.chunks = self.process_chunks(archive, target, item) + self.process_chunks(archive, target, item) target.stats.nfiles += 1 target.add_item(item) self.print_file_status(file_status(item.mode), item.path) @@ -1477,77 +1481,62 @@ class ArchiveRecreater: for chunk_id, size, csize in item.chunks: self.cache.chunk_incref(chunk_id, target.stats) return item.chunks - new_chunks = [] - chunk_iterator = self.create_chunk_iterator(archive, target, item) + chunk_iterator = self.create_chunk_iterator(archive, target, list(item.chunks)) compress = self.compression_decider1.decide(item.path) - for chunk in chunk_iterator: - chunk.meta['compress'] = compress - chunk_id = self.key.id_hash(chunk.data) - if chunk_id in self.seen_chunks: - new_chunks.append(self.cache.chunk_incref(chunk_id, target.stats)) - else: - compression_spec, chunk = self.key.compression_decider2.decide(chunk) - overwrite = self.recompress - if self.recompress and not self.always_recompress and chunk_id in self.cache.chunks: - # Check if this chunk is already compressed the way we want it - old_chunk = self.key.decrypt(None, self.repository.get(chunk_id), decompress=False) - if Compressor.detect(old_chunk.data).name == compression_spec['name']: - # Stored chunk has the same compression we wanted - overwrite = False - chunk_id, size, csize = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite) - new_chunks.append((chunk_id, size, csize)) - self.seen_chunks.add(chunk_id) - if self.progress: - target.stats.show_progress(item=item, dt=0.2) - return new_chunks + chunk_processor = partial(self.chunk_processor, target, compress) + target.chunk_file(item, self.cache, target.stats, chunk_iterator, chunk_processor) - def create_chunk_iterator(self, archive, target, item): + def chunk_processor(self, target, compress, data): + chunk_id = self.key.id_hash(data) + if chunk_id in self.seen_chunks: + return self.cache.chunk_incref(chunk_id, target.stats) + chunk = Chunk(data, compress=compress) + compression_spec, chunk = self.key.compression_decider2.decide(chunk) + overwrite = self.recompress + if self.recompress and not self.always_recompress and chunk_id in self.cache.chunks: + # Check if this chunk is already compressed the way we want it + old_chunk = self.key.decrypt(None, self.repository.get(chunk_id), decompress=False) + if Compressor.detect(old_chunk.data).name == compression_spec['name']: + # Stored chunk has the same compression we wanted + overwrite = False + chunk_id, size, csize = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite) + self.seen_chunks.add(chunk_id) + return chunk_id, size, csize + + def create_chunk_iterator(self, archive, target, chunks): """Return iterator of chunks to store for 'item' from 'archive' in 'target'.""" - chunk_iterator = archive.pipeline.fetch_many([chunk_id for chunk_id, _, _ in item.chunks]) + chunk_iterator = archive.pipeline.fetch_many([chunk_id for chunk_id, _, _ in chunks]) if target.recreate_rechunkify: # The target.chunker will read the file contents through ChunkIteratorFileWrapper chunk-by-chunk # (does not load the entire file into memory) file = ChunkIteratorFileWrapper(chunk_iterator) + return target.chunker.chunkify(file) + else: + for chunk in chunk_iterator: + yield chunk.data - def _chunk_iterator(): - for data in target.chunker.chunkify(file): - yield Chunk(data) - - chunk_iterator = _chunk_iterator() - return chunk_iterator - - def save(self, archive, target, comment=None, completed=True, metadata=None, replace_original=True): + def save(self, archive, target, comment=None, replace_original=True): """Save target archive. If completed, replace source. If not, save temporary with additional 'metadata' dict.""" if self.dry_run: - return completed - if completed: - timestamp = archive.ts.replace(tzinfo=None) - if comment is None: - comment = archive.metadata.get('comment', '') - target.save(timestamp=timestamp, comment=comment, additional_metadata={ - 'cmdline': archive.metadata.cmdline, - 'recreate_cmdline': sys.argv, - }) - if replace_original: - archive.delete(Statistics(), progress=self.progress) - target.rename(archive.name) - if self.stats: - target.end = datetime.utcnow() - log_multi(DASHES, - str(target), - DASHES, - str(target.stats), - str(self.cache), - DASHES) - else: - additional_metadata = metadata or {} - additional_metadata.update({ - 'recreate_source_id': archive.id, - 'recreate_args': sys.argv[1:], - }) - target.save(name=archive.name + '.recreate', additional_metadata=additional_metadata) - logger.info('Run the same command again to resume.') - return completed + return + timestamp = archive.ts.replace(tzinfo=None) + if comment is None: + comment = archive.metadata.get('comment', '') + target.save(timestamp=timestamp, comment=comment, additional_metadata={ + 'cmdline': archive.metadata.cmdline, + 'recreate_cmdline': sys.argv, + }) + if replace_original: + archive.delete(Statistics(), progress=self.progress) + target.rename(archive.name) + if self.stats: + target.end = datetime.utcnow() + log_multi(DASHES, + str(target), + DASHES, + str(target.stats), + str(self.cache), + DASHES) def matcher_add_tagged_dirs(self, archive): """Add excludes to the matcher created by exclude_cache and exclude_if_present.""" @@ -1595,9 +1584,7 @@ class ArchiveRecreater: def create_target_archive(self, name): target = Archive(self.repository, self.key, self.manifest, name, create=True, progress=self.progress, chunker_params=self.chunker_params, cache=self.cache, - checkpoint_interval=0, compression=self.compression) - target.recreate_partial_chunks = None - target.recreate_uncomitted_bytes = 0 + checkpoint_interval=self.checkpoint_interval, compression=self.compression) return target def open_archive(self, name, **kwargs): diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 3c63e37e..1eff7c5e 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1075,6 +1075,7 @@ class Archiver: always_recompress=args.always_recompress, progress=args.progress, stats=args.stats, file_status_printer=self.print_file_status, + checkpoint_interval=args.checkpoint_interval, dry_run=args.dry_run) if args.location.archive: @@ -2412,6 +2413,9 @@ class Archiver: type=archivename_validator(), help='create a new archive with the name ARCHIVE, do not replace existing archive ' '(only applies for a single archive)') + archive_group.add_argument('-c', '--checkpoint-interval', dest='checkpoint_interval', + type=int, default=1800, metavar='SECONDS', + help='write checkpoint every SECONDS seconds (Default: 1800)') archive_group.add_argument('--comment', dest='comment', metavar='COMMENT', default=None, help='add a comment text to the archive') archive_group.add_argument('--timestamp', dest='timestamp', @@ -2424,7 +2428,7 @@ class Archiver: help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') archive_group.add_argument('--always-recompress', dest='always_recompress', action='store_true', - help='always recompress chunks, don\'t skip chunks already compressed with the same' + help='always recompress chunks, don\'t skip chunks already compressed with the same ' 'algorithm.') archive_group.add_argument('--compression-from', dest='compression_files', type=argparse.FileType('r'), action='append', From 15cefe8ddebc7d226c6f05be44d1a9f1f94d9bea Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 20 Nov 2016 13:31:46 +0100 Subject: [PATCH 0409/1387] recreate/archiver tests: add check_cache tool - lints refcounts --- src/borg/testsuite/archiver.py | 55 +++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index da372056..847b4ca9 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -29,7 +29,7 @@ from ..archiver import Archiver from ..cache import Cache from ..constants import * # NOQA from ..crypto import bytes_to_long, num_aes_blocks -from ..helpers import PatternMatcher, parse_pattern +from ..helpers import PatternMatcher, parse_pattern, Location from ..helpers import Chunk, Manifest from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from ..helpers import bin_to_hex @@ -260,6 +260,9 @@ class ArchiverTestCaseBase(BaseTestCase): archive = Archive(repository, key, manifest, name) return archive, repository + def open_repository(self): + return Repository(self.repository_path, exclusive=True) + def create_regular_file(self, name, size=0, contents=None): filename = os.path.join(self.input_path, name) if not os.path.exists(os.path.dirname(filename)): @@ -1626,6 +1629,40 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', self.repository_location, exit_code=1) assert not os.path.exists(self.repository_location) + def check_cache(self): + # First run a regular borg check + self.cmd('check', self.repository_location) + # Then check that the cache on disk matches exactly what's in the repo. + with self.open_repository() as repository: + manifest, key = Manifest.load(repository) + with Cache(repository, key, manifest, sync=False) as cache: + original_chunks = cache.chunks + cache.destroy(repository) + with Cache(repository, key, manifest) as cache: + correct_chunks = cache.chunks + assert original_chunks is not correct_chunks + seen = set() + for id, (refcount, size, csize) in correct_chunks.iteritems(): + o_refcount, o_size, o_csize = original_chunks[id] + assert refcount == o_refcount + assert size == o_size + assert csize == o_csize + seen.add(id) + for id, (refcount, size, csize) in original_chunks.iteritems(): + assert id in seen + + def test_check_cache(self): + self.cmd('init', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + with self.open_repository() as repository: + manifest, key = Manifest.load(repository) + with Cache(repository, key, manifest, sync=False) as cache: + cache.begin_txn() + cache.chunks.incref(list(cache.chunks.iteritems())[0][0]) + cache.commit() + with pytest.raises(AssertionError): + self.check_cache() + def test_recreate_target_rc(self): self.cmd('init', self.repository_location) output = self.cmd('recreate', self.repository_location, '--target=asdf', exit_code=2) @@ -1634,10 +1671,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_target(self): self.create_test_files() self.cmd('init', self.repository_location) + self.check_cache() archive = self.repository_location + '::test0' self.cmd('create', archive, 'input') + self.check_cache() original_archive = self.cmd('list', self.repository_location) self.cmd('recreate', archive, 'input/dir2', '-e', 'input/dir2/file3', '--target=new-archive') + self.check_cache() archives = self.cmd('list', self.repository_location) assert original_archive in archives assert 'new-archive' in archives @@ -1655,6 +1695,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): archive = self.repository_location + '::test0' self.cmd('create', archive, 'input') self.cmd('recreate', archive, 'input/dir2', '-e', 'input/dir2/file3') + self.check_cache() listing = self.cmd('list', '--short', archive) assert 'file1' not in listing assert 'dir2/file2' in listing @@ -1666,6 +1707,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self._extract_hardlinks_setup() self.cmd('create', self.repository_location + '::test2', 'input') self.cmd('recreate', self.repository_location + '::test', 'input/dir1') + self.check_cache() with changedir('output'): self.cmd('extract', self.repository_location + '::test') assert os.stat('input/dir1/hardlink').st_nlink == 2 @@ -1689,6 +1731,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # test1 and test2 do not deduplicate assert num_chunks == unique_chunks self.cmd('recreate', self.repository_location, '--chunker-params', 'default') + self.check_cache() # test1 and test2 do deduplicate after recreate assert not int(self.cmd('list', self.repository_location + '::test1', 'input/large_file', '--format', '{unique_chunks}')) @@ -1702,6 +1745,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): size, csize, sha256_before = file_list.split(' ') assert int(csize) >= int(size) # >= due to metadata overhead self.cmd('recreate', self.repository_location, '-C', 'lz4') + self.check_cache() file_list = self.cmd('list', self.repository_location + '::test', 'input/compressible', '--format', '{size} {csize} {sha256}') size, csize, sha256_after = file_list.split(' ') @@ -1714,6 +1758,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('create', self.repository_location + '::test', 'input') archives_before = self.cmd('list', self.repository_location + '::test') self.cmd('recreate', self.repository_location, '-n', '-e', 'input/compressible') + self.check_cache() archives_after = self.cmd('list', self.repository_location + '::test') assert archives_after == archives_before @@ -1723,6 +1768,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('create', self.repository_location + '::test', 'input') info_before = self.cmd('info', self.repository_location + '::test') self.cmd('recreate', self.repository_location, '--chunker-params', 'default') + self.check_cache() info_after = self.cmd('info', self.repository_location + '::test') assert info_before == info_after # includes archive ID @@ -1743,18 +1789,22 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('create', self.repository_location + '::test', 'input') output = self.cmd('recreate', '--list', '--info', self.repository_location + '::test', '-e', 'input/file2') + self.check_cache() self.assert_in("input/file1", output) self.assert_in("x input/file2", output) output = self.cmd('recreate', '--list', self.repository_location + '::test', '-e', 'input/file3') + self.check_cache() self.assert_in("input/file1", output) self.assert_in("x input/file3", output) output = self.cmd('recreate', self.repository_location + '::test', '-e', 'input/file4') + self.check_cache() self.assert_not_in("input/file1", output) self.assert_not_in("x input/file4", output) output = self.cmd('recreate', '--info', self.repository_location + '::test', '-e', 'input/file5') + self.check_cache() self.assert_not_in("input/file1", output) self.assert_not_in("x input/file5", output) @@ -2095,6 +2145,9 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): class RemoteArchiverTestCase(ArchiverTestCase): prefix = '__testsuite__:' + def open_repository(self): + return RemoteRepository(Location(self.repository_location)) + def test_remote_repo_restrict_to_path(self): # restricted to repo directory itself: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', self.repository_path]): From c8b58e0fd83e386147b0bdf8d2b20c8e933dfee3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 24 Nov 2016 01:53:23 +0100 Subject: [PATCH 0410/1387] improve cache / index docs, esp. files cache docs, fixes #1825 --- docs/internals.rst | 102 +++++++++++++++++++++++++++++++++------------ 1 file changed, 76 insertions(+), 26 deletions(-) diff --git a/docs/internals.rst b/docs/internals.rst index 798ce856..4df70e94 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -252,44 +252,94 @@ For some more general usage hints see also ``--chunker-params``. Indexes / Caches ---------------- -The **files cache** is stored in ``cache/files`` and is indexed on the -``file path hash``. At backup time, it is used to quickly determine whether we -need to chunk a given file (or whether it is unchanged and we already have all -its pieces). -It contains: +The **files cache** is stored in ``cache/files`` and is used at backup time to +quickly determine whether a given file is unchanged and we have all its chunks. -* age -* file inode number -* file size -* file mtime_ns -* file content chunk hashes +The files cache is a key -> value mapping and contains: -The inode number is stored to make sure we distinguish between +* key: + + - full, absolute file path id_hash +* value: + + - file inode number + - file size + - file mtime_ns + - list of file content chunk id hashes + - age (0 [newest], 1, 2, 3, ..., BORG_FILES_CACHE_TTL - 1) + +To determine whether a file has not changed, cached values are looked up via +the key in the mapping and compared to the current file attribute values. + +If the file's size, mtime_ns and inode number is still the same, it is +considered to not have changed. In that case, we check that all file content +chunks are (still) present in the repository (we check that via the chunks +cache). + +If everything is matching and all chunks are present, the file is not read / +chunked / hashed again (but still a file metadata item is written to the +archive, made from fresh file metadata read from the filesystem). This is +what makes borg so fast when processing unchanged files. + +If there is a mismatch or a chunk is missing, the file is read / chunked / +hashed. Chunks already present in repo won't be transferred to repo again. + +The inode number is stored and compared to make sure we distinguish between different files, as a single path may not be unique across different archives in different setups. -The files cache is stored as a python associative array storing -python objects, which generates a lot of overhead. +Not all filesystems have stable inode numbers. If that is the case, borg can +be told to ignore the inode number in the check via --ignore-inode. -The **chunks cache** is stored in ``cache/chunks`` and is indexed on the -``chunk id_hash``. It is used to determine whether we already have a specific -chunk, to count references to it and also for statistics. -It contains: +The age value is used for cache management. If a file is "seen" in a backup +run, its age is reset to 0, otherwise its age is incremented by one. +If a file was not seen in BORG_FILES_CACHE_TTL backups, its cache entry is +removed. See also: :ref:`always_chunking` and :ref:`a_status_oddity` -* reference count -* size -* encrypted/compressed size +The files cache is a python dictionary, storing python objects, which +generates a lot of overhead. -The **repository index** is stored in ``repo/index.%d`` and is indexed on the -``chunk id_hash``. It is used to determine a chunk's location in the repository. -It contains: +Borg can also work without using the files cache (saves memory if you have a +lot of files or not much RAM free), then all files are assumed to have changed. +This is usually much slower than with files cache. -* segment (that contains the chunk) -* offset (where the chunk is located in the segment) +The **chunks cache** is stored in ``cache/chunks`` and is used to determine +whether we already have a specific chunk, to count references to it and also +for statistics. + +The chunks cache is a key -> value mapping and contains: + +* key: + + - chunk id_hash +* value: + + - reference count + - size + - encrypted/compressed size + +The chunks cache is a hashindex, a hash table implemented in C and tuned for +memory efficiency. + +The **repository index** is stored in ``repo/index.%d`` and is used to +determine a chunk's location in the repository. + +The repo index is a key -> value mapping and contains: + +* key: + + - chunk id_hash +* value: + + - segment (that contains the chunk) + - offset (where the chunk is located in the segment) + +The repo index is a hashindex, a hash table implemented in C and tuned for +memory efficiency. -The repository index file is random access. Hints are stored in a file (``repo/hints.%d``). + It contains: * version From 6bb363c7070c12ff1051025a7cb483c96c57f24c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 24 Nov 2016 02:27:38 +0100 Subject: [PATCH 0411/1387] Vagrantfile: cosmetic: remove tabs / empty lines --- Vagrantfile | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 646669af..6e6b5610 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -172,14 +172,14 @@ def packages_cygwin(version) set CYGSETUP=#{setup_exe} REM --- Install build version of CygWin in a subfolder set OURPATH=%cd% - set CYGBUILD="C:\\cygwin\\CygWin" - set CYGMIRROR=ftp://mirrors.kernel.org/sourceware/cygwin/ - set BUILDPKGS=python3,python3-setuptools,binutils,gcc-g++,libopenssl,openssl-devel,git,make,openssh,liblz4-devel,liblz4_1,rsync,curl,python-devel + set CYGBUILD="C:\\cygwin\\CygWin" + set CYGMIRROR=ftp://mirrors.kernel.org/sourceware/cygwin/ + set BUILDPKGS=python3,python3-setuptools,binutils,gcc-g++,libopenssl,openssl-devel,git,make,openssh,liblz4-devel,liblz4_1,rsync,curl,python-devel %CYGSETUP% -q -B -o -n -R %CYGBUILD% -L -D -s %CYGMIRROR% -P %BUILDPKGS% cd /d C:\\cygwin\\CygWin\\bin regtool set /HKLM/SYSTEM/CurrentControlSet/Services/OpenSSHd/ImagePath "C:\\cygwin\\CygWin\\bin\\cygrunsrv.exe" bash -c "ssh-host-config --no" - ' > /cygdrive/c/cygwin/install.bat + ' > /cygdrive/c/cygwin/install.bat cd /cygdrive/c/cygwin && cmd.exe /c install.bat echo "alias mkdir='mkdir -p'" > ~/.profile @@ -201,7 +201,6 @@ def install_cygwin_venv EOF end - def install_pyenv(boxname) return <<-EOF curl -s -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash @@ -344,13 +343,11 @@ end def fix_perms return <<-EOF # . ~/.profile - if id "vagrant" >/dev/null 2>&1; then chown -R vagrant /vagrant/borg else chown -R ubuntu /vagrant/borg fi - EOF end From 1dc51e6df8f4302c034cfbf3668787904283a96c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 24 Nov 2016 04:03:54 +0100 Subject: [PATCH 0412/1387] Vagrantfile: reduce code duplication --- Vagrantfile | 102 +++++++++++++++++++++++----------------------------- 1 file changed, 45 insertions(+), 57 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 6e6b5610..f5524840 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -247,8 +247,8 @@ def build_pyenv_venv(boxname) EOF end -def install_borg(boxname) - return <<-EOF +def install_borg(fuse) + script = <<-EOF . ~/.bash_profile cd /vagrant/borg . borg-env/bin/activate @@ -259,55 +259,43 @@ def install_borg(boxname) rm -f borg/{chunker,crypto,compress,hashindex,platform_linux}.c rm -rf borg/__pycache__ borg/support/__pycache__ borg/testsuite/__pycache__ pip install -r requirements.d/development.txt - # by using [fuse], setup.py can handle different fuse requirements: - pip install -e .[fuse] EOF + if fuse + script += <<-EOF + # by using [fuse], setup.py can handle different fuse requirements: + pip install -e .[fuse] + EOF + else + script += <<-EOF + pip install -e . + # do not install llfuse into the virtualenvs built by tox: + sed -i.bak '/fuse.txt/d' tox.ini + EOF + end + return script end -def install_borg_no_fuse(boxname) - return <<-EOF - . ~/.bash_profile - cd /vagrant/borg - . borg-env/bin/activate - pip install -U wheel # upgrade wheel, too old for 3.5 - cd borg - # clean up (wrong/outdated) stuff we likely got via rsync: - rm -f borg/*.so borg/*.cpy* - rm -f borg/{chunker,crypto,compress,hashindex,platform_linux}.c - rm -rf borg/__pycache__ borg/support/__pycache__ borg/testsuite/__pycache__ - pip install -r requirements.d/development.txt - pip install -e . - # do not install llfuse into the virtualenvs built by tox: - sed -i.bak '/fuse.txt/d' tox.ini - EOF -end - -def install_pyinstaller(boxname) - return <<-EOF +def install_pyinstaller(bootloader) + script = <<-EOF . ~/.bash_profile cd /vagrant/borg . borg-env/bin/activate git clone https://github.com/pyinstaller/pyinstaller.git cd pyinstaller git checkout v3.1.1 + EOF + if bootloader + script += <<-EOF + # build bootloader, if it is not included + cd bootloader + python ./waf all + cd .. + EOF + end + script += <<-EOF pip install -e . EOF -end - -def install_pyinstaller_bootloader(boxname) - return <<-EOF - . ~/.bash_profile - cd /vagrant/borg - . borg-env/bin/activate - git clone https://github.com/pyinstaller/pyinstaller.git - cd pyinstaller - git checkout v3.1.1 - # build bootloader, if it is not included - cd bootloader - python ./waf all - cd .. - pip install -e . - EOF + return script end def build_binary_with_pyinstaller(boxname) @@ -375,7 +363,7 @@ Vagrant.configure(2) do |config| b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos7_64") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("centos7_64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("centos7_64") - b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("centos7_64") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("centos7_64") end @@ -385,7 +373,7 @@ Vagrant.configure(2) do |config| b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos6_32") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("centos6_32") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("centos6_32") - b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg_no_fuse("centos6_32") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(false) b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("centos6_32") end @@ -398,7 +386,7 @@ Vagrant.configure(2) do |config| b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos6_64") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("centos6_64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("centos6_64") - b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg_no_fuse("centos6_64") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(false) b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("centos6_64") end @@ -409,7 +397,7 @@ Vagrant.configure(2) do |config| end b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("xenial64") - b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("xenial64") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("xenial64") end @@ -420,7 +408,7 @@ Vagrant.configure(2) do |config| end b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("trusty64") - b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("trusty64") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("trusty64") end @@ -431,7 +419,7 @@ Vagrant.configure(2) do |config| end b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("jessie64") - b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("jessie64") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("jessie64") end @@ -442,8 +430,8 @@ Vagrant.configure(2) do |config| b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("wheezy32") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("wheezy32") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("wheezy32") - b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("wheezy32") - b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller("wheezy32") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) + b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller(false) b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("wheezy32") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("wheezy32") end @@ -455,8 +443,8 @@ Vagrant.configure(2) do |config| b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("wheezy64") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("wheezy64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("wheezy64") - b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("wheezy64") - b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller("wheezy64") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) + b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller(false) b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("wheezy64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("wheezy64") end @@ -469,8 +457,8 @@ Vagrant.configure(2) do |config| b.vm.provision "fix pyenv", :type => :shell, :privileged => false, :inline => fix_pyenv_darwin("darwin64") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("darwin64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("darwin64") - b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("darwin64") - b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller("darwin64") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) + b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller(false) b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("darwin64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("darwin64") end @@ -485,8 +473,8 @@ Vagrant.configure(2) do |config| b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("freebsd") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("freebsd") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("freebsd") - b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("freebsd") - b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller_bootloader("freebsd") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) + b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller(true) b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("freebsd") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("freebsd") end @@ -498,7 +486,7 @@ Vagrant.configure(2) do |config| end b.vm.provision "packages openbsd", :type => :shell, :inline => packages_openbsd b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("openbsd64") - b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg_no_fuse("openbsd64") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(false) b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("openbsd64") end @@ -509,7 +497,7 @@ Vagrant.configure(2) do |config| end b.vm.provision "packages netbsd", :type => :shell, :inline => packages_netbsd b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("netbsd64") - b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg_no_fuse("netbsd64") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(false) b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("netbsd64") end @@ -536,7 +524,7 @@ Vagrant.configure(2) do |config| b.vm.provision :reload b.vm.provision "cygwin install pip", :type => :shell, :privileged => false, :inline => install_cygwin_venv b.vm.provision "cygwin build env", :type => :shell, :privileged => false, :inline => build_sys_venv("windows10") - b.vm.provision "cygwin install borg", :type => :shell, :privileged => false, :inline => install_borg_no_fuse("windows10") + b.vm.provision "cygwin install borg", :type => :shell, :privileged => false, :inline => install_borg(false) b.vm.provision "cygwin run tests", :type => :shell, :privileged => false, :inline => run_tests("windows10") end end From e999f3ff5178f10d05a3f18443bc602a63fb2f84 Mon Sep 17 00:00:00 2001 From: Abogical Date: Thu, 24 Nov 2016 01:26:29 +0200 Subject: [PATCH 0413/1387] Add ProgressIndicatorMessage and abstract class ProgressIndicatorBase --- src/borg/helpers.py | 57 ++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index ab2e1271..d6c71d05 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1209,23 +1209,10 @@ def ellipsis_truncate(msg, space): return msg + ' ' * (space - msg_width) -class ProgressIndicatorPercent: +class ProgressIndicatorBase: LOGGER = 'borg.output.progress' - def __init__(self, total=0, step=5, start=0, msg="%3.0f%%"): - """ - Percentage-based progress indicator - - :param total: total amount of items - :param step: step size in percent - :param start: at which percent value to start - :param msg: output message, must contain one %f placeholder for the percentage - """ - self.counter = 0 # 0 .. (total-1) - self.total = total - self.trigger_at = start # output next percentage value when reaching (at least) this - self.step = step - self.msg = msg + def __init__(self): self.handler = None self.logger = logging.getLogger(self.LOGGER) @@ -1247,6 +1234,41 @@ class ProgressIndicatorPercent: self.logger.removeHandler(self.handler) self.handler.close() + +def justify_to_terminal_size(message): + terminal_space = get_terminal_size(fallback=(-1, -1))[0] + # justify only if we are outputting to a terminal + if terminal_space != -1: + return message.ljust(terminal_space) + return message + + +class ProgressIndicatorMessage(ProgressIndicatorBase): + def output(self, msg): + self.logger.info(justify_to_terminal_size(msg)) + + def finish(self): + self.output('') + + +class ProgressIndicatorPercent(ProgressIndicatorBase): + def __init__(self, total=0, step=5, start=0, msg="%3.0f%%"): + """ + Percentage-based progress indicator + + :param total: total amount of items + :param step: step size in percent + :param start: at which percent value to start + :param msg: output message, must contain one %f placeholder for the percentage + """ + self.counter = 0 # 0 .. (total-1) + self.total = total + self.trigger_at = start # output next percentage value when reaching (at least) this + self.step = step + self.msg = msg + + super().__init__() + def progress(self, current=None, increase=1): if current is not None: self.counter = current @@ -1279,10 +1301,7 @@ class ProgressIndicatorPercent: def output(self, message, justify=True): if justify: - terminal_space = get_terminal_size(fallback=(-1, -1))[0] - # no need to ljust if we're not outputing to a terminal - if terminal_space != -1: - message = message.ljust(terminal_space) + message = justify_to_terminal_size(message) self.logger.info(message) def finish(self): From f9b3d28c19e72e339e5c73dfb3a43eccb3eb356b Mon Sep 17 00:00:00 2001 From: Abogical Date: Thu, 24 Nov 2016 01:46:50 +0200 Subject: [PATCH 0414/1387] Add progress messages for cache.commit --- src/borg/cache.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/borg/cache.py b/src/borg/cache.py index e2ce3651..a0cce1ec 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -18,6 +18,7 @@ from .helpers import get_cache_dir from .helpers import decode_dict, int_to_bigint, bigint_to_int, bin_to_hex from .helpers import format_file_size from .helpers import yes +from .helpers import ProgressIndicatorMessage from .item import Item, ArchiveItem from .key import PlaintextKey from .locking import Lock @@ -246,7 +247,9 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" """ if not self.txn_active: return + pi = ProgressIndicatorMessage() if self.files is not None: + pi.output('Saving files cache') ttl = int(os.environ.get('BORG_FILES_CACHE_TTL', 20)) with SaveFile(os.path.join(self.path, 'files'), binary=True) as fd: for path_hash, item in self.files.items(): @@ -257,17 +260,20 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if entry.age == 0 and bigint_to_int(entry.mtime) < self._newest_mtime or \ entry.age > 0 and entry.age < ttl: msgpack.pack((path_hash, entry), fd) + pi.output('Saving cache config') self.config.set('cache', 'manifest', self.manifest.id_str) self.config.set('cache', 'timestamp', self.manifest.timestamp) self.config.set('cache', 'key_type', str(self.key.TYPE)) self.config.set('cache', 'previous_location', self.repository._location.canonical_path()) with SaveFile(os.path.join(self.path, 'config')) as fd: self.config.write(fd) + pi.output('Saving chunks cache') self.chunks.write(os.path.join(self.path, 'chunks').encode('utf-8')) os.rename(os.path.join(self.path, 'txn.active'), os.path.join(self.path, 'txn.tmp')) shutil.rmtree(os.path.join(self.path, 'txn.tmp')) self.txn_active = False + pi.finish() def rollback(self): """Roll back partial and aborted transactions From fbada18b0dfe918dc528acf273d627e268b62c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Tue, 22 Nov 2016 20:01:26 -0500 Subject: [PATCH 0415/1387] display README correctly on PyPI I have discovered that PyPI is way more sensitive to RST warnings than other platforms: warnings and errors will make the document not show up correctly, which is currently the case here: https://pypi.python.org/pypi/borgbackup/ the suggested changes remove Sphinx-specific markup from the output, namely the badges and `|replacement|` patterns, but also the `higlight` directive. this also requires adding tags to the README to mark the badges to remove and removal of the `none` argument for the `.. code-block::` element which was not having any significant in Sphinx anyways. --- README.rst | 6 +++++- setup.py | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2c407c1d..3c8a0b80 100644 --- a/README.rst +++ b/README.rst @@ -91,7 +91,7 @@ Initialize a new backup repository and create a backup archive:: Now doing another backup, just to show off the great deduplication: -.. code-block:: none +.. code-block:: $ borg create -v --stats /path/to/repo::Saturday2 ~/Documents ----------------------------------------------------------------------------- @@ -141,6 +141,8 @@ THIS IS SOFTWARE IN DEVELOPMENT, DECIDE YOURSELF WHETHER IT FITS YOUR NEEDS. Security issues should be reported to the `Security contact`_ (or see ``docs/suppport.rst`` in the source distribution). +.. start-badges + |doc| |build| |coverage| |bestpractices| .. |doc| image:: https://readthedocs.org/projects/borgbackup/badge/?version=stable @@ -162,3 +164,5 @@ see ``docs/suppport.rst`` in the source distribution). .. |bestpractices| image:: https://bestpractices.coreinfrastructure.org/projects/271/badge :alt: Best Practices Score :target: https://bestpractices.coreinfrastructure.org/projects/271 + +.. end-badges diff --git a/setup.py b/setup.py index ffb9aae9..d5194156 100644 --- a/setup.py +++ b/setup.py @@ -138,6 +138,12 @@ elif not on_rtd: with open('README.rst', 'r') as fd: long_description = fd.read() + # remove badges + long_description = re.compile(r'^\.\. start-badges.*^\.\. end-badges', re.M | re.S).sub('', long_description) + # remove |substitutions| + long_description = re.compile(r'\|screencast\|').sub('', long_description) + # remove unknown directives + long_description = re.compile(r'^\.\. highlight:: \w+$', re.M).sub('', long_description) class build_usage(Command): From 0a0b913739708981622c1bddb908aceba2db9eec Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 24 Nov 2016 23:43:27 +0100 Subject: [PATCH 0416/1387] implement BORG_NEW_PASSPHRASE, fixes #1768 --- docs/usage.rst | 16 ++++++++++++++++ src/borg/key.py | 15 +++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index eab98cbe..912adf1f 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -144,6 +144,13 @@ General: can either leave it away or abbreviate as `::`, if a positional parameter is required. BORG_PASSPHRASE When set, use the value to answer the passphrase question for encrypted repositories. + It is used when a passphrase is needed to access a encrypted repo as well as when a new + passphrase should be initially set when initializing an encrypted repo. + See also BORG_NEW_PASSPHRASE. + BORG_NEW_PASSPHRASE + When set, use the value to answer the passphrase question when a **new** passphrase is asked for. + This variable is checked first. If it is not set, BORG_PASSPHRASE will be checked also. + Main usecase for this is to fully automate ``borg change-passphrase``. BORG_DISPLAY_PASSPHRASE When set, use the value to answer the "display the passphrase for verification" question when defining a new passphrase for encrypted repositories. BORG_LOGGING_CONF @@ -649,6 +656,15 @@ Examples Remember your passphrase. Your data will be inaccessible without it. Key updated +Fully automated using environment variables: + +:: + + $ BORG_NEW_PASSPHRASE=old borg init repo + # now "old" is the current passphrase. + $ BORG_PASSPHRASE=old BORG_NEW_PASSPHRASE=new borg change-passphrase repo + # now "new" is the current passphrase. + .. include:: usage/serve.rst.inc diff --git a/src/borg/key.py b/src/borg/key.py index f5636ba3..eb9c7d94 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -247,11 +247,19 @@ class AESKeyBase(KeyBase): class Passphrase(str): @classmethod - def env_passphrase(cls, default=None): - passphrase = os.environ.get('BORG_PASSPHRASE', default) + def _env_passphrase(cls, env_var, default=None): + passphrase = os.environ.get(env_var, default) if passphrase is not None: return cls(passphrase) + @classmethod + def env_passphrase(cls, default=None): + return cls._env_passphrase('BORG_PASSPHRASE', default) + + @classmethod + def env_new_passphrase(cls, default=None): + return cls._env_passphrase('BORG_NEW_PASSPHRASE', default) + @classmethod def getpass(cls, prompt): return cls(getpass.getpass(prompt)) @@ -276,6 +284,9 @@ class Passphrase(str): @classmethod def new(cls, allow_empty=False): + passphrase = cls.env_new_passphrase() + if passphrase is not None: + return passphrase passphrase = cls.env_passphrase() if passphrase is not None: return passphrase From fa8ce2d9776df94c1c041cbf277a35eafed346c4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 25 Nov 2016 00:06:35 +0100 Subject: [PATCH 0417/1387] add test for borg change-passphrase / BORG_NEW_PASSPHRASE --- src/borg/testsuite/archiver.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 847b4ca9..03d54b73 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1363,6 +1363,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): size, csize = self._get_sizes('lzma', compressible=False) assert csize >= size + def test_change_passphrase(self): + self.cmd('init', self.repository_location) + os.environ['BORG_NEW_PASSPHRASE'] = 'newpassphrase' + # here we have both BORG_PASSPHRASE and BORG_NEW_PASSPHRASE set: + self.cmd('change-passphrase', self.repository_location) + os.environ['BORG_PASSPHRASE'] = 'newpassphrase' + self.cmd('list', self.repository_location) + def test_break_lock(self): self.cmd('init', self.repository_location) self.cmd('break-lock', self.repository_location) From d519ae6b6fb52c26b1389ef52c1f619e99a8c77c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 25 Nov 2016 01:31:28 +0100 Subject: [PATCH 0418/1387] remove some unneeded test overriding methods they are not there any more in the base class, so there is nothing to override. --- src/borg/testsuite/archiver.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 847b4ca9..39bc1e90 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1929,22 +1929,6 @@ class ArchiverTestCaseBinary(ArchiverTestCase): def test_init_interrupt(self): pass - @unittest.skip('patches objects') - def test_recreate_rechunkify_interrupt(self): - pass - - @unittest.skip('patches objects') - def test_recreate_interrupt(self): - pass - - @unittest.skip('patches objects') - def test_recreate_interrupt2(self): - pass - - @unittest.skip('patches objects') - def test_recreate_changed_source(self): - pass - @unittest.skip('test_basic_functionality seems incompatible with fakeroot and/or the binary.') def test_basic_functionality(self): pass From 76c5f1a258717e14ccc6cd5bf9d73e989154cdc9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 26 Nov 2016 04:34:01 +0100 Subject: [PATCH 0419/1387] add more details about resource usage --- docs/internals.rst | 21 ++++++++++++- docs/usage.rst | 78 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 81 insertions(+), 18 deletions(-) diff --git a/docs/internals.rst b/docs/internals.rst index 4df70e94..138761b2 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -364,7 +364,7 @@ varies between 33% and 300%. Indexes / Caches memory usage ----------------------------- -Here is the estimated memory usage of |project_name|: +Here is the estimated memory usage of |project_name| - it's complicated: chunk_count ~= total_file_size / 2 ^ HASH_MASK_BITS @@ -377,6 +377,14 @@ Here is the estimated memory usage of |project_name|: mem_usage ~= repo_index_usage + chunks_cache_usage + files_cache_usage = chunk_count * 164 + total_file_count * 240 +Due to the hashtables, the best/usual/worst cases for memory allocation can +be estimated like that: + + mem_allocation = mem_usage / load_factor # l_f = 0.25 .. 0.75 + + mem_allocation_peak = mem_allocation * (1 + growth_factor) # g_f = 1.1 .. 2 + + All units are Bytes. It is assuming every chunk is referenced exactly once (if you have a lot of @@ -388,6 +396,17 @@ more chunks than estimated above, because 1 file is at least 1 chunk). If a remote repository is used the repo index will be allocated on the remote side. +The chunks cache, files cache and the repo index are all implemented as hash +tables. A hash table must have a significant amount of unused entries to be +fast - the so-called load factor gives the used/unused elements ratio. + +When a hash table gets full (load factor getting too high), it needs to be +grown (allocate new, bigger hash table, copy all elements over to it, free old +hash table) - this will lead to short-time peaks in memory usage each time this +happens. Usually does not happen for all hashtables at the same time, though. +For small hash tables, we start with a growth factor of 2, which comes down to +~1.1x for big hash tables. + E.g. backing up a total count of 1 Mi (IEC binary prefix i.e. 2^20) files with a total size of 1TiB. a) with ``create --chunker-params 10,23,16,4095`` (custom, like borg < 1.0 or attic): diff --git a/docs/usage.rst b/docs/usage.rst index 61b469b1..b00ad8c3 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -206,36 +206,79 @@ Resource Usage |project_name| might use a lot of resources depending on the size of the data set it is dealing with. -CPU: +If one uses |project_name| in a client/server way (with a ssh: repository), +the resource usage occurs in part on the client and in another part on the +server. + +If one uses |project_name| as a single process (with a filesystem repo), +all the resource usage occurs in that one process, so just add up client + +server to get the approximate resource usage. + +CPU client: + borg create: does chunking, hashing, compression, crypto (high CPU usage) + chunks cache sync: quite heavy on CPU, doing lots of hashtable operations. + borg extract: crypto, decompression (medium to high CPU usage) + borg check: similar to extract, but depends on options given. + borg prune / borg delete archive: low to medium CPU usage + borg delete repo: done on the server It won't go beyond 100% of 1 core as the code is currently single-threaded. Especially higher zlib and lzma compression levels use significant amounts - of CPU cycles. + of CPU cycles. Crypto might be cheap on the CPU (if hardware accelerated) or + expensive (if not). -Memory (RAM): +CPU server: + It usually doesn't need much CPU, it just deals with the key/value store + (repository) and uses the repository index for that. + + borg check: the repository check computes the checksums of all chunks + (medium CPU usage) + borg delete repo: low CPU usage + +CPU (only for client/server operation): + When using borg in a client/server way with a ssh:-type repo, the ssh + processes used for the transport layer will need some CPU on the client and + on the server due to the crypto they are doing - esp. if you are pumping + big amounts of data. + +Memory (RAM) client: The chunks index and the files index are read into memory for performance - reasons. + reasons. Might need big amounts of memory (see below). Compression, esp. lzma compression with high levels might need substantial amounts of memory. -Temporary files: - Reading data and metadata from a FUSE mounted repository will consume about - the same space as the deduplicated chunks used to represent them in the - repository. +Memory (RAM) server: + The server process will load the repository index into memory. Might need + considerable amounts of memory, but less than on the client (see below). -Cache files: - Contains the chunks index and files index (plus a compressed collection of - single-archive chunk indexes). - -Chunks index: +Chunks index (client only): Proportional to the amount of data chunks in your repo. Lots of chunks in your repo imply a big chunks index. It is possible to tweak the chunker params (see create options). -Files index: - Proportional to the amount of files in your last backup. Can be switched +Files index (client only): + Proportional to the amount of files in your last backups. Can be switched off (see create options), but next backup will be much slower if you do. -Network: +Repository index (server only): + Proportional to the amount of data chunks in your repo. Lots of chunks + in your repo imply a big repository index. + It is possible to tweak the chunker params (see create options) to + influence the amount of chunks being created. + +Temporary files (client): + Reading data and metadata from a FUSE mounted repository will consume about + the same space as the deduplicated chunks used to represent them in the + repository. + +Temporary files (server): + Not much. + +Cache files (client only): + Contains the chunks index and files index (plus a collection of single- + archive chunk indexes which might need huge amounts of disk space, + depending on archive count and size - see FAQ about how to reduce). + +Network (only for client/server operation): If your repository is remote, all deduplicated (and optionally compressed/ encrypted) data of course has to go over the connection (ssh: repo url). If you use a locally mounted network filesystem, additionally some copy @@ -243,7 +286,8 @@ Network: you backup multiple sources to one target repository, additional traffic happens for cache resynchronization. -In case you are interested in more details, please read the internals documentation. +In case you are interested in more details (like formulas), please read the +internals documentation. Units From be6341b95617a16b491e9125dd53331b11bda983 Mon Sep 17 00:00:00 2001 From: Abogical Date: Sat, 26 Nov 2016 02:28:43 +0200 Subject: [PATCH 0420/1387] Add begin_txn progress message --- src/borg/cache.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/borg/cache.py b/src/borg/cache.py index a0cce1ec..45205689 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -233,14 +233,19 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def begin_txn(self): # Initialize transaction snapshot + pi = ProgressIndicatorMessage() txn_dir = os.path.join(self.path, 'txn.tmp') os.mkdir(txn_dir) + pi.output('Initializing cache transaction: Reading config') shutil.copy(os.path.join(self.path, 'config'), txn_dir) + pi.output('Initializing cache transaction: Reading chunks') shutil.copy(os.path.join(self.path, 'chunks'), txn_dir) + pi.output('Initializing cache transaction: Reading files') shutil.copy(os.path.join(self.path, 'files'), txn_dir) os.rename(os.path.join(self.path, 'txn.tmp'), os.path.join(self.path, 'txn.active')) self.txn_active = True + pi.finish() def commit(self): """Commit transaction From 30d1e21e5370444b910bb585af4fb31f248ac78b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 26 Nov 2016 20:49:39 +0100 Subject: [PATCH 0421/1387] fixup: fixes, clarify --- docs/usage.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index b00ad8c3..21d9c9cc 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -257,7 +257,8 @@ Chunks index (client only): Files index (client only): Proportional to the amount of files in your last backups. Can be switched - off (see create options), but next backup will be much slower if you do. + off (see create options), but next backup might be much slower if you do. + The speed benefit of using the files cache is proportional to file size. Repository index (server only): Proportional to the amount of data chunks in your repo. Lots of chunks @@ -266,12 +267,12 @@ Repository index (server only): influence the amount of chunks being created. Temporary files (client): - Reading data and metadata from a FUSE mounted repository will consume about - the same space as the deduplicated chunks used to represent them in the - repository. + Reading data and metadata from a FUSE mounted repository will consume up to + the size of all deduplicated, small chunks in the repository. Big chunks + won't be locally cached. Temporary files (server): - Not much. + None. Cache files (client only): Contains the chunks index and files index (plus a collection of single- @@ -286,8 +287,8 @@ Network (only for client/server operation): you backup multiple sources to one target repository, additional traffic happens for cache resynchronization. -In case you are interested in more details (like formulas), please read the -internals documentation. +In case you are interested in more details (like formulas), please see +:ref:`internals`. Units From 9451ab6534b6d5b2a8585742b4b1e9820f080d08 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 26 Nov 2016 21:12:16 +0100 Subject: [PATCH 0422/1387] implement noatime / noctime, fixes #1853 --- borg/archive.py | 13 ++++++++++--- borg/archiver.py | 9 ++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index da8a0e70..f69834ce 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -185,7 +185,7 @@ class Archive: """Failed to encode filename "{}" into file system encoding "{}". Consider configuring the LANG environment variable.""" def __init__(self, repository, key, manifest, name, cache=None, create=False, - checkpoint_interval=300, numeric_owner=False, progress=False, + checkpoint_interval=300, numeric_owner=False, noatime=False, noctime=False, progress=False, chunker_params=CHUNKER_PARAMS, start=None, end=None): self.cwd = os.getcwd() self.key = key @@ -198,6 +198,8 @@ class Archive: self.name = name self.checkpoint_interval = checkpoint_interval self.numeric_owner = numeric_owner + self.noatime = noatime + self.noctime = noctime if start is None: start = datetime.utcnow() self.start = start @@ -571,10 +573,15 @@ Number of files: {0.stats.nfiles}'''.format( b'mode': st.st_mode, b'uid': st.st_uid, b'user': uid2user(st.st_uid), b'gid': st.st_gid, b'group': gid2group(st.st_gid), - b'atime': int_to_bigint(st.st_atime_ns), - b'ctime': int_to_bigint(st.st_ctime_ns), b'mtime': int_to_bigint(st.st_mtime_ns), } + # borg can work with archives only having mtime (older attic archives do not have + # atime/ctime). it can be useful to omit atime/ctime, if they change without the + # file content changing - e.g. to get better metadata deduplication. + if not self.noatime: + item[b'atime'] = int_to_bigint(st.st_atime_ns) + if not self.noctime: + item[b'ctime'] = int_to_bigint(st.st_ctime_ns) if self.numeric_owner: item[b'user'] = item[b'group'] = None with backup_io(): diff --git a/borg/archiver.py b/borg/archiver.py index ecc44be7..bd338225 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -281,7 +281,8 @@ class Archiver: with Cache(repository, key, manifest, do_files=args.cache_files, lock_wait=self.lock_wait) as cache: archive = Archive(repository, key, manifest, args.location.archive, cache=cache, create=True, checkpoint_interval=args.checkpoint_interval, - numeric_owner=args.numeric_owner, progress=args.progress, + numeric_owner=args.numeric_owner, noatime=args.noatime, noctime=args.noctime, + progress=args.progress, chunker_params=args.chunker_params, start=t0) create_inner(archive, cache) else: @@ -1301,6 +1302,12 @@ class Archiver: subparser.add_argument('--numeric-owner', dest='numeric_owner', action='store_true', default=False, help='only store numeric user and group identifiers') + subparser.add_argument('--noatime', dest='noatime', + action='store_true', default=False, + help='do not store atime into archive') + subparser.add_argument('--noctime', dest='noctime', + action='store_true', default=False, + help='do not store ctime into archive') subparser.add_argument('--timestamp', dest='timestamp', type=timestamp, default=None, metavar='yyyy-mm-ddThh:mm:ss', From 9643e642119a282fb6ec648faa5d96e7533481c5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 27 Nov 2016 02:09:19 +0100 Subject: [PATCH 0423/1387] upgrade FUSE for macOS to 3.5.3 --- Vagrantfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index f5524840..df636ce3 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -65,9 +65,9 @@ def packages_darwin # install all the (security and other) updates sudo softwareupdate --install --all # get osxfuse 3.x release code from github: - curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.5.2/osxfuse-3.5.2.dmg >osxfuse.dmg + curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.5.3/osxfuse-3.5.3.dmg >osxfuse.dmg MOUNTDIR=$(echo `hdiutil mount osxfuse.dmg | tail -1 | awk '{$1="" ; print $0}'` | xargs -0 echo) \ - && sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for macOS 3.5.2.pkg" -target / + && sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for macOS 3.5.3.pkg" -target / sudo chown -R vagrant /usr/local # brew must be able to create stuff here ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew update From cc158067667481701a0bacbca1af3fba37d8cd4a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 20 Nov 2016 01:12:49 +0100 Subject: [PATCH 0424/1387] update CHANGES (1.0-maint) --- docs/changes.rst | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 70c1d372..5504e98a 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,6 +50,65 @@ The best check that everything is ok is to run a dry-run extraction:: borg extract -v --dry-run REPO::ARCHIVE +Version 1.0.9rc1 (2016-11-27) +----------------------------- + +Bug fixes: + +- files cache: fix determination of newest mtime in backup set (which is + used in cache cleanup and led to wrong "A" [added] status for unchanged + files in next backup), #1860. + +- borg check: + + - fix incorrectly reporting attic 0.13 and earlier archives as corrupt + - handle repo w/o objects gracefully and also bail out early if repo is + *completely* empty, #1815. +- fix tox/pybuild in 1.0-maint +- at xattr module import time, loggers are not initialized yet + +New features: + +- borg umount + exposed already existing umount code via the CLI api, so users can use it, + which is more consistent than using borg to mount and fusermount -u (or + umount) to un-mount, #1855. +- implement borg create --noatime --noctime, fixes #1853 + +Other changes: + +- docs: + + - display README correctly on PyPI + - improve cache / index docs, esp. files cache docs, fixes #1825 + - different pattern matching for --exclude, #1779 + - datetime formatting examples for {now} placeholder, #1822 + - clarify passphrase mode attic repo upgrade, #1854 + - clarify --umask usage, #1859 + - clarify how to choose PR target branch + - clarify prune behavior for different archive contents, #1824 + - fix PDF issues, add logo, fix authors, headings, TOC + - move security verification to support section + - fix links in standalone README (:ref: tags) + - add link to security contact in README + - add FAQ about security + - move fork differences to FAQ + - add more details about resource usage +- tests: skip remote tests on cygwin, #1268 +- travis: + + - allow OS X failures until the brew cask osxfuse issue is fixed + - caskroom osxfuse-beta gone, it's osxfuse now (3.5.3) +- vagrant: + + - upgrade OSXfuse / FUSE for macOS to 3.5.3 + - remove llfuse from tox.ini at a central place + - do not try to install llfuse on centos6 + - fix fuse test for darwin, #1546 + - add windows virtual machine with cygwin + - Vagrantfile cleanup / code deduplication + + Version 1.0.8 (2016-10-29) -------------------------- From bd0e14040ea37d74b433c44d23082f1aad6d22e1 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 24 Nov 2016 00:21:58 +0100 Subject: [PATCH 0425/1387] move "important notes" to own section --- docs/changes.rst | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 5504e98a..0ffc24e8 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,8 +1,10 @@ -Changelog -========= +Important notes +=============== -Important note about pre-1.0.4 potential repo corruption --------------------------------------------------------- +This section is used for infos about e.g. security and corruption issues. + +Pre-1.0.4 potential repo corruption +----------------------------------- Some external errors (like network or disk I/O errors) could lead to corruption of the backup repository due to issue #1138. @@ -50,6 +52,9 @@ The best check that everything is ok is to run a dry-run extraction:: borg extract -v --dry-run REPO::ARCHIVE +Changelog +========= + Version 1.0.9rc1 (2016-11-27) ----------------------------- From 91fa568d5e39d44b66cd0c3d365447555217c0ea Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 24 Nov 2016 00:33:03 +0100 Subject: [PATCH 0426/1387] add note about issue #1837 --- docs/changes.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 0ffc24e8..dad0149f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -3,6 +3,21 @@ Important notes This section is used for infos about e.g. security and corruption issues. +Pre-1.0.9 potential data loss +----------------------------- + +If you have archives in your repository that were made with attic <= 0.13 +(and later migrated to borg), running borg check would report errors in these +archives. See issue #1837. + +The reason for this is a invalid (and useless) metadata key that was +always added due to a bug in these old attic versions. + +If you run borg check --repair, things escalate quickly: all archive items +with invalid metadata will be killed. Due to that attic bug, that means all +items in all archives made with these old attic versions. + + Pre-1.0.4 potential repo corruption ----------------------------------- From 5de31f57e50258d18ed4de80001cb03ea75ada8e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 27 Nov 2016 02:33:57 +0100 Subject: [PATCH 0427/1387] ran build_usage --- docs/usage/create.rst.inc | 8 +++++--- docs/usage/prune.rst.inc | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index c2d42f44..56133ef7 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -12,7 +12,7 @@ borg create [--filter STATUSCHARS] [-e PATTERN] [--exclude-from EXCLUDEFILE] [--exclude-caches] [--exclude-if-present FILENAME] [--keep-tag-files] - [-c SECONDS] [-x] [--numeric-owner] + [-c SECONDS] [-x] [--numeric-owner] [--noatime] [--noctime] [--timestamp yyyy-mm-ddThh:mm:ss] [--chunker-params CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE] [--ignore-inode] [-C COMPRESSION] [--read-special] [-n] @@ -60,6 +60,8 @@ borg create -x, --one-file-system stay in same file system, do not cross mount points --numeric-owner only store numeric user and group identifiers + --noatime do not store atime into archive + --noctime do not store ctime into archive --timestamp yyyy-mm-ddThh:mm:ss manually specify the archive creation date/time (UTC). alternatively, give a reference file/directory. @@ -89,8 +91,8 @@ The archive name needs to be unique. It must not end in '.checkpoint' or '.checkpoint.N' (with N being a number), because these names are used for checkpoints and treated in special ways. -In the archive name, you may use the following format tags: -{now}, {utcnow}, {fqdn}, {hostname}, {user}, {pid}, {borgversion} +In the archive name, you may use the following placeholders: +{now}, {utcnow}, {fqdn}, {hostname}, {user} and some others. To speed up pulling backups over sshfs and similar network file systems which do not provide correct inode information the --ignore-inode flag can be used. This diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index 274d60c2..f8247c95 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -76,3 +76,5 @@ If a prefix is set with -P, then only archives that start with the prefix are considered for deletion and only those archives count towards the totals specified by the rules. Otherwise, *all* archives in the repository are candidates for deletion! +There is no automatic distinction between archives representing different +contents. These need to be distinguished by specifying matching prefixes. From a2505517ee1b20a5d6a6990663759c844bd0e4bd Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 27 Nov 2016 20:15:05 +0100 Subject: [PATCH 0428/1387] FAQ: fix link to changelog --- docs/changes.rst | 1 + docs/faq.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index dad0149f..4b13273f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -66,6 +66,7 @@ The best check that everything is ok is to run a dry-run extraction:: borg extract -v --dry-run REPO::ARCHIVE +.. _changelog: Changelog ========= diff --git a/docs/faq.rst b/docs/faq.rst index a7fc74b1..ed4b6f76 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -544,7 +544,7 @@ Here's a (incomplete) list of some major changes: * better logging, screen output, progress indication * tested on misc. Linux systems, 32 and 64bit, FreeBSD, OpenBSD, NetBSD, Mac OS X -Please read the `ChangeLog`_ (or ``docs/changes.rst`` in the source distribution) for more +Please read the :ref:`changelog` (or ``docs/changes.rst`` in the source distribution) for more information. Borg is not compatible with original attic (but there is a one-way conversion). From 64428da030d7fb87f388208fc961531d40738287 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 27 Nov 2016 20:16:41 +0100 Subject: [PATCH 0429/1387] README: fix code-block without an argument --- README.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 3c8a0b80..07e8b0d3 100644 --- a/README.rst +++ b/README.rst @@ -89,9 +89,7 @@ Initialize a new backup repository and create a backup archive:: $ borg init /path/to/repo $ borg create /path/to/repo::Saturday1 ~/Documents -Now doing another backup, just to show off the great deduplication: - -.. code-block:: +Now doing another backup, just to show off the great deduplication:: $ borg create -v --stats /path/to/repo::Saturday2 ~/Documents ----------------------------------------------------------------------------- From bf3a1f0c3300cb5b8cd649134e4c129aabdda472 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 27 Nov 2016 20:18:27 +0100 Subject: [PATCH 0430/1387] docs/usage: fix literal/emph without end-string (two instances) --- docs/usage.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 21d9c9cc..cb034394 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -42,7 +42,7 @@ Note: you may also prepend a ``file://`` to a filesystem path to get URL style. ``user@host:~other/path/to/repo`` - path relative to other's home directory Note: giving ``user@host:/./path/to/repo`` or ``user@host:/~/path/to/repo`` or -``user@host:/~other/path/to/repo``is also supported, but not required here. +``user@host:/~other/path/to/repo`` is also supported, but not required here. **Remote repositories with relative pathes, alternative syntax with port**: @@ -437,7 +437,7 @@ Notes - the --exclude patterns are not like tar. In tar --exclude .bundler/gems will exclude foo/.bundler/gems. In borg it will not, you need to use --exclude - '*/.bundler/gems' to get the same effect. See ``borg help patterns`` for + '\*/.bundler/gems' to get the same effect. See ``borg help patterns`` for more information. From a49fc6faf52c83f16ebc2ac91a70e86faf953b8d Mon Sep 17 00:00:00 2001 From: Ben Creasy Date: Sun, 27 Nov 2016 03:29:11 -0800 Subject: [PATCH 0431/1387] Clarify extract is relative to current directory I'm still hoping that a destination switch can be added (requested long ago in https://github.com/jborg/attic/issues/195), but in the meantime this may help. I'm guessing this clobbers any existing files. --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 26387aec..78966eb5 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -88,7 +88,7 @@ A step by step example -rw-r--r-- user group 7961 Mon, 2016-02-15 18:22:30 home/user/Documents/Important.doc ... -6. Restore the *Monday* archive:: +6. Restore the *Monday* archive by extracting the files relative to the current directory:: $ borg extract /path/to/repo::Monday From 4746d20534e8068b69e311f7ac3dedd0fd83117b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 28 Nov 2016 02:25:56 +0100 Subject: [PATCH 0432/1387] ran build_usage had merge conflicts in the usage files, decided to just recreate them afterwards. --- docs/usage/check.rst.inc | 14 ++++++++++---- docs/usage/create.rst.inc | 8 ++++++-- docs/usage/diff.rst.inc | 17 +++++++++-------- docs/usage/help.rst.inc | 14 +++++++------- docs/usage/init.rst.inc | 17 +++++++++++++++-- docs/usage/mount.rst.inc | 10 ++++++++++ docs/usage/prune.rst.inc | 2 ++ docs/usage/recreate.rst.inc | 16 +++++----------- docs/usage/umount.rst.inc | 34 +++++++++------------------------- 9 files changed, 73 insertions(+), 59 deletions(-) diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index a142df98..7705471b 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -23,16 +23,22 @@ optional arguments | attempt to repair any inconsistencies found ``--save-space`` | work slower, but using less space - ``--last N`` - | only check last N archives (Default: all) - ``-P``, ``--prefix`` - | only consider archive names starting with this prefix ``-p``, ``--progress`` | show progress display while checking `Common options`_ | +filters + ``-P``, ``--prefix`` + | only consider archive names starting with this prefix + ``--sort-by`` + | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + ``--first N`` + | consider first N archives after other filters were applied + ``--last N`` + | consider last N archives after other filters were applied + Description ~~~~~~~~~~~ diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index ba2dfe21..1fd794b7 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -46,6 +46,10 @@ Filesystem options | stay in same file system, do not cross mount points ``--numeric-owner`` | only store numeric user and group identifiers + ``--noatime`` + | do not store atime into archive + ``--noctime`` + | do not store ctime into archive ``--ignore-inode`` | ignore inode data in the file metadata cache used to detect unchanged files. ``--read-special`` @@ -76,8 +80,8 @@ The archive name needs to be unique. It must not end in '.checkpoint' or '.checkpoint.N' (with N being a number), because these names are used for checkpoints and treated in special ways. -In the archive name, you may use the following format tags: -{now}, {utcnow}, {fqdn}, {hostname}, {user}, {pid}, {uuid4}, {borgversion} +In the archive name, you may use the following placeholders: +{now}, {utcnow}, {fqdn}, {hostname}, {user} and some others. To speed up pulling backups over sshfs and similar network file systems which do not provide correct inode information the --ignore-inode flag can be used. This diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index 9836af57..1c245cf2 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -6,15 +6,15 @@ borg diff --------- :: - borg diff ARCHIVE1 ARCHIVE2 PATH + borg diff REPO_ARCHIVE1 ARCHIVE2 PATH positional arguments - ARCHIVE1 - archive + REPO_ARCHIVE1 + repository location and ARCHIVE1 name ARCHIVE2 - archive to compare with ARCHIVE1 (no repository location) + ARCHIVE2 name (no repository location allowed) PATH - paths to compare; patterns are supported + paths of items inside the archives to compare; patterns are supported optional arguments ``-e PATTERN``, ``--exclude PATTERN`` @@ -34,10 +34,11 @@ optional arguments Description ~~~~~~~~~~~ -This command finds differences in files (contents, user, group, mode) between archives. +This command finds differences (file contents, user/group/mode) between archives. -Both archives need to be in the same repository, and a repository location may only -be specified for ARCHIVE1. +A repository location and an archive name must be specified for REPO_ARCHIVE1. +ARCHIVE2 is just another archive name in same repository (no repository location +allowed). For archives created with Borg 1.1 or newer diff automatically detects whether the archives are created with the same chunker params. If so, only chunk IDs diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index 02c2ed9a..a4a11c4f 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -130,19 +130,19 @@ placeholders: {borgversion} - The version of borg, e.g.: 1.0.8rc1 + The version of borg, e.g.: 1.0.8rc1 - {borgmajor} +{borgmajor} - The version of borg, only the major version, e.g.: 1 + The version of borg, only the major version, e.g.: 1 - {borgminor} +{borgminor} - The version of borg, only major and minor version, e.g.: 1.0 + The version of borg, only major and minor version, e.g.: 1.0 - {borgpatch} +{borgpatch} - The version of borg, only major, minor and patch version, e.g.: 1.0.8 + The version of borg, only major, minor and patch version, e.g.: 1.0.8 Examples:: diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index 7b989497..a754c5da 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -66,5 +66,18 @@ a different keyboard layout. You can change your passphrase for existing repos at any time, it won't affect the encryption/decryption key or other secrets. -When encrypting, AES-CTR-256 is used for encryption, and HMAC-SHA256 for -authentication. Hardware acceleration will be used automatically. +Encryption modes +++++++++++++++++ + +repokey and keyfile use AES-CTR-256 for encryption and HMAC-SHA256 for +authentication in an encrypt-then-MAC (EtM) construction. The chunk ID hash +is HMAC-SHA256 as well (with a separate key). + +repokey-blake2 and keyfile-blake2 use the same authenticated encryption, but +use a keyed BLAKE2b-256 hash for the chunk ID hash. + +"authenticated" mode uses no encryption, but authenticates repository contents +through the same keyed BLAKE2b-256 hash as the other blake2 modes. +The key is stored like repokey. + +Hardware acceleration will be used automatically. diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index a9f3668e..ee63cb42 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -23,6 +23,16 @@ optional arguments `Common options`_ | +filters + ``-P``, ``--prefix`` + | only consider archive names starting with this prefix + ``--sort-by`` + | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + ``--first N`` + | consider first N archives after other filters were applied + ``--last N`` + | consider last N archives after other filters were applied + Description ~~~~~~~~~~~ diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index 5c63d44f..e0c6e16a 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -61,6 +61,8 @@ If a prefix is set with -P, then only archives that start with the prefix are considered for deletion and only those archives count towards the totals specified by the rules. Otherwise, *all* archives in the repository are candidates for deletion! +There is no automatic distinction between archives representing different +contents. These need to be distinguished by specifying matching prefixes. If you have multiple sequences of archives with different data sets (e.g. from different machines) in one shared repository, use one prune call per diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index 56a6e07f..8b07f2f3 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -44,24 +44,18 @@ Exclusion options Archive options ``--target TARGET`` | create a new archive with the name ARCHIVE, do not replace existing archive (only applies for a single archive) + ``-c SECONDS``, ``--checkpoint-interval SECONDS`` + | write checkpoint every SECONDS seconds (Default: 1800) ``--comment COMMENT`` | add a comment text to the archive ``--timestamp yyyy-mm-ddThh:mm:ss`` | manually specify the archive creation date/time (UTC). alternatively, give a reference file/directory. ``-C COMPRESSION``, ``--compression COMPRESSION`` - | select compression algorithm (and level): - | none == no compression (default), - | auto,C[,L] == built-in heuristic decides between none or C[,L] - with C[,L] - | being any valid compression algorithm (and optional level), - | lz4 == lz4, - | zlib == zlib (default level 6), - | zlib,0 .. zlib,9 == zlib (with level 0..9), - | lzma == lzma (default level 6), - | lzma,0 .. lzma,9 == lzma (with level 0..9). + | select compression algorithm, see the output of the "borg help compression" command for details. ``--always-recompress`` - | always recompress chunks, don't skip chunks already compressed with the samealgorithm. + | always recompress chunks, don't skip chunks already compressed with the same algorithm. ``--compression-from COMPRESSIONCONFIG`` - | read compression patterns from COMPRESSIONCONFIG, one per line + | read compression patterns from COMPRESSIONCONFIG, see the output of the "borg help compression" command for details. ``--chunker-params CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE`` | specify the chunker parameters (or "default"). diff --git a/docs/usage/umount.rst.inc b/docs/usage/umount.rst.inc index febacda0..28c5f8f0 100644 --- a/docs/usage/umount.rst.inc +++ b/docs/usage/umount.rst.inc @@ -6,31 +6,15 @@ borg umount ----------- :: - usage: borg umount [-h] [--critical] [--error] [--warning] [--info] [--debug] - [--lock-wait N] [--show-rc] [--no-files-cache] [--umask M] - [--remote-path PATH] - MOUNTPOINT - - un-mount the FUSE filesystem - - positional arguments: - MOUNTPOINT mountpoint of the filesystem to umount - - optional arguments: - -h, --help show this help message and exit - --critical work on log level CRITICAL - --error work on log level ERROR - --warning work on log level WARNING (default) - --info, -v, --verbose - work on log level INFO - --debug work on log level DEBUG - --lock-wait N wait for the lock, but max. N seconds (default: 1). - --show-rc show/log the return code (rc) - --no-files-cache do not load/update the file metadata cache used to - detect unchanged files - --umask M set umask to M (local and remote, default: 0077) - --remote-path PATH set remote path to executable (default: "borg") - + borg umount MOUNTPOINT + +positional arguments + MOUNTPOINT + mountpoint of the filesystem to umount + +`Common options`_ + | + Description ~~~~~~~~~~~ From 619cb123e56e1ceb043ff56044ecb4d372972657 Mon Sep 17 00:00:00 2001 From: enkore Date: Mon, 28 Nov 2016 22:51:01 +0100 Subject: [PATCH 0433/1387] 1.0 maint AUTHORS +me --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 9749b1c5..9111a450 100644 --- a/AUTHORS +++ b/AUTHORS @@ -7,6 +7,7 @@ Borg authors ("The Borg Collective") - Yuri D'Elia - Michael Hanselmann - Teemu Toivanen +- Marian Beermann Borg is a fork of Attic. From 9e760a69a29f7ebc055c4adf6f81b0a4de6aba52 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 29 Nov 2016 14:08:58 +0100 Subject: [PATCH 0434/1387] test_get_(cache|keys)_dir: clean env state, fixes #1897 make sure the BORG_(CACHE|KEYS)_DIR env var is not set initially. --- borg/testsuite/helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py index 1087b552..d0fa86b6 100644 --- a/borg/testsuite/helpers.py +++ b/borg/testsuite/helpers.py @@ -627,6 +627,7 @@ class TestParseTimestamp(BaseTestCase): def test_get_cache_dir(monkeypatch): """test that get_cache_dir respects environment""" + monkeypatch.delenv('BORG_CACHE_DIR', raising=False) monkeypatch.delenv('XDG_CACHE_HOME', raising=False) assert get_cache_dir() == os.path.join(os.path.expanduser('~'), '.cache', 'borg') monkeypatch.setenv('XDG_CACHE_HOME', '/var/tmp/.cache') @@ -637,6 +638,7 @@ def test_get_cache_dir(monkeypatch): def test_get_keys_dir(monkeypatch): """test that get_keys_dir respects environment""" + monkeypatch.delenv('BORG_KEYS_DIR', raising=False) monkeypatch.delenv('XDG_CONFIG_HOME', raising=False) assert get_keys_dir() == os.path.join(os.path.expanduser('~'), '.config', 'borg', 'keys') monkeypatch.setenv('XDG_CONFIG_HOME', '/var/tmp/.config') From c3a2dc5f557ed1920b2282e8a68de50c6de88a79 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 27 Nov 2016 12:08:26 +0100 Subject: [PATCH 0435/1387] Rename BORG_NONCES_DIR to BORG_SECURITY_DIR --- docs/usage.rst | 7 ++++--- src/borg/helpers.py | 16 +++++++++------- src/borg/nonces.py | 4 ++-- src/borg/testsuite/helpers.py | 15 ++++++++------- src/borg/testsuite/key.py | 4 ++-- src/borg/testsuite/nonces.py | 6 +++--- 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 51fa600a..8410d115 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -189,9 +189,10 @@ Directories and files: Default to '~/.config/borg/keys'. This directory contains keys for encrypted repositories. BORG_KEY_FILE When set, use the given filename as repository key file. - BORG_NONCES_DIR - Default to '~/.config/borg/key-nonces'. This directory contains information borg uses to - track its usage of NONCES ("numbers used once" - usually in encryption context). + BORG_SECURITY_DIR + Default to '~/.config/borg/security'. This directory contains information borg uses to + track its usage of NONCES ("numbers used once" - usually in encryption context) and other + security relevant data. BORG_CACHE_DIR Default to '~/.cache/borg'. This directory contains the local cache and might need a lot of space for dealing with big repositories). diff --git a/src/borg/helpers.py b/src/borg/helpers.py index b3a48d16..93413845 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -288,15 +288,17 @@ def get_keys_dir(): return keys_dir -def get_nonces_dir(): - """Determine where to store the local nonce high watermark""" +def get_security_dir(repository_id=None): + """Determine where to store local security information.""" xdg_config = os.environ.get('XDG_CONFIG_HOME', os.path.join(get_home_dir(), '.config')) - nonces_dir = os.environ.get('BORG_NONCES_DIR', os.path.join(xdg_config, 'borg', 'key-nonces')) - if not os.path.exists(nonces_dir): - os.makedirs(nonces_dir) - os.chmod(nonces_dir, stat.S_IRWXU) - return nonces_dir + security_dir = os.environ.get('BORG_SECURITY_DIR', os.path.join(xdg_config, 'borg', 'security')) + if repository_id: + security_dir = os.path.join(security_dir, repository_id) + if not os.path.exists(security_dir): + os.makedirs(security_dir) + os.chmod(security_dir, stat.S_IRWXU) + return security_dir def get_cache_dir(): diff --git a/src/borg/nonces.py b/src/borg/nonces.py index 4f929958..e6eb7a2c 100644 --- a/src/borg/nonces.py +++ b/src/borg/nonces.py @@ -3,7 +3,7 @@ import sys from binascii import unhexlify from .crypto import bytes_to_long, long_to_bytes -from .helpers import get_nonces_dir +from .helpers import get_security_dir from .helpers import bin_to_hex from .platform import SaveFile from .remote import InvalidRPCMethod @@ -19,7 +19,7 @@ class NonceManager: self.enc_cipher = enc_cipher self.end_of_nonce_reservation = None self.manifest_nonce = manifest_nonce - self.nonce_file = os.path.join(get_nonces_dir(), self.repository.id_str) + self.nonce_file = os.path.join(get_security_dir(self.repository.id_str), 'nonce') def get_local_free_nonce(self): try: diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index f41cb442..a66d681f 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -15,7 +15,7 @@ from ..helpers import Buffer from ..helpers import partial_format, format_file_size, parse_file_size, format_timedelta, format_line, PlaceholderError, replace_placeholders from ..helpers import make_path_safe, clean_lines from ..helpers import prune_within, prune_split -from ..helpers import get_cache_dir, get_keys_dir, get_nonces_dir +from ..helpers import get_cache_dir, get_keys_dir, get_security_dir from ..helpers import is_slow_msgpack from ..helpers import yes, TRUISH, FALSISH, DEFAULTISH from ..helpers import StableDict, int_to_bigint, bigint_to_int, bin_to_hex @@ -660,14 +660,15 @@ def test_get_keys_dir(monkeypatch): assert get_keys_dir() == '/var/tmp' -def test_get_nonces_dir(monkeypatch): - """test that get_nonces_dir respects environment""" +def test_get_security_dir(monkeypatch): + """test that get_security_dir respects environment""" monkeypatch.delenv('XDG_CONFIG_HOME', raising=False) - assert get_nonces_dir() == os.path.join(os.path.expanduser('~'), '.config', 'borg', 'key-nonces') + assert get_security_dir() == os.path.join(os.path.expanduser('~'), '.config', 'borg', 'security') + assert get_security_dir(repository_id='1234') == os.path.join(os.path.expanduser('~'), '.config', 'borg', 'security', '1234') monkeypatch.setenv('XDG_CONFIG_HOME', '/var/tmp/.config') - assert get_nonces_dir() == os.path.join('/var/tmp/.config', 'borg', 'key-nonces') - monkeypatch.setenv('BORG_NONCES_DIR', '/var/tmp') - assert get_nonces_dir() == '/var/tmp' + assert get_security_dir() == os.path.join('/var/tmp/.config', 'borg', 'security') + monkeypatch.setenv('BORG_SECURITY_DIR', '/var/tmp') + assert get_security_dir() == '/var/tmp' def test_file_size(): diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index 5f456b0b..0702301e 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -10,7 +10,7 @@ from ..crypto import bytes_to_long, num_aes_blocks from ..helpers import Location from ..helpers import Chunk from ..helpers import IntegrityError -from ..helpers import get_nonces_dir +from ..helpers import get_security_dir from ..key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, AuthenticatedKey from ..key import Passphrase, PasswordRetriesExceeded, bin_to_hex @@ -118,7 +118,7 @@ class TestKey: def test_keyfile_nonce_rollback_protection(self, monkeypatch, keys_dir): monkeypatch.setenv('BORG_PASSPHRASE', 'test') repository = self.MockRepository() - with open(os.path.join(get_nonces_dir(), repository.id_str), "w") as fd: + with open(os.path.join(get_security_dir(repository.id_str), 'nonce'), "w") as fd: fd.write("0000000000002000") key = KeyfileKey.create(repository, self.MockArgs()) data = key.encrypt(Chunk(b'ABC')) diff --git a/src/borg/testsuite/nonces.py b/src/borg/testsuite/nonces.py index 14d1f52d..d88d260a 100644 --- a/src/borg/testsuite/nonces.py +++ b/src/borg/testsuite/nonces.py @@ -2,7 +2,7 @@ import os.path import pytest -from ..helpers import get_nonces_dir +from ..helpers import get_security_dir from ..key import bin_to_hex from ..nonces import NonceManager from ..remote import InvalidRPCMethod @@ -61,11 +61,11 @@ class TestNonceManager: self.repository = None def cache_nonce(self): - with open(os.path.join(get_nonces_dir(), self.repository.id_str), "r") as fd: + with open(os.path.join(get_security_dir(self.repository.id_str), 'nonce'), "r") as fd: return fd.read() def set_cache_nonce(self, nonce): - with open(os.path.join(get_nonces_dir(), self.repository.id_str), "w") as fd: + with open(os.path.join(get_security_dir(self.repository.id_str), 'nonce'), "w") as fd: assert fd.write(nonce) def test_empty_cache_and_old_server(self, monkeypatch): From f62a22392e444ef2a31e34c580a530d2423b5f87 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 27 Nov 2016 12:39:49 +0100 Subject: [PATCH 0436/1387] Implement security dir perks Key type, location and manifest timestamp checks now survive cache deletion. This also means that you can now delete your cache and avoid previous warnings, since Borg can still tell it's safe. --- src/borg/cache.py | 141 ++++++++++++++++++++++++++------- src/borg/testsuite/archiver.py | 88 +++++++++++++++++++- 2 files changed, 197 insertions(+), 32 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index aa91e7b3..813c9469 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -14,7 +14,7 @@ from .constants import CACHE_README from .hashindex import ChunkIndex, ChunkIndexEntry from .helpers import Location from .helpers import Error -from .helpers import get_cache_dir +from .helpers import get_cache_dir, get_security_dir from .helpers import decode_dict, int_to_bigint, bigint_to_int, bin_to_hex from .helpers import format_file_size from .helpers import yes @@ -29,6 +29,113 @@ ChunkListEntry = namedtuple('ChunkListEntry', 'id size csize') FileCacheEntry = namedtuple('FileCacheEntry', 'age inode size mtime chunk_ids') +class SecurityManager: + def __init__(self, repository): + self.repository = repository + self.dir = get_security_dir(repository.id_str) + self.key_type_file = os.path.join(self.dir, 'key-type') + self.location_file = os.path.join(self.dir, 'location') + self.manifest_ts_file = os.path.join(self.dir, 'manifest-timestamp') + + def known(self): + return os.path.exists(self.key_type_file) + + def key_matches(self, key): + if not self.known(): + return False + try: + with open(self.key_type_file, 'r') as fd: + type = fd.read() + return type == str(key.TYPE) + except OSError as exc: + logger.warning('Could not read/parse key type file: %s', exc) + + def save(self, manifest, key, cache): + logger.debug('security: saving state for %s to %s', self.repository.id_str, self.dir) + current_location = cache.repository._location.canonical_path() + logger.debug('security: current location %s', current_location) + logger.debug('security: key type %s', str(key.TYPE)) + logger.debug('security: manifest timestamp %s', manifest.timestamp) + with open(self.location_file, 'w') as fd: + fd.write(current_location) + with open(self.key_type_file, 'w') as fd: + fd.write(str(key.TYPE)) + with open(self.manifest_ts_file, 'w') as fd: + fd.write(manifest.timestamp) + + def assert_location_matches(self, cache): + # Warn user before sending data to a relocated repository + try: + with open(self.location_file) as fd: + previous_location = fd.read() + logger.debug('security: read previous_location %r', previous_location) + except FileNotFoundError: + logger.debug('security: previous_location file %s not found', self.location_file) + previous_location = None + except OSError as exc: + logger.warning('Could not read previous location file: %s', exc) + previous_location = None + if cache.previous_location and previous_location != cache.previous_location: + # Reconcile cache and security dir; we take the cache location. + previous_location = cache.previous_location + logger.debug('security: using previous_location of cache: %r', previous_location) + if previous_location and previous_location != self.repository._location.canonical_path(): + msg = ("Warning: The repository at location {} was previously located at {}\n".format( + self.repository._location.canonical_path(), previous_location) + + "Do you want to continue? [yN] ") + if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", + retry=False, env_var_override='BORG_RELOCATED_REPO_ACCESS_IS_OK'): + raise Cache.RepositoryAccessAborted() + # adapt on-disk config immediately if the new location was accepted + logger.debug('security: updating location stored in cache and security dir') + with open(self.location_file, 'w') as fd: + fd.write(cache.repository._location.canonical_path()) + cache.begin_txn() + cache.commit() + + def assert_no_manifest_replay(self, manifest, key, cache): + try: + with open(self.manifest_ts_file) as fd: + timestamp = fd.read() + logger.debug('security: read manifest timestamp %r', timestamp) + except FileNotFoundError: + logger.debug('security: manifest timestamp file %s not found', self.manifest_ts_file) + timestamp = '' + except OSError as exc: + logger.warning('Could not read previous location file: %s', exc) + timestamp = '' + timestamp = max(timestamp, cache.timestamp or '') + logger.debug('security: determined newest manifest timestamp as %s', timestamp) + # If repository is older than the cache or security dir something fishy is going on + if timestamp and timestamp > manifest.timestamp: + if isinstance(key, PlaintextKey): + raise Cache.RepositoryIDNotUnique() + else: + raise Cache.RepositoryReplay() + + def assert_key_type(self, key, cache): + # Make sure an encrypted repository has not been swapped for an unencrypted repository + if cache.key_type is not None and cache.key_type != str(key.TYPE): + raise Cache.EncryptionMethodMismatch() + if self.known() and not self.key_matches(key): + raise Cache.EncryptionMethodMismatch() + + def assert_secure(self, manifest, key, cache): + self.assert_location_matches(cache) + self.assert_key_type(key, cache) + self.assert_no_manifest_replay(manifest, key, cache) + if not self.known(): + self.save(manifest, key, cache) + + def assert_access_unknown(self, warn_if_unencrypted, key): + if warn_if_unencrypted and isinstance(key, PlaintextKey) and not self.known(): + msg = ("Warning: Attempting to access a previously unknown unencrypted repository!\n" + + "Do you want to continue? [yN] ") + if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", + retry=False, env_var_override='BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK'): + raise Cache.CacheInitAbortedError() + + class Cache: """Client Side cache """ @@ -77,44 +184,19 @@ class Cache: self.key = key self.manifest = manifest self.path = path or os.path.join(get_cache_dir(), repository.id_str) + self.security_manager = SecurityManager(repository) self.hostname_is_unique = yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', prompt=False, env_msg=None) if self.hostname_is_unique: logger.info('Enabled removal of stale cache locks') self.do_files = do_files # Warn user before sending data to a never seen before unencrypted repository if not os.path.exists(self.path): - if warn_if_unencrypted and isinstance(key, PlaintextKey): - msg = ("Warning: Attempting to access a previously unknown unencrypted repository!" + - "\n" + - "Do you want to continue? [yN] ") - if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", - retry=False, env_var_override='BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK'): - raise self.CacheInitAbortedError() + self.security_manager.assert_access_unknown(warn_if_unencrypted, key) self.create() self.open(lock_wait=lock_wait) try: - # Warn user before sending data to a relocated repository - if self.previous_location and self.previous_location != repository._location.canonical_path(): - msg = ("Warning: The repository at location {} was previously located at {}".format(repository._location.canonical_path(), self.previous_location) + - "\n" + - "Do you want to continue? [yN] ") - if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", - retry=False, env_var_override='BORG_RELOCATED_REPO_ACCESS_IS_OK'): - raise self.RepositoryAccessAborted() - # adapt on-disk config immediately if the new location was accepted - self.begin_txn() - self.commit() - + self.security_manager.assert_secure(manifest, key, self) if sync and self.manifest.id != self.manifest_id: - # If repository is older than the cache something fishy is going on - if self.timestamp and self.timestamp > manifest.timestamp: - if isinstance(key, PlaintextKey): - raise self.RepositoryIDNotUnique() - else: - raise self.RepositoryReplay() - # Make sure an encrypted repository has not been swapped for an unencrypted repository - if self.key_type is not None and self.key_type != str(key.TYPE): - raise self.EncryptionMethodMismatch() self.sync() self.commit() except: @@ -252,6 +334,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" """ if not self.txn_active: return + self.security_manager.save(self.manifest, self.key, self) pi = ProgressIndicatorMessage() if self.files is not None: if self._newest_mtime is None: diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 6e8aa9c7..75f67ecb 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -29,7 +29,7 @@ from ..archiver import Archiver from ..cache import Cache from ..constants import * # NOQA from ..crypto import bytes_to_long, num_aes_blocks -from ..helpers import PatternMatcher, parse_pattern, Location +from ..helpers import PatternMatcher, parse_pattern, Location, get_security_dir from ..helpers import Chunk, Manifest from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from ..helpers import bin_to_hex @@ -382,8 +382,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): item_count = 4 if has_lchflags else 5 # one file is UF_NODUMP self.assert_in('Number of files: %d' % item_count, info_output) shutil.rmtree(self.cache_path) - with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='yes'): - info_output2 = self.cmd('info', self.repository_location + '::test') + info_output2 = self.cmd('info', self.repository_location + '::test') def filter(output): # filter for interesting "info" output, ignore cache rebuilding related stuff @@ -563,6 +562,89 @@ class ArchiverTestCase(ArchiverTestCaseBase): else: self.assert_raises(Cache.RepositoryAccessAborted, lambda: self.cmd('create', self.repository_location + '_encrypted::test.2', 'input')) + def test_repository_swap_detection_no_cache(self): + self.create_test_files() + os.environ['BORG_PASSPHRASE'] = 'passphrase' + self.cmd('init', '--encryption=repokey', self.repository_location) + repository_id = self._extract_repository_id(self.repository_path) + self.cmd('create', self.repository_location + '::test', 'input') + shutil.rmtree(self.repository_path) + self.cmd('init', '--encryption=none', self.repository_location) + self._set_repository_id(self.repository_path, repository_id) + self.assert_equal(repository_id, self._extract_repository_id(self.repository_path)) + self.cmd('delete', '--cache-only', self.repository_location) + if self.FORK_DEFAULT: + self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR) + else: + self.assert_raises(Cache.EncryptionMethodMismatch, lambda: self.cmd('create', self.repository_location + '::test.2', 'input')) + + def test_repository_swap_detection2_no_cache(self): + self.create_test_files() + self.cmd('init', '--encryption=none', self.repository_location + '_unencrypted') + os.environ['BORG_PASSPHRASE'] = 'passphrase' + self.cmd('init', '--encryption=repokey', self.repository_location + '_encrypted') + self.cmd('create', self.repository_location + '_encrypted::test', 'input') + self.cmd('delete', '--cache-only', self.repository_location + '_unencrypted') + self.cmd('delete', '--cache-only', self.repository_location + '_encrypted') + shutil.rmtree(self.repository_path + '_encrypted') + os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted') + if self.FORK_DEFAULT: + self.cmd('create', self.repository_location + '_encrypted::test.2', 'input', exit_code=EXIT_ERROR) + else: + with pytest.raises(Cache.RepositoryAccessAborted): + self.cmd('create', self.repository_location + '_encrypted::test.2', 'input') + + def test_repository_move(self): + self.cmd('init', self.repository_location) + repository_id = bin_to_hex(self._extract_repository_id(self.repository_path)) + os.rename(self.repository_path, self.repository_path + '_new') + with environment_variable(BORG_RELOCATED_REPO_ACCESS_IS_OK='yes'): + self.cmd('info', self.repository_location + '_new') + security_dir = get_security_dir(repository_id) + with open(os.path.join(security_dir, 'location')) as fd: + location = fd.read() + assert location == Location(self.repository_location + '_new').canonical_path() + # Needs no confirmation anymore + self.cmd('info', self.repository_location + '_new') + shutil.rmtree(self.cache_path) + self.cmd('info', self.repository_location + '_new') + shutil.rmtree(security_dir) + self.cmd('info', self.repository_location + '_new') + for file in ('location', 'key-type', 'manifest-timestamp'): + assert os.path.exists(os.path.join(security_dir, file)) + + def test_security_dir_compat(self): + self.cmd('init', self.repository_location) + repository_id = bin_to_hex(self._extract_repository_id(self.repository_path)) + security_dir = get_security_dir(repository_id) + with open(os.path.join(security_dir, 'location'), 'w') as fd: + fd.write('something outdated') + # This is fine, because the cache still has the correct information. security_dir and cache can disagree + # if older versions are used to confirm a renamed repository. + self.cmd('info', self.repository_location) + + def test_unknown_unencrypted(self): + self.cmd('init', '--encryption=none', self.repository_location) + repository_id = bin_to_hex(self._extract_repository_id(self.repository_path)) + security_dir = get_security_dir(repository_id) + # Ok: repository is known + self.cmd('info', self.repository_location) + + # Ok: repository is still known (through security_dir) + shutil.rmtree(self.cache_path) + self.cmd('info', self.repository_location) + + # Needs confirmation: cache and security dir both gone (eg. another host or rm -rf ~) + shutil.rmtree(self.cache_path) + shutil.rmtree(security_dir) + if self.FORK_DEFAULT: + self.cmd('info', self.repository_location, exit_code=EXIT_ERROR) + else: + with pytest.raises(Cache.CacheInitAbortedError): + self.cmd('info', self.repository_location) + with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='yes'): + self.cmd('info', self.repository_location) + def test_strip_components(self): self.cmd('init', self.repository_location) self.create_regular_file('dir/file') From bd96b43af99e5914db63be24e44715d63a6b5645 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 27 Nov 2016 18:40:34 +0100 Subject: [PATCH 0437/1387] borg info: print security directory --- src/borg/archiver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 6b443257..996eb945 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -970,6 +970,7 @@ class Archiver: if key.NAME.startswith('key file'): print('Key file: %s' % key.find_key()) print('Cache: %s' % cache.path) + print('Security dir: %s' % cache.security_manager.dir) print(DASHES) print(STATS_HEADER) print(str(cache)) From cd50e286f712242fbbcc5c2143ab26c9ecf0d7d4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 29 Nov 2016 01:34:11 +0100 Subject: [PATCH 0438/1387] fix traceback in Error handler if id is None, fixes #1894 --- borg/key.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/borg/key.py b/borg/key.py index 7b65d909..944e17d7 100644 --- a/borg/key.py +++ b/borg/key.py @@ -105,7 +105,8 @@ class PlaintextKey(KeyBase): def decrypt(self, id, data): if data[0] != self.TYPE: - raise IntegrityError('Chunk %s: Invalid encryption envelope' % bin_to_hex(id)) + id_str = bin_to_hex(id) if id is not None else '(unknown)' + raise IntegrityError('Chunk %s: Invalid encryption envelope' % id_str) data = self.compressor.decompress(memoryview(data)[1:]) if id and sha256(data).digest() != id: raise IntegrityError('Chunk %s: id verification failed' % bin_to_hex(id)) From f3ce6be30b9b65e59135983d3e2629ebe7c8a2c3 Mon Sep 17 00:00:00 2001 From: Abogical Date: Sat, 26 Nov 2016 22:15:59 +0200 Subject: [PATCH 0439/1387] Add cache.sync progress display --- src/borg/archiver.py | 8 +++++--- src/borg/cache.py | 13 +++++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 5e764344..d444890a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -95,7 +95,8 @@ def with_repository(fake=False, create=False, lock=True, exclusive=False, manife kwargs['manifest'], kwargs['key'] = Manifest.load(repository) if cache: with Cache(repository, kwargs['key'], kwargs['manifest'], - do_files=getattr(args, 'cache_files', False), lock_wait=self.lock_wait) as cache_: + do_files=getattr(args, 'cache_files', False), + progress=getattr(args, 'progress', False), lock_wait=self.lock_wait) as cache_: return method(self, args, repository=repository, cache=cache_, **kwargs) else: return method(self, args, repository=repository, **kwargs) @@ -341,7 +342,8 @@ class Archiver: dry_run = args.dry_run t0 = datetime.utcnow() if not dry_run: - with Cache(repository, key, manifest, do_files=args.cache_files, lock_wait=self.lock_wait) as cache: + with Cache(repository, key, manifest, do_files=args.cache_files, progress=args.progress, + lock_wait=self.lock_wait) as cache: archive = Archive(repository, key, manifest, args.location.archive, cache=cache, create=True, checkpoint_interval=args.checkpoint_interval, numeric_owner=args.numeric_owner, progress=args.progress, @@ -794,7 +796,7 @@ class Archiver: if args.stats: log_multi(DASHES, STATS_HEADER, logger=stats_logger) - with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: + with Cache(repository, key, manifest, progress=args.progress, lock_wait=self.lock_wait) as cache: for i, archive_name in enumerate(archive_names, 1): logger.info('Deleting {} ({}/{}):'.format(archive_name, i, len(archive_names))) archive = Archive(repository, key, manifest, archive_name, cache=cache) diff --git a/src/borg/cache.py b/src/borg/cache.py index 45205689..30726df5 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -18,7 +18,8 @@ from .helpers import get_cache_dir from .helpers import decode_dict, int_to_bigint, bigint_to_int, bin_to_hex from .helpers import format_file_size from .helpers import yes -from .helpers import ProgressIndicatorMessage +from .helpers import remove_surrogates +from .helpers import ProgressIndicatorPercent, ProgressIndicatorMessage from .item import Item, ArchiveItem from .key import PlaintextKey from .locking import Lock @@ -62,7 +63,7 @@ class Cache: shutil.rmtree(path) def __init__(self, repository, key, manifest, path=None, sync=True, do_files=False, warn_if_unencrypted=True, - lock_wait=None): + progress=False, lock_wait=None): """ :param do_files: use file metadata cache :param warn_if_unencrypted: print warning if accessing unknown unencrypted repository @@ -76,6 +77,7 @@ class Cache: self.repository = repository self.key = key self.manifest = manifest + self.progress = progress self.path = path or os.path.join(get_cache_dir(), repository.id_str) self.hostname_is_unique = yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', prompt=False, env_msg=None) if self.hostname_is_unique: @@ -379,8 +381,13 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" cleanup_outdated(cached_ids - archive_ids) if archive_ids: chunk_idx = None + if self.progress: + pi = ProgressIndicatorPercent(total=len(archive_ids), step=0.1, + msg='%3.0f%% Syncing chunks cache. Processing archive %s') for archive_id in archive_ids: archive_name = lookup_name(archive_id) + if self.progress: + pi.show(info=[remove_surrogates(archive_name)]) if archive_id in cached_ids: archive_chunk_idx_path = mkpath(archive_id) logger.info("Reading cached archive chunk index for %s ..." % archive_name) @@ -396,6 +403,8 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" chunk_idx = archive_chunk_idx else: chunk_idx.merge(archive_chunk_idx) + if self.progress: + pi.finish() logger.info('Done.') return chunk_idx From 6290e70c80ebbf5bb286f5f781c479e852a666cf Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 29 Nov 2016 21:30:23 +0100 Subject: [PATCH 0440/1387] partially remove virtualenv/pip version requirement, fixes #1738 Is needed only for python 3.2 support. For normal development, we expect you have py34+ for borg 1.1. For vagrant, it is still needed because of older VMs like wheezy (py32). Not needed for Travis-CI any more, we moved to trusty VMs (py34) there. --- .travis/install.sh | 4 ++-- requirements.d/development.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis/install.sh b/.travis/install.sh index 6a2e9cb7..309a3e9d 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -31,9 +31,9 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then ;; esac pyenv rehash - python -m pip install --user 'virtualenv<14.0' + python -m pip install --user virtualenv else - pip install 'virtualenv<14.0' + pip install virtualenv sudo apt-get update sudo apt-get install -y liblz4-dev sudo apt-get install -y libacl1-dev diff --git a/requirements.d/development.txt b/requirements.d/development.txt index a0cb3c2a..f14c3abf 100644 --- a/requirements.d/development.txt +++ b/requirements.d/development.txt @@ -1,4 +1,4 @@ -virtualenv<14.0 +virtualenv tox pytest pytest-cov From 01ad1a5153576ba87828bb4011859f18fd650525 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 29 Nov 2016 23:02:24 +0100 Subject: [PATCH 0441/1387] implement "health" item formatter key, fixes #1749 --- src/borg/helpers.py | 3 +++ src/borg/testsuite/archiver.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 93413845..a1624f1a 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1430,6 +1430,7 @@ class ItemFormatter(BaseFormatter): 'csize': 'compressed size', 'num_chunks': 'number of chunks in this file', 'unique_chunks': 'number of unique chunks in this file', + 'health': 'either "healthy" (file ok) or "broken" (if file has all-zero replacement chunks)', } KEY_GROUPS = ( ('type', 'mode', 'uid', 'gid', 'user', 'group', 'path', 'bpath', 'source', 'linktarget', 'flags'), @@ -1437,6 +1438,7 @@ class ItemFormatter(BaseFormatter): ('mtime', 'ctime', 'atime', 'isomtime', 'isoctime', 'isoatime'), tuple(sorted(hashlib.algorithms_guaranteed)), ('archiveid', 'archivename', 'extra'), + ('health', ) ) @classmethod @@ -1526,6 +1528,7 @@ class ItemFormatter(BaseFormatter): item_data['linktarget'] = source item_data['extra'] = extra item_data['flags'] = item.get('bsdflags') + item_data['health'] = 'broken' if 'chunks_healthy' in item else 'healthy' for key in self.used_call_keys: item_data[key] = self.call_keys[key](item) return item_data diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 75f67ecb..4c8c765d 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2082,6 +2082,8 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): output = self.cmd('check', '--repair', self.repository_location, exit_code=0) self.assert_in('New missing file chunk detected', output) self.cmd('check', self.repository_location, exit_code=0) + output = self.cmd('list', '--format={health}#{path}{LF}', self.repository_location + '::archive1', exit_code=0) + self.assert_in('broken#', output) # check that the file in the old archives has now a different chunk list without the killed chunk for archive_name in ('archive1', 'archive2'): archive, repository = self.open_archive(archive_name) @@ -2110,6 +2112,9 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): break else: self.assert_true(False) # should not happen + # list is also all-healthy again + output = self.cmd('list', '--format={health}#{path}{LF}', self.repository_location + '::archive1', exit_code=0) + self.assert_not_in('broken#', output) def test_missing_archive_item_chunk(self): archive, repository = self.open_archive('archive1') From 8949f2c758ab070af7c581d649a82e3221752711 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 30 Nov 2016 00:24:03 +0100 Subject: [PATCH 0442/1387] blake2b key modes: use B2B as MAC; longer keys. --- src/borg/key.py | 34 ++++++++++++++++++++++++++++++---- src/borg/testsuite/key.py | 4 ++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/borg/key.py b/src/borg/key.py index eb9c7d94..d63e3c16 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -161,6 +161,20 @@ class PlaintextKey(KeyBase): return Chunk(data) +def random_blake2b_256_key(): + # This might look a bit curious, but is the same construction used in the keyed mode of BLAKE2b. + # Why limit the key to 64 bytes and pad it with 64 nulls nonetheless? The answer is that BLAKE2b + # has a 128 byte block size, but only 64 bytes of internal state (this is also referred to as a + # "local wide pipe" design, because the compression function transforms (block, state) => state, + # and len(block) >= len(state), hence wide.) + # In other words, a key longer than 64 bytes would have simply no advantage, since the function + # has no way of propagating more than 64 bytes of entropy internally. + # It's padded to a full block so that the key is never buffered internally by blake2b_update, ie. + # it remains in a single memory location that can be tracked and could be erased securely, if we + # wanted to. + return os.urandom(64) + bytes(64) + + class ID_BLAKE2b_256: """ Key mix-in class for using BLAKE2b-256 for the id key. @@ -171,6 +185,12 @@ class ID_BLAKE2b_256: def id_hash(self, data): return blake2b_256(self.id_key, data) + def init_from_random_data(self, data=None): + assert data is None # PassphraseKey is the only caller using *data* + super().init_from_random_data() + self.enc_hmac_key = random_blake2b_256_key() + self.id_key = random_blake2b_256_key() + class ID_HMAC_SHA_256: """ @@ -198,12 +218,14 @@ class AESKeyBase(KeyBase): PAYLOAD_OVERHEAD = 1 + 32 + 8 # TYPE + HMAC + NONCE + MAC = hmac_sha256 + def encrypt(self, chunk): chunk = self.compress(chunk) self.nonce_manager.ensure_reservation(num_aes_blocks(len(chunk.data))) self.enc_cipher.reset() data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(chunk.data))) - hmac = hmac_sha256(self.enc_hmac_key, data) + hmac = self.MAC(self.enc_hmac_key, data) return b''.join((self.TYPE_STR, hmac, data)) def decrypt(self, id, data, decompress=True): @@ -212,7 +234,7 @@ class AESKeyBase(KeyBase): raise IntegrityError('Chunk %s: Invalid encryption envelope' % bin_to_hex(id)) data_view = memoryview(data) hmac_given = data_view[1:33] - hmac_computed = memoryview(hmac_sha256(self.enc_hmac_key, data_view[33:])) + hmac_computed = memoryview(self.MAC(self.enc_hmac_key, data_view[33:])) if not compare_digest(hmac_computed, hmac_given): raise IntegrityError('Chunk %s: Encryption envelope checksum mismatch' % bin_to_hex(id)) self.dec_cipher.reset(iv=PREFIX + data[33:41]) @@ -230,7 +252,9 @@ class AESKeyBase(KeyBase): nonce = bytes_to_long(payload[33:41]) return nonce - def init_from_random_data(self, data): + def init_from_random_data(self, data=None): + if data is None: + data = os.urandom(100) self.enc_key = data[0:32] self.enc_hmac_key = data[32:64] self.id_key = data[64:96] @@ -457,7 +481,7 @@ class KeyfileKeyBase(AESKeyBase): passphrase = Passphrase.new(allow_empty=True) key = cls(repository) key.repository_id = repository.id - key.init_from_random_data(os.urandom(100)) + key.init_from_random_data() key.init_ciphers() target = key.get_new_target(args) key.save(target, passphrase) @@ -568,11 +592,13 @@ class Blake2KeyfileKey(ID_BLAKE2b_256, KeyfileKey): TYPE = 0x04 NAME = 'key file BLAKE2b' FILE_ID = 'BORG_KEY' + MAC = blake2b_256 class Blake2RepoKey(ID_BLAKE2b_256, RepoKey): TYPE = 0x05 NAME = 'repokey BLAKE2b' + MAC = blake2b_256 class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index 0702301e..99e3a6c8 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -45,8 +45,8 @@ class TestKey: zKvtzupDyTsfrJMqppdGVyYXRpb25zzgABhqCkc2FsdNoAIGTK3TR09UZqw1bPi17gyHOi 7YtSp4BVK7XptWeKh6Vip3ZlcnNpb24B""".strip() - keyfile_blake2_cdata = bytes.fromhex('04dd21cc91140ef009bc9e4dd634d075e39d39025ccce1289c' - '5536f9cb57f5f8130404040404040408ec852921309243b164') + keyfile_blake2_cdata = bytes.fromhex('045d225d745e07af9002d739391e4e7509ff82a04f98debd74' + '012f09b82cc1d07e0404040404040408ec852921309243b164') # Verified against b2sum. Entire string passed to BLAKE2, including the 32 byte key contained in # keyfile_blake2_key_file above is # 037fb9b75b20d623f1d5a568050fccde4a1b7c5f5047432925e941a17c7a2d0d7061796c6f6164 From 989b2286ff171ecae4ed3b821acb4a1a14090766 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 30 Nov 2016 00:37:30 +0100 Subject: [PATCH 0443/1387] fix TypeError in errorhandler, fixes #1903 --- borg/key.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/borg/key.py b/borg/key.py index 944e17d7..e4fcd03d 100644 --- a/borg/key.py +++ b/borg/key.py @@ -143,11 +143,13 @@ class AESKeyBase(KeyBase): def decrypt(self, id, data): if not (data[0] == self.TYPE or data[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)): - raise IntegrityError('Chunk %s: Invalid encryption envelope' % bin_to_hex(id)) + id_str = bin_to_hex(id) if id is not None else '(unknown)' + raise IntegrityError('Chunk %s: Invalid encryption envelope' % id_str) hmac_given = memoryview(data)[1:33] hmac_computed = memoryview(HMAC(self.enc_hmac_key, memoryview(data)[33:], sha256).digest()) if not compare_digest(hmac_computed, hmac_given): - raise IntegrityError('Chunk %s: Encryption envelope checksum mismatch' % bin_to_hex(id)) + id_str = bin_to_hex(id) if id is not None else '(unknown)' + raise IntegrityError('Chunk %s: Encryption envelope checksum mismatch' % id_str) self.dec_cipher.reset(iv=PREFIX + data[33:41]) data = self.compressor.decompress(self.dec_cipher.decrypt(data[41:])) if id: From 71775bac97858d917e99e6030402fa35406a5da8 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 30 Nov 2016 01:06:21 +0100 Subject: [PATCH 0444/1387] check: rebuild manifest if it's corrupted --- borg/archive.py | 10 ++++++++-- borg/testsuite/archiver.py | 13 +++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index f69834ce..3c27c45d 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -19,7 +19,7 @@ from . import xattr from .helpers import Error, uid2user, user2uid, gid2group, group2gid, bin_to_hex, \ parse_timestamp, to_localtime, format_time, format_timedelta, remove_surrogates, \ Manifest, Statistics, decode_dict, make_path_safe, StableDict, int_to_bigint, bigint_to_int, \ - ProgressIndicatorPercent + ProgressIndicatorPercent, IntegrityError from .platform import acl_get, acl_set from .chunker import Chunker from .hashindex import ChunkIndex @@ -849,7 +849,13 @@ class ArchiveChecker: self.error_found = True self.manifest = self.rebuild_manifest() else: - self.manifest, _ = Manifest.load(repository, key=self.key) + try: + self.manifest, _ = Manifest.load(repository, key=self.key) + except IntegrityError as exc: + logger.error('Repository manifest is corrupted: %s', exc) + self.error_found = True + del self.chunks[Manifest.MANIFEST_ID] + self.manifest = self.rebuild_manifest() self.rebuild_refcounts(archive=archive, last=last, prefix=prefix) self.orphan_chunks_check() self.finish(save_space=save_space) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index a7d8ff3e..b493be53 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1433,6 +1433,19 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.assert_in('archive2', output) self.cmd('check', self.repository_location, exit_code=0) + def test_corrupted_manifest(self): + archive, repository = self.open_archive('archive1') + with repository: + manifest = repository.get(Manifest.MANIFEST_ID) + corrupted_manifest = manifest + b'corrupted!' + repository.put(Manifest.MANIFEST_ID, corrupted_manifest) + repository.commit() + self.cmd('check', self.repository_location, exit_code=1) + output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0) + self.assert_in('archive1', output) + self.assert_in('archive2', output) + self.cmd('check', self.repository_location, exit_code=0) + def test_extra_chunks(self): self.cmd('check', self.repository_location, exit_code=0) with Repository(self.repository_location, exclusive=True) as repository: From 029469d54497dfe6905b3844c6b3bc61977a2e07 Mon Sep 17 00:00:00 2001 From: Abogical Date: Tue, 29 Nov 2016 00:29:01 +0200 Subject: [PATCH 0445/1387] Let prune accept and pass the progress argument --- src/borg/archiver.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index d444890a..ac136071 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1024,7 +1024,8 @@ class Archiver: else: if args.output_list: list_logger.info('Pruning archive: %s' % format_archive(archive)) - Archive(repository, key, manifest, archive.name, cache).delete(stats, forced=args.forced) + Archive(repository, key, manifest, archive.name, cache, + progress=args.progress).delete(stats, forced=args.forced) else: if args.output_list: list_logger.info('Keeping archive: %s' % format_archive(archive)) @@ -2244,6 +2245,9 @@ class Archiver: subparser.add_argument('--force', dest='forced', action='store_true', default=False, help='force pruning of corrupted archives') + subparser.add_argument('-p', '--progress', dest='progress', + action='store_true', default=False, + help='show progress display while deleting archives') subparser.add_argument('-s', '--stats', dest='stats', action='store_true', default=False, help='print statistics for the deleted archive') From 146d586b3b31fba57bb5c1987606c362f345dc10 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 30 Nov 2016 01:43:01 +0100 Subject: [PATCH 0446/1387] check: skip corrupted chunks during manifest rebuild --- borg/archive.py | 7 ++++++- borg/testsuite/archiver.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/borg/archive.py b/borg/archive.py index 3c27c45d..ebfa091f 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -912,7 +912,12 @@ class ArchiveChecker: archive_keys_serialized = [msgpack.packb(name) for name in ARCHIVE_KEYS] for chunk_id, _ in self.chunks.iteritems(): cdata = self.repository.get(chunk_id) - data = self.key.decrypt(chunk_id, cdata) + try: + data = self.key.decrypt(chunk_id, cdata) + except IntegrityError as exc: + logger.error('Skipping corrupted chunk: %s', exc) + self.error_found = True + continue if not valid_msgpacked_dict(data, archive_keys_serialized): continue if b'cmdline' not in data or b'\xa7version\x01' not in data: diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index b493be53..67b5e581 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1446,6 +1446,22 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.assert_in('archive2', output) self.cmd('check', self.repository_location, exit_code=0) + def test_manifest_rebuild_corrupted_chunk(self): + archive, repository = self.open_archive('archive1') + with repository: + manifest = repository.get(Manifest.MANIFEST_ID) + corrupted_manifest = manifest + b'corrupted!' + repository.put(Manifest.MANIFEST_ID, corrupted_manifest) + + chunk = repository.get(archive.id) + corrupted_chunk = chunk + b'corrupted!' + repository.put(archive.id, corrupted_chunk) + repository.commit() + self.cmd('check', self.repository_location, exit_code=1) + output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0) + self.assert_in('archive2', output) + self.cmd('check', self.repository_location, exit_code=0) + def test_extra_chunks(self): self.cmd('check', self.repository_location, exit_code=0) with Repository(self.repository_location, exclusive=True) as repository: From d6d3f275df646ec03f9f9bcf457aec9490af6430 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 30 Nov 2016 02:50:20 +0100 Subject: [PATCH 0447/1387] docs: add python3-devel as a dependency for cygwin-based installation --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index ff5cf7d1..d3bc7300 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -266,7 +266,7 @@ Cygwin Use the Cygwin installer to install the dependencies:: - python3 python3-setuptools + python3 python3-devel python3-setuptools binutils gcc-g++ libopenssl openssl-devel liblz4_1 liblz4-devel From b410392899d89bd6fcfd3b6ddb3185f4fbf8cffc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Dec 2016 11:09:52 +0100 Subject: [PATCH 0448/1387] recreate repo: fix only one archive being processed --- src/borg/archive.py | 2 +- src/borg/archiver.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 1bd8e8e6..176653c1 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1447,7 +1447,7 @@ class ArchiveRecreater: return True self.process_items(archive, target) replace_original = target_name is None - return self.save(archive, target, comment, replace_original=replace_original) + self.save(archive, target, comment, replace_original=replace_original) def process_items(self, archive, target): matcher = self.matcher diff --git a/src/borg/archiver.py b/src/borg/archiver.py index ea8d95c9..99c97b94 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1101,8 +1101,7 @@ class Archiver: if recreater.is_temporary_archive(name): continue print('Processing', name) - if not recreater.recreate(name, args.comment): - break + recreater.recreate(name, args.comment) manifest.write() repository.commit() cache.commit() From eb940e6779ec939ae82016506b710dfefac3f119 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Dec 2016 11:20:26 +0100 Subject: [PATCH 0449/1387] recreate: fix rechunking dropping all chunks on the floor --- src/borg/archive.py | 2 +- src/borg/testsuite/archiver.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 176653c1..d30cb4a4 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1528,7 +1528,7 @@ class ArchiveRecreater: # The target.chunker will read the file contents through ChunkIteratorFileWrapper chunk-by-chunk # (does not load the entire file into memory) file = ChunkIteratorFileWrapper(chunk_iterator) - return target.chunker.chunkify(file) + yield from target.chunker.chunkify(file) else: for chunk in chunk_iterator: yield chunk.data diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index c51b06b7..711aca68 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1823,6 +1823,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('recreate', self.repository_location, '--chunker-params', 'default') self.check_cache() # test1 and test2 do deduplicate after recreate + assert int(self.cmd('list', self.repository_location + '::test1', 'input/large_file', '--format={size}')) assert not int(self.cmd('list', self.repository_location + '::test1', 'input/large_file', '--format', '{unique_chunks}')) From 0f07b6acf470f9979b1b6be0ada2159070f098f9 Mon Sep 17 00:00:00 2001 From: "OEM Configuration (temporary user)" Date: Thu, 1 Dec 2016 18:12:15 +0000 Subject: [PATCH 0450/1387] borg info:fixed bug when called without arguments,issue #1914 --- src/borg/archiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index ea8d95c9..ae4c25ef 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2192,7 +2192,7 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='show repository or archive information') subparser.set_defaults(func=self.do_info) - subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', + subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='', type=location_validator(), help='archive or repository to display information about') self.add_archives_filters_args(subparser) From eade10a0a8dd8575511552bf7917ad7a5cedb44f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Dec 2016 11:39:10 +0100 Subject: [PATCH 0451/1387] recreate: fix crash on checkpoint --- src/borg/archive.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index d30cb4a4..1b8faff7 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1517,9 +1517,9 @@ class ArchiveRecreater: if Compressor.detect(old_chunk.data).name == compression_spec['name']: # Stored chunk has the same compression we wanted overwrite = False - chunk_id, size, csize = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite) - self.seen_chunks.add(chunk_id) - return chunk_id, size, csize + chunk_entry = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite) + self.seen_chunks.add(chunk_entry.id) + return chunk_entry def create_chunk_iterator(self, archive, target, chunks): """Return iterator of chunks to store for 'item' from 'archive' in 'target'.""" From c1ccad82c3df97a27a43fe4255b55bdbce857d00 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Dec 2016 12:54:27 +0100 Subject: [PATCH 0452/1387] recreate: update/remove/rename outdated comments --- src/borg/archive.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 1b8faff7..af1a4c08 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1494,12 +1494,11 @@ class ArchiveRecreater: self.print_file_status(file_status(item.mode), item.path) def process_chunks(self, archive, target, item): - """Return new chunk ID list for 'item'.""" if not self.recompress and not target.recreate_rechunkify: for chunk_id, size, csize in item.chunks: self.cache.chunk_incref(chunk_id, target.stats) return item.chunks - chunk_iterator = self.create_chunk_iterator(archive, target, list(item.chunks)) + chunk_iterator = self.iter_chunks(archive, target, list(item.chunks)) compress = self.compression_decider1.decide(item.path) chunk_processor = partial(self.chunk_processor, target, compress) target.chunk_file(item, self.cache, target.stats, chunk_iterator, chunk_processor) @@ -1521,8 +1520,7 @@ class ArchiveRecreater: self.seen_chunks.add(chunk_entry.id) return chunk_entry - def create_chunk_iterator(self, archive, target, chunks): - """Return iterator of chunks to store for 'item' from 'archive' in 'target'.""" + def iter_chunks(self, archive, target, chunks): chunk_iterator = archive.pipeline.fetch_many([chunk_id for chunk_id, _, _ in chunks]) if target.recreate_rechunkify: # The target.chunker will read the file contents through ChunkIteratorFileWrapper chunk-by-chunk @@ -1534,7 +1532,6 @@ class ArchiveRecreater: yield chunk.data def save(self, archive, target, comment=None, replace_original=True): - """Save target archive. If completed, replace source. If not, save temporary with additional 'metadata' dict.""" if self.dry_run: return timestamp = archive.ts.replace(tzinfo=None) From c6f0969352b45309009f74455db0733d2d5a57db Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Dec 2016 13:20:43 +0100 Subject: [PATCH 0453/1387] recreate: update --help --- src/borg/archiver.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 99c97b94..0a188a42 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2355,6 +2355,8 @@ class Archiver: recreate_epilog = textwrap.dedent(""" Recreate the contents of existing archives. + This is an *experimental* feature. Do *not* use this on your only backup. + --exclude, --exclude-from and PATH have the exact same semantics as in "borg create". If PATHs are specified the resulting archive will only contain files from these PATHs. @@ -2371,15 +2373,6 @@ class Archiver: used to have upgraded Borg 0.xx or Attic archives deduplicate with Borg 1.x archives. - borg recreate is signal safe. Send either SIGINT (Ctrl-C on most terminals) or - SIGTERM to request termination. - - Use the *exact same* command line to resume the operation later - changing excludes - or paths will lead to inconsistencies (changed excludes will only apply to newly - processed files/dirs). Changing compression leads to incorrect size information - (which does not cause any data loss, but can be misleading). - Changing chunker params between invocations might lead to data loss. - USE WITH CAUTION. Depending on the PATHs and patterns given, recreate can be used to permanently delete files from archives. From 1c261f6b7b497abd6896ba0f9d7bfb08821ca2be Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Dec 2016 13:25:24 +0100 Subject: [PATCH 0454/1387] setup.py: fix build_usage not processing all commands --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d5194156..e321d41f 100644 --- a/setup.py +++ b/setup.py @@ -185,7 +185,7 @@ class build_usage(Command): print('generating help for %s' % command) if self.generate_level(command + " ", parser, Archiver): - break + continue with open('docs/usage/%s.rst.inc' % command.replace(" ", "_"), 'w') as doc: doc.write(".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n") From 288cac788c677013068ea2025605214941b626c1 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Dec 2016 13:28:49 +0100 Subject: [PATCH 0455/1387] setup.py: build_usage: don't generate includes for debug commands --- docs/usage/debug-delete-obj.rst.inc | 39 --------------------- docs/usage/debug-dump-archive-items.rst.inc | 38 -------------------- docs/usage/debug-dump-repo-objs.rst.inc | 38 -------------------- docs/usage/debug-get-obj.rst.inc | 39 --------------------- docs/usage/debug-info.rst.inc | 35 ------------------ docs/usage/debug-put-obj.rst.inc | 38 -------------------- setup.py | 5 ++- 7 files changed, 4 insertions(+), 228 deletions(-) delete mode 100644 docs/usage/debug-delete-obj.rst.inc delete mode 100644 docs/usage/debug-dump-archive-items.rst.inc delete mode 100644 docs/usage/debug-dump-repo-objs.rst.inc delete mode 100644 docs/usage/debug-get-obj.rst.inc delete mode 100644 docs/usage/debug-info.rst.inc delete mode 100644 docs/usage/debug-put-obj.rst.inc diff --git a/docs/usage/debug-delete-obj.rst.inc b/docs/usage/debug-delete-obj.rst.inc deleted file mode 100644 index 7ed144bf..00000000 --- a/docs/usage/debug-delete-obj.rst.inc +++ /dev/null @@ -1,39 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug-delete-obj: - -borg debug-delete-obj ---------------------- -:: - - usage: borg debug-delete-obj [-h] [--critical] [--error] [--warning] [--info] - [--debug] [--lock-wait N] [--show-rc] - [--no-files-cache] [--umask M] - [--remote-path PATH] - [REPOSITORY] IDs [IDs ...] - - delete the objects with the given IDs from the repo - - positional arguments: - REPOSITORY repository to use - IDs hex object ID(s) to delete from the repo - - optional arguments: - -h, --help show this help message and exit - --critical work on log level CRITICAL - --error work on log level ERROR - --warning work on log level WARNING (default) - --info, -v, --verbose - work on log level INFO - --debug work on log level DEBUG - --lock-wait N wait for the lock, but max. N seconds (default: 1). - --show-rc show/log the return code (rc) - --no-files-cache do not load/update the file metadata cache used to - detect unchanged files - --umask M set umask to M (local and remote, default: 0077) - --remote-path PATH set remote path to executable (default: "borg") - -Description -~~~~~~~~~~~ - -This command deletes objects from the repository. diff --git a/docs/usage/debug-dump-archive-items.rst.inc b/docs/usage/debug-dump-archive-items.rst.inc deleted file mode 100644 index e83bbf85..00000000 --- a/docs/usage/debug-dump-archive-items.rst.inc +++ /dev/null @@ -1,38 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug-dump-archive-items: - -borg debug-dump-archive-items ------------------------------ -:: - - usage: borg debug-dump-archive-items [-h] [--critical] [--error] [--warning] - [--info] [--debug] [--lock-wait N] - [--show-rc] [--no-files-cache] - [--umask M] [--remote-path PATH] - ARCHIVE - - dump (decrypted, decompressed) archive items metadata (not: data) - - positional arguments: - ARCHIVE archive to dump - - optional arguments: - -h, --help show this help message and exit - --critical work on log level CRITICAL - --error work on log level ERROR - --warning work on log level WARNING (default) - --info, -v, --verbose - work on log level INFO - --debug work on log level DEBUG - --lock-wait N wait for the lock, but max. N seconds (default: 1). - --show-rc show/log the return code (rc) - --no-files-cache do not load/update the file metadata cache used to - detect unchanged files - --umask M set umask to M (local and remote, default: 0077) - --remote-path PATH set remote path to executable (default: "borg") - -Description -~~~~~~~~~~~ - -This command dumps raw (but decrypted and decompressed) archive items (only metadata) to files. diff --git a/docs/usage/debug-dump-repo-objs.rst.inc b/docs/usage/debug-dump-repo-objs.rst.inc deleted file mode 100644 index 4fcd45ae..00000000 --- a/docs/usage/debug-dump-repo-objs.rst.inc +++ /dev/null @@ -1,38 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug-dump-repo-objs: - -borg debug-dump-repo-objs -------------------------- -:: - - usage: borg debug-dump-repo-objs [-h] [--critical] [--error] [--warning] - [--info] [--debug] [--lock-wait N] - [--show-rc] [--no-files-cache] [--umask M] - [--remote-path PATH] - REPOSITORY - - dump (decrypted, decompressed) repo objects - - positional arguments: - REPOSITORY repo to dump - - optional arguments: - -h, --help show this help message and exit - --critical work on log level CRITICAL - --error work on log level ERROR - --warning work on log level WARNING (default) - --info, -v, --verbose - work on log level INFO - --debug work on log level DEBUG - --lock-wait N wait for the lock, but max. N seconds (default: 1). - --show-rc show/log the return code (rc) - --no-files-cache do not load/update the file metadata cache used to - detect unchanged files - --umask M set umask to M (local and remote, default: 0077) - --remote-path PATH set remote path to executable (default: "borg") - -Description -~~~~~~~~~~~ - -This command dumps raw (but decrypted and decompressed) repo objects to files. diff --git a/docs/usage/debug-get-obj.rst.inc b/docs/usage/debug-get-obj.rst.inc deleted file mode 100644 index f38f0e88..00000000 --- a/docs/usage/debug-get-obj.rst.inc +++ /dev/null @@ -1,39 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug-get-obj: - -borg debug-get-obj ------------------- -:: - - usage: borg debug-get-obj [-h] [--critical] [--error] [--warning] [--info] - [--debug] [--lock-wait N] [--show-rc] - [--no-files-cache] [--umask M] [--remote-path PATH] - [REPOSITORY] ID PATH - - get object contents from the repository and write it into file - - positional arguments: - REPOSITORY repository to use - ID hex object ID to get from the repo - PATH file to write object data into - - optional arguments: - -h, --help show this help message and exit - --critical work on log level CRITICAL - --error work on log level ERROR - --warning work on log level WARNING (default) - --info, -v, --verbose - work on log level INFO - --debug work on log level DEBUG - --lock-wait N wait for the lock, but max. N seconds (default: 1). - --show-rc show/log the return code (rc) - --no-files-cache do not load/update the file metadata cache used to - detect unchanged files - --umask M set umask to M (local and remote, default: 0077) - --remote-path PATH set remote path to executable (default: "borg") - -Description -~~~~~~~~~~~ - -This command gets an object from the repository. diff --git a/docs/usage/debug-info.rst.inc b/docs/usage/debug-info.rst.inc deleted file mode 100644 index 2f4f7237..00000000 --- a/docs/usage/debug-info.rst.inc +++ /dev/null @@ -1,35 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug-info: - -borg debug-info ---------------- -:: - - usage: borg debug-info [-h] [--critical] [--error] [--warning] [--info] - [--debug] [--lock-wait N] [--show-rc] - [--no-files-cache] [--umask M] [--remote-path PATH] - - display system information for debugging / bug reports - - optional arguments: - -h, --help show this help message and exit - --critical work on log level CRITICAL - --error work on log level ERROR - --warning work on log level WARNING (default) - --info, -v, --verbose - work on log level INFO - --debug work on log level DEBUG - --lock-wait N wait for the lock, but max. N seconds (default: 1). - --show-rc show/log the return code (rc) - --no-files-cache do not load/update the file metadata cache used to - detect unchanged files - --umask M set umask to M (local and remote, default: 0077) - --remote-path PATH set remote path to executable (default: "borg") - -Description -~~~~~~~~~~~ - -This command displays some system information that might be useful for bug -reports and debugging problems. If a traceback happens, this information is -already appended at the end of the traceback. diff --git a/docs/usage/debug-put-obj.rst.inc b/docs/usage/debug-put-obj.rst.inc deleted file mode 100644 index 4f02324b..00000000 --- a/docs/usage/debug-put-obj.rst.inc +++ /dev/null @@ -1,38 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug-put-obj: - -borg debug-put-obj ------------------- -:: - - usage: borg debug-put-obj [-h] [--critical] [--error] [--warning] [--info] - [--debug] [--lock-wait N] [--show-rc] - [--no-files-cache] [--umask M] [--remote-path PATH] - [REPOSITORY] PATH [PATH ...] - - put file(s) contents into the repository - - positional arguments: - REPOSITORY repository to use - PATH file(s) to read and create object(s) from - - optional arguments: - -h, --help show this help message and exit - --critical work on log level CRITICAL - --error work on log level ERROR - --warning work on log level WARNING (default) - --info, -v, --verbose - work on log level INFO - --debug work on log level DEBUG - --lock-wait N wait for the lock, but max. N seconds (default: 1). - --show-rc show/log the return code (rc) - --no-files-cache do not load/update the file metadata cache used to - detect unchanged files - --umask M set umask to M (local and remote, default: 0077) - --remote-path PATH set remote path to executable (default: "borg") - -Description -~~~~~~~~~~~ - -This command puts objects into the repository. diff --git a/setup.py b/setup.py index e321d41f..00e48f3f 100644 --- a/setup.py +++ b/setup.py @@ -181,7 +181,10 @@ class build_usage(Command): return print('found commands: %s' % list(choices.keys())) - for command, parser in choices.items(): + for command, parser in sorted(choices.items()): + if command.startswith('debug'): + print('skipping', command) + continue print('generating help for %s' % command) if self.generate_level(command + " ", parser, Archiver): From 30df63c509d6155cb2f1115a71122843678f5312 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Dec 2016 18:15:11 +0100 Subject: [PATCH 0456/1387] recreate: remove special-cased --dry-run --- src/borg/archive.py | 10 ++-------- src/borg/archiver.py | 11 ++++++----- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index af1a4c08..9d246914 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1394,10 +1394,6 @@ class ArchiveChecker: class ArchiveRecreater: - class FakeTargetArchive: - def __init__(self): - self.stats = Statistics() - class Interrupted(Exception): def __init__(self, metadata=None): self.metadata = metadata or {} @@ -1434,7 +1430,7 @@ class ArchiveRecreater: self.stats = stats self.progress = progress self.print_file_status = file_status_printer or (lambda *args: None) - self.checkpoint_interval = checkpoint_interval + self.checkpoint_interval = None if dry_run else checkpoint_interval def recreate(self, archive_name, comment=None, target_name=None): assert not self.is_temporary_archive(archive_name) @@ -1444,7 +1440,7 @@ class ArchiveRecreater: self.matcher_add_tagged_dirs(archive) if self.matcher.empty() and not self.recompress and not target.recreate_rechunkify and comment is None: logger.info("Skipping archive %s, nothing to do", archive_name) - return True + return self.process_items(archive, target) replace_original = target_name is None self.save(archive, target, comment, replace_original=replace_original) @@ -1588,8 +1584,6 @@ class ArchiveRecreater: def create_target(self, archive, target_name=None): """Create target archive.""" - if self.dry_run: - return self.FakeTargetArchive(), None target_name = target_name or archive.name + '.recreate' target = self.create_target_archive(target_name) # If the archives use the same chunker params, then don't rechunkify diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 0a188a42..2ecba5fd 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1102,9 +1102,10 @@ class Archiver: continue print('Processing', name) recreater.recreate(name, args.comment) - manifest.write() - repository.commit() - cache.commit() + if not args.dry_run: + manifest.write() + repository.commit() + cache.commit() return self.exit_code @with_repository(manifest=False, exclusive=True) @@ -2387,8 +2388,8 @@ class Archiver: When rechunking space usage can be substantial, expect at least the entire deduplicated size of the archives using the previous chunker params. - When recompressing approximately 1 % of the repository size or 512 MB - (whichever is greater) of additional space is used. + When recompressing expect approx throughput / checkpoint-interval in space usage, + assuming all chunks are recompressed. """) subparser = subparsers.add_parser('recreate', parents=[common_parser], add_help=False, description=self.do_recreate.__doc__, From a9395dd8b122e3dd715bf7777c735e4575d4ca99 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Dec 2016 20:19:59 +0100 Subject: [PATCH 0457/1387] recreate: don't rechunkify unless explicitly told so --- src/borg/archive.py | 8 +++++++- src/borg/archiver.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 9d246914..9a9e2cce 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1417,6 +1417,9 @@ class ArchiveRecreater: self.exclude_if_present = exclude_if_present or [] self.keep_tag_files = keep_tag_files + self.rechunkify = chunker_params is not None + if self.rechunkify: + logger.debug('Rechunking archives to %s', chunker_params) self.chunker_params = chunker_params or CHUNKER_PARAMS self.recompress = bool(compression) self.always_recompress = always_recompress @@ -1587,7 +1590,10 @@ class ArchiveRecreater: target_name = target_name or archive.name + '.recreate' target = self.create_target_archive(target_name) # If the archives use the same chunker params, then don't rechunkify - target.recreate_rechunkify = tuple(archive.metadata.get('chunker_params', [])) != self.chunker_params + source_chunker_params = tuple(archive.metadata.get('chunker_params', [])) + target.recreate_rechunkify = self.rechunkify and source_chunker_params != target.chunker_params + if target.recreate_rechunkify: + logger.debug('Rechunking archive from %s to %s', source_chunker_params or '(unknown)', target.chunker_params) return target def create_target_archive(self, name): diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 2ecba5fd..e66d9b65 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2388,7 +2388,7 @@ class Archiver: When rechunking space usage can be substantial, expect at least the entire deduplicated size of the archives using the previous chunker params. - When recompressing expect approx throughput / checkpoint-interval in space usage, + When recompressing expect approx. (throughput / checkpoint-interval) in space usage, assuming all chunks are recompressed. """) subparser = subparsers.add_parser('recreate', parents=[common_parser], add_help=False, From b885841c3963504905e6c0956f5efa0d3c809846 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 30 Nov 2016 12:43:28 +0100 Subject: [PATCH 0458/1387] make item native code This makes an surprisingly large difference. Test case: ~70000 empty files. (Ie. little data shoveling, lots of metadata shoveling). Before: 9.1 seconds +- 0.1 seconds. After: 8.4 seconds +- 0.1 seconds.). That's a huge win for changing a few lines. I'd expect that this improves performance in almost all areas that touch the items (list, delete, prune). --- setup.py | 7 ++++++- src/borg/helpers.py | 4 +++- src/borg/{item.py => item.pyx} | 2 ++ 3 files changed, 11 insertions(+), 2 deletions(-) rename src/borg/{item.py => item.pyx} (99%) diff --git a/setup.py b/setup.py index e2998c8e..5dfcbb30 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ compress_source = 'src/borg/compress.pyx' crypto_source = 'src/borg/crypto.pyx' chunker_source = 'src/borg/chunker.pyx' hashindex_source = 'src/borg/hashindex.pyx' +item_source = 'src/borg/item.pyx' platform_posix_source = 'src/borg/platform/posix.pyx' platform_linux_source = 'src/borg/platform/linux.pyx' platform_darwin_source = 'src/borg/platform/darwin.pyx' @@ -60,6 +61,7 @@ cython_sources = [ crypto_source, chunker_source, hashindex_source, + item_source, platform_posix_source, platform_linux_source, @@ -83,6 +85,7 @@ try: 'src/borg/crypto.c', 'src/borg/chunker.c', 'src/borg/_chunker.c', 'src/borg/hashindex.c', 'src/borg/_hashindex.c', + 'src/borg/item.c', 'src/borg/platform/posix.c', 'src/borg/platform/linux.c', 'src/borg/platform/freebsd.c', @@ -99,6 +102,7 @@ except ImportError: crypto_source = crypto_source.replace('.pyx', '.c') chunker_source = chunker_source.replace('.pyx', '.c') hashindex_source = hashindex_source.replace('.pyx', '.c') + item_source = item_source.replace('.pyx', '.c') platform_posix_source = platform_posix_source.replace('.pyx', '.c') platform_linux_source = platform_linux_source.replace('.pyx', '.c') platform_freebsd_source = platform_freebsd_source.replace('.pyx', '.c') @@ -358,7 +362,8 @@ if not on_rtd: Extension('borg.compress', [compress_source], libraries=['lz4'], include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), Extension('borg.crypto', [crypto_source], libraries=crypto_libraries, include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), Extension('borg.chunker', [chunker_source]), - Extension('borg.hashindex', [hashindex_source]) + Extension('borg.hashindex', [hashindex_source]), + Extension('borg.item', [item_source]), ] if sys.platform.startswith(('linux', 'freebsd', 'darwin')): ext_modules.append(Extension('borg.platform.posix', [platform_posix_source])) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index a1624f1a..0b2e016b 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -86,7 +86,7 @@ class PlaceholderError(Error): def check_extension_modules(): - from . import platform, compress + from . import platform, compress, item if hashindex.API_VERSION != 4: raise ExtensionModuleError if chunker.API_VERSION != 2: @@ -97,6 +97,8 @@ def check_extension_modules(): raise ExtensionModuleError if platform.API_VERSION != platform.OS_API_VERSION != 5: raise ExtensionModuleError + if item.API_VERSION != 1: + raise ExtensionModuleError ArchiveInfo = namedtuple('ArchiveInfo', 'name id ts') diff --git a/src/borg/item.py b/src/borg/item.pyx similarity index 99% rename from src/borg/item.py rename to src/borg/item.pyx index e44e4367..755f96be 100644 --- a/src/borg/item.py +++ b/src/borg/item.pyx @@ -3,6 +3,8 @@ from .helpers import safe_encode, safe_decode from .helpers import bigint_to_int, int_to_bigint from .helpers import StableDict +API_VERSION = 1 + class PropDict: """ From b3707f717513af32190bb152575ca13a87f9c7ba Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 3 Dec 2016 00:12:48 +0100 Subject: [PATCH 0459/1387] Replace backup_io with a singleton This is some 15 times faster than @contextmanager, because no instance creation is involved and no generator has to be maintained. Overall difference is low, but still nice for a very simple change. --- src/borg/archive.py | 39 +++++++++++++++++++---------------- src/borg/testsuite/archive.py | 2 +- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 9a9e2cce..8f97442e 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -125,19 +125,22 @@ class BackupOSError(Exception): return str(self.os_error) -@contextmanager -def backup_io(): - """Context manager changing OSError to BackupOSError.""" - try: - yield - except OSError as os_error: - raise BackupOSError(os_error) from os_error +class BackupIO: + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type and issubclass(exc_type, OSError): + raise BackupOSError(exc_val) from exc_val + + +backup_io = BackupIO() def backup_io_iter(iterator): while True: try: - with backup_io(): + with backup_io: item = next(iterator) except StopIteration: return @@ -475,13 +478,13 @@ Number of files: {0.stats.nfiles}'''.format( pass mode = item.mode if stat.S_ISREG(mode): - with backup_io(): + with backup_io: if not os.path.exists(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) # Hard link? if 'source' in item: source = os.path.join(dest, *item.source.split(os.sep)[stripped_components:]) - with backup_io(): + with backup_io: if os.path.exists(path): os.unlink(path) if item.source not in hardlink_masters: @@ -490,24 +493,24 @@ Number of files: {0.stats.nfiles}'''.format( item.chunks, link_target = hardlink_masters[item.source] if link_target: # Hard link was extracted previously, just link - with backup_io(): + with backup_io: os.link(link_target, path) return # Extract chunks, since the item which had the chunks was not extracted - with backup_io(): + with backup_io: fd = open(path, 'wb') with fd: ids = [c.id for c in item.chunks] for _, data in self.pipeline.fetch_many(ids, is_preloaded=True): if pi: pi.show(increase=len(data), info=[remove_surrogates(item.path)]) - with backup_io(): + with backup_io: if sparse and self.zeros.startswith(data): # all-zero chunk: create a hole in a sparse file fd.seek(len(data), 1) else: fd.write(data) - with backup_io(): + with backup_io: pos = fd.tell() fd.truncate(pos) fd.flush() @@ -519,7 +522,7 @@ Number of files: {0.stats.nfiles}'''.format( # Update master entry with extracted file path, so that following hardlinks don't extract twice. hardlink_masters[item.get('source') or original_path] = (None, path) return - with backup_io(): + with backup_io: # No repository access beyond this point. if stat.S_ISDIR(mode): if not os.path.exists(path): @@ -705,7 +708,7 @@ Number of files: {0.stats.nfiles}'''.format( def stat_ext_attrs(self, st, path): attrs = {} - with backup_io(): + with backup_io: xattrs = xattr.get_all(path, follow_symlinks=False) bsdflags = get_flags(path, st) acl_get(path, attrs, st, self.numeric_owner) @@ -742,7 +745,7 @@ Number of files: {0.stats.nfiles}'''.format( return 'b' # block device def process_symlink(self, path, st): - with backup_io(): + with backup_io: source = os.readlink(path) item = Item(path=make_path_safe(path), source=source) item.update(self.stat_attrs(st, path)) @@ -854,7 +857,7 @@ Number of files: {0.stats.nfiles}'''.format( else: compress = self.compression_decider1.decide(path) self.file_compression_logger.debug('%s -> compression %s', path, compress['name']) - with backup_io(): + with backup_io: fh = Archive._open_rb(path) with os.fdopen(fh, 'rb') as fd: self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd, fh)), compress=compress) diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index 49648ef4..3f315fe4 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -220,7 +220,7 @@ def test_key_length_msgpacked_items(): def test_backup_io(): with pytest.raises(BackupOSError): - with backup_io(): + with backup_io: raise OSError(123) From 8b2e7ec68099fc85ac7298d462db14b5f0ee7486 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 3 Dec 2016 00:16:21 +0100 Subject: [PATCH 0460/1387] don't do "bigint" conversion for nanosecond mtime 2**63 nanoseconds are 292 years, so this change is good until 2262. See also https://en.wikipedia.org/wiki/Time_formatting_and_storage_bugs#Year_2262 I expect that we will have plenty of time to revert this commit in time for 2262. timespec := time_t + long, so it's probably only 64 bits on some platforms anyway. --- src/borg/cache.py | 8 ++++---- src/borg/item.pyx | 7 +++---- src/borg/testsuite/item.py | 11 ----------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index e0f77caa..be1783f8 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -15,7 +15,7 @@ from .hashindex import ChunkIndex, ChunkIndexEntry from .helpers import Location from .helpers import Error from .helpers import get_cache_dir, get_security_dir -from .helpers import decode_dict, int_to_bigint, bigint_to_int, bin_to_hex +from .helpers import bin_to_hex from .helpers import format_file_size from .helpers import yes from .helpers import remove_surrogates @@ -350,7 +350,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" # this is to avoid issues with filesystem snapshots and mtime granularity. # Also keep files from older backups that have not reached BORG_FILES_CACHE_TTL yet. entry = FileCacheEntry(*msgpack.unpackb(item)) - if entry.age == 0 and bigint_to_int(entry.mtime) < self._newest_mtime or \ + if entry.age == 0 and entry.mtime < self._newest_mtime or \ entry.age > 0 and entry.age < ttl: msgpack.pack((path_hash, entry), fd) pi.output('Saving cache config') @@ -567,7 +567,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if not entry: return None entry = FileCacheEntry(*msgpack.unpackb(entry)) - if (entry.size == st.st_size and bigint_to_int(entry.mtime) == st.st_mtime_ns and + if (entry.size == st.st_size and entry.mtime == st.st_mtime_ns and (ignore_inode or entry.inode == st.st_ino)): self.files[path_hash] = msgpack.packb(entry._replace(age=0)) return entry.chunk_ids @@ -577,6 +577,6 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def memorize_file(self, path_hash, st, ids): if not (self.do_files and stat.S_ISREG(st.st_mode)): return - entry = FileCacheEntry(age=0, inode=st.st_ino, size=st.st_size, mtime=int_to_bigint(st.st_mtime_ns), chunk_ids=ids) + entry = FileCacheEntry(age=0, inode=st.st_ino, size=st.st_size, mtime=st.st_mtime_ns, chunk_ids=ids) self.files[path_hash] = msgpack.packb(entry) self._newest_mtime = max(self._newest_mtime or 0, st.st_mtime_ns) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 755f96be..802322a8 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -1,6 +1,5 @@ from .constants import ITEM_KEYS from .helpers import safe_encode, safe_decode -from .helpers import bigint_to_int, int_to_bigint from .helpers import StableDict API_VERSION = 1 @@ -153,9 +152,9 @@ class Item(PropDict): rdev = PropDict._make_property('rdev', int) bsdflags = PropDict._make_property('bsdflags', int) - atime = PropDict._make_property('atime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int) - ctime = PropDict._make_property('ctime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int) - mtime = PropDict._make_property('mtime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int) + atime = PropDict._make_property('atime', int) + ctime = PropDict._make_property('ctime', int) + mtime = PropDict._make_property('mtime', int) hardlink_master = PropDict._make_property('hardlink_master', bool) diff --git a/src/borg/testsuite/item.py b/src/borg/testsuite/item.py index fc60e91d..35934f3b 100644 --- a/src/borg/testsuite/item.py +++ b/src/borg/testsuite/item.py @@ -77,17 +77,6 @@ def test_item_int_property(): item.mode = "invalid" -def test_item_bigint_property(): - item = Item() - small, big = 42, 2 ** 65 - item.atime = small - assert item.atime == small - assert item.as_dict() == {'atime': small} - item.atime = big - assert item.atime == big - assert item.as_dict() == {'atime': b'\0' * 8 + b'\x02'} - - def test_item_user_group_none(): item = Item() item.user = None From b7eaeee26631f145a2b7d73f73f20fb56314c3a8 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 3 Dec 2016 17:50:50 +0100 Subject: [PATCH 0461/1387] clean imports, remove unused code --- src/borg/archive.py | 7 +++---- src/borg/archiver.py | 2 +- src/borg/helpers.py | 20 +------------------- src/borg/key.py | 2 +- src/borg/testsuite/helpers.py | 16 ++-------------- 5 files changed, 8 insertions(+), 39 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 8f97442e..ee3b0976 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -29,12 +29,11 @@ from .helpers import Error, IntegrityError from .helpers import uid2user, user2uid, gid2group, group2gid from .helpers import parse_timestamp, to_localtime from .helpers import format_time, format_timedelta, format_file_size, file_status -from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates, swidth_slice -from .helpers import decode_dict, StableDict -from .helpers import int_to_bigint, bigint_to_int, bin_to_hex +from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates +from .helpers import StableDict +from .helpers import bin_to_hex from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi from .helpers import PathPrefixPattern, FnmatchPattern -from .helpers import consume, chunkit from .helpers import CompressionDecider1, CompressionDecider2, CompressionSpec from .item import Item, ArchiveItem from .key import key_factory diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f7effe5a..7c51d034 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -24,7 +24,7 @@ logger = create_logger() from . import __version__ from . import helpers from .archive import Archive, ArchiveChecker, ArchiveRecreater, Statistics, is_special -from .archive import BackupOSError, CHUNKER_PARAMS +from .archive import BackupOSError from .cache import Cache from .constants import * # NOQA from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 0b2e016b..cd6af5cd 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -693,7 +693,7 @@ def SortBySpec(text): def safe_timestamp(item_timestamp_ns): try: - return datetime.fromtimestamp(bigint_to_int(item_timestamp_ns) / 1e9) + return datetime.fromtimestamp(item_timestamp_ns / 1e9) except OverflowError: # likely a broken file time and datetime did not want to go beyond year 9999 return datetime(9999, 12, 31, 23, 59, 59) @@ -1092,24 +1092,6 @@ class StableDict(dict): return sorted(super().items()) -def bigint_to_int(mtime): - """Convert bytearray to int - """ - if isinstance(mtime, bytes): - return int.from_bytes(mtime, 'little', signed=True) - return mtime - - -def int_to_bigint(value): - """Convert integers larger than 64 bits to bytearray - - Smaller integers are left alone - """ - if value.bit_length() > 63: - return value.to_bytes((value.bit_length() + 9) // 8, 'little', signed=True) - return value - - def is_slow_msgpack(): return msgpack.Packer is msgpack.fallback.Packer diff --git a/src/borg/key.py b/src/borg/key.py index bec595da..3a8168db 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -14,7 +14,7 @@ logger = create_logger() from .constants import * # NOQA from .compress import Compressor, get_compressor -from .crypto import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256 +from .crypto import AES, bytes_to_long, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256 from .helpers import Chunk from .helpers import Error, IntegrityError from .helpers import yes diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index de19a35a..8c5306e8 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -18,7 +18,7 @@ from ..helpers import prune_within, prune_split from ..helpers import get_cache_dir, get_keys_dir, get_security_dir from ..helpers import is_slow_msgpack from ..helpers import yes, TRUISH, FALSISH, DEFAULTISH -from ..helpers import StableDict, int_to_bigint, bigint_to_int, bin_to_hex +from ..helpers import StableDict, bin_to_hex from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams, Chunk from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless from ..helpers import load_excludes @@ -27,19 +27,7 @@ from ..helpers import parse_pattern, PatternMatcher, RegexPattern, PathPrefixPat from ..helpers import swidth_slice from ..helpers import chunkit -from . import BaseTestCase, environment_variable, FakeInputs - - -class BigIntTestCase(BaseTestCase): - - def test_bigint(self): - self.assert_equal(int_to_bigint(0), 0) - self.assert_equal(int_to_bigint(2**63-1), 2**63-1) - self.assert_equal(int_to_bigint(-2**63+1), -2**63+1) - self.assert_equal(int_to_bigint(2**63), b'\x00\x00\x00\x00\x00\x00\x00\x80\x00') - self.assert_equal(int_to_bigint(-2**63), b'\x00\x00\x00\x00\x00\x00\x00\x80\xff') - self.assert_equal(bigint_to_int(int_to_bigint(-2**70)), -2**70) - self.assert_equal(bigint_to_int(int_to_bigint(2**70)), 2**70) +from . import BaseTestCase, FakeInputs def test_bin_to_hex(): From dfd37d09d5981dd9f58ebac60175b8b2c2897457 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 3 Dec 2016 18:34:34 +0100 Subject: [PATCH 0462/1387] add a PR template pointing to guidelines --- .github/PULL_REQUEST_TEMPLATE | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE new file mode 100644 index 00000000..f01ff53c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE @@ -0,0 +1,8 @@ +Thank you for contributing code to Borg, your help is appreciated! + +Please, before you submit a pull request, make sure it complies with the +guidelines given in our documentation: + +https://borgbackup.readthedocs.io/en/latest/development.html#contributions + +**Please remove all above text before submitting your pull request.** From 0994a0a681154383db1772957401cdece2b60cef Mon Sep 17 00:00:00 2001 From: Abogical Date: Thu, 1 Dec 2016 19:52:17 +0200 Subject: [PATCH 0463/1387] Let prune --list display archives deleted per total archives --- src/borg/archiver.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index ac136071..0af64814 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1016,6 +1016,10 @@ class Archiver: stats = Statistics() with Cache(repository, key, manifest, do_files=args.cache_files, lock_wait=self.lock_wait) as cache: list_logger = logging.getLogger('borg.output.list') + if args.output_list: + # set up counters for the progress display + to_delete_len = len(to_delete) + archives_deleted = 0 for archive in archives_checkpoints: if archive in to_delete: if args.dry_run: @@ -1023,7 +1027,9 @@ class Archiver: list_logger.info('Would prune: %s' % format_archive(archive)) else: if args.output_list: - list_logger.info('Pruning archive: %s' % format_archive(archive)) + archives_deleted += 1 + list_logger.info('Pruning archive: %s (%d/%d)' % (format_archive(archive), + archives_deleted, to_delete_len)) Archive(repository, key, manifest, archive.name, cache, progress=args.progress).delete(stats, forced=args.forced) else: From 58752c9de9cec1bb73881d8e8a1005f52906a2b2 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 4 Dec 2016 17:56:23 +0100 Subject: [PATCH 0464/1387] add assertion to demonstrate mac key length issue in test data --- src/borg/key.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/borg/key.py b/src/borg/key.py index 3a8168db..accec7ac 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -226,6 +226,8 @@ class AESKeyBase(KeyBase): self.nonce_manager.ensure_reservation(num_aes_blocks(len(chunk.data))) self.enc_cipher.reset() data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(chunk.data))) + assert (self.MAC is blake2b_256 and len(self.enc_hmac_key) == 128 or + self.MAC is hmac_sha256 and len(self.enc_hmac_key) == 32) hmac = self.MAC(self.enc_hmac_key, data) return b''.join((self.TYPE_STR, hmac, data)) @@ -236,6 +238,8 @@ class AESKeyBase(KeyBase): raise IntegrityError('Chunk %s: Invalid encryption envelope' % id_str) data_view = memoryview(data) hmac_given = data_view[1:33] + assert (self.MAC is blake2b_256 and len(self.enc_hmac_key) == 128 or + self.MAC is hmac_sha256 and len(self.enc_hmac_key) == 32) hmac_computed = memoryview(self.MAC(self.enc_hmac_key, data_view[33:])) if not compare_digest(hmac_computed, hmac_given): id_str = bin_to_hex(id) if id is not None else '(unknown)' From e169510116dfe434a44a4b03f1be5dc1045d09d0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 8 Dec 2016 22:39:04 +0100 Subject: [PATCH 0465/1387] cache: don't create Item in fetch_and_build_idx --- src/borg/cache.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index be1783f8..8dca5405 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -435,10 +435,8 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if not isinstance(item, dict): logger.error('Error: Did not get expected metadata dict - archive corrupted!') continue - item = Item(internal_dict=item) - if 'chunks' in item: - for chunk_id, size, csize in item.chunks: - chunk_idx.add(chunk_id, 1, size, csize) + for chunk_id, size, csize in item.get(b'chunks', []): + chunk_idx.add(chunk_id, 1, size, csize) if self.do_cache: fn = mkpath(archive_id) fn_tmp = mkpath(archive_id, suffix='.tmp') From be18418b74b6b1dfbfb0d246657dd92b9f89b954 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 3 Dec 2016 12:06:22 +0100 Subject: [PATCH 0466/1387] cache: no archive caches => work directly on master cache (no merges) --- src/borg/cache.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 8dca5405..21efcbc9 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -418,8 +418,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" for id in ids: os.unlink(mkpath(id)) - def fetch_and_build_idx(archive_id, repository, key): - chunk_idx = ChunkIndex() + def fetch_and_build_idx(archive_id, repository, key, chunk_idx): cdata = repository.get(archive_id) _, data = key.decrypt(archive_id, cdata) chunk_idx.add(archive_id, 1, len(data), len(cdata)) @@ -446,7 +445,6 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" os.unlink(fn_tmp) else: os.rename(fn_tmp, fn) - return chunk_idx def lookup_name(archive_id): for info in self.manifest.archives.list(): @@ -472,21 +470,27 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" archive_name = lookup_name(archive_id) if self.progress: pi.show(info=[remove_surrogates(archive_name)]) - if archive_id in cached_ids: - archive_chunk_idx_path = mkpath(archive_id) - logger.info("Reading cached archive chunk index for %s ..." % archive_name) - archive_chunk_idx = ChunkIndex.read(archive_chunk_idx_path) + if self.do_cache: + if archive_id in cached_ids: + archive_chunk_idx_path = mkpath(archive_id) + logger.info("Reading cached archive chunk index for %s ..." % archive_name) + archive_chunk_idx = ChunkIndex.read(archive_chunk_idx_path) + else: + logger.info('Fetching and building archive index for %s ...' % archive_name) + archive_chunk_idx = ChunkIndex() + fetch_and_build_idx(archive_id, repository, self.key, archive_chunk_idx) + logger.info("Merging into master chunks index ...") + if chunk_idx is None: + # we just use the first archive's idx as starting point, + # to avoid growing the hash table from 0 size and also + # to save 1 merge call. + chunk_idx = archive_chunk_idx + else: + chunk_idx.merge(archive_chunk_idx) else: - logger.info('Fetching and building archive index for %s ...' % archive_name) - archive_chunk_idx = fetch_and_build_idx(archive_id, repository, self.key) - logger.info("Merging into master chunks index ...") - if chunk_idx is None: - # we just use the first archive's idx as starting point, - # to avoid growing the hash table from 0 size and also - # to save 1 merge call. - chunk_idx = archive_chunk_idx - else: - chunk_idx.merge(archive_chunk_idx) + chunk_idx = chunk_idx or ChunkIndex() + logger.info('Fetching archive index for %s ...' % archive_name) + fetch_and_build_idx(archive_id, repository, self.key, chunk_idx) if self.progress: pi.finish() logger.info('Done.') From 335d599db4f812b3ad6a87bb3a2b31a6c9a772a9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 9 Dec 2016 03:37:13 +0100 Subject: [PATCH 0467/1387] fix location parser for archives with @ char, add test, fixes #1930 we must exclude colon and slash chars from the username, otherwise the term for the user part will match everything up to a @ char in the archive name. a slash can't be in a username as the home directory would contain a illegal slash (slash is path sep), a colon likely also should not be in a username because chown user:group ... syntax. --- borg/helpers.py | 4 ++-- borg/testsuite/helpers.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/borg/helpers.py b/borg/helpers.py index 72129f08..2eae2ac1 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -791,7 +791,7 @@ class Location: # regexes for misc. kinds of supported location specifiers: ssh_re = re.compile(r""" (?Pssh):// # ssh:// - (?:(?P[^@]+)@)? # user@ (optional) + (?:(?P[^@:/]+)@)? # user@ (optional) - username must not contain : or / (?P[^:/]+)(?::(?P\d+))? # host or host:port """ + path_re + optional_archive_re, re.VERBOSE) # path or path::archive @@ -802,7 +802,7 @@ class Location: # note: scp_re is also use for local pathes scp_re = re.compile(r""" ( - (?:(?P[^@]+)@)? # user@ (optional) + (?:(?P[^@:/]+)@)? # user@ (optional) - username must not contain : or / (?P[^:/]+): # host: (don't match / in host to disambiguate from file:) )? # user@host: part is optional """ + path_re + optional_archive_re, re.VERBOSE) # path with optional archive diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py index d0fa86b6..c6dadbec 100644 --- a/borg/testsuite/helpers.py +++ b/borg/testsuite/helpers.py @@ -94,6 +94,13 @@ class TestLocationWithoutEnv: assert repr(Location('/abs/path:with:colons')) == \ "Location(proto='file', user=None, host=None, port=None, path='/abs/path:with:colons', archive=None)" + def test_user_parsing(self): + # see issue #1930 + assert repr(Location('host:path::2016-12-31@23:59:59')) == \ + "Location(proto='ssh', user=None, host='host', port=None, path='path', archive='2016-12-31@23:59:59')" + assert repr(Location('ssh://host/path::2016-12-31@23:59:59')) == \ + "Location(proto='ssh', user=None, host='host', port=None, path='/path', archive='2016-12-31@23:59:59')" + def test_underspecified(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) with pytest.raises(ValueError): From bd8b4a44897e64b52b51d4ae492274b9ad3adf52 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 9 Dec 2016 04:42:23 +0100 Subject: [PATCH 0468/1387] update CHANGES (1.0-maint) --- docs/changes.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 4b13273f..7a79c61c 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -71,6 +71,32 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= +Version 1.0.9 (not released yet) +-------------------------------- + +Bug fixes: + +- borg check: + + - rebuild manifest if it's corrupted + - skip corrupted chunks during manifest rebuild +- fix TypeError in errorhandler, #1903, #1894 + +Other changes: + +- docs: + + - add python3-devel as a dependency for cygwin-based installation + - clarify extract is relative to current directory + - FAQ: fix link to changelog + - markup fixes +- tests: test_get_(cache|keys)_dir: clean env state, #1897 +- setup.py build_usage: + + - fixed build_usage not processing all commands + - fixed build_usage not generating includes for debug commands + + Version 1.0.9rc1 (2016-11-27) ----------------------------- From 09b1079b189439bcf525b12f54be30eaaabd7bbc Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 11 Dec 2016 06:20:10 +0100 Subject: [PATCH 0469/1387] update CHANGES (master / 1.1 beta) --- docs/changes.rst | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 3cd75c4a..8cc4051a 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -130,6 +130,81 @@ Other changes: - Vagrantfile cleanup / code deduplication +Version 1.1.0b3 (not released yet) +---------------------------------- + +Bug fixes: + +- borg recreate: + + - fix rechunking dropping all chunks on the floor + - fix crash on checkpoint + - don't rechunkify unless explicitly told so + - repo-only location: fix only one archive being processed +- borg info:fixed bug when called without arguments, #1914 +- borg init: fix free space check crashing if disk is full, #1821 +- borg debug delete/get obj: fix wrong reference to exception + +New features: + +- add blake2b key modes (use blake2b as MAC) +- crypto: link against system libb2, if possible, otherwise use bundled code +- automatically remove stale locks - set BORG_HOSTNAME_IS_UNIQUE env var + to enable stale lock killing. If set, stale locks in both cache and + repository are deleted. #562 +- borg info : print general repo information, #1680 +- borg check --first / --last / --sort / --prefix, #1663 +- borg mount --first / --last / --sort / --prefix, #1542 +- implement "health" item formatter key, #1749 +- BORG_SECURITY_DIR to remember security related infos outside the cache. + Key type, location and manifest timestamp checks now survive cache + deletion. This also means that you can now delete your cache and avoid + previous warnings, since Borg can still tell it's safe. +- implement BORG_NEW_PASSPHRASE, #1768 + +Other changes: + +- borg recreate: + + - remove special-cased --dry-run + - update --help + - remove bloat: interruption blah, autocommit blah, resuming blah + - re-use existing checkpoint functionality + - archiver tests: add check_cache tool - lints refcounts + +- borg check --verify-data: faster due to linear on-disk-order scan +- borg debug-xxx commands removed, we use "debug xxx" subcommands now, #1627 +- improve metadata handling speed +- shortcut hashindex_set by having hashindex_lookup hint about address +- improve / add progress displays, #1721 +- check for index vs. segment files object count mismatch +- make RPC protocol more extensible: use named parameters. +- RemoteRepository: misc. code cleanups / refactors +- clarify cache/repository README file + +- docs: + + - quickstart: add a comment about other (remote) filesystems + - quickstart: only give one possible ssh url syntax, all others are + documented in usage chapter. + - mention file:// + - document repo URLs / archive location + - clarify borg diff help, #980 + - deployment: synthesize alternative --restrict-to-path example + - improve cache / index docs, esp. files cache docs, #1825 + - document using "git merge 1.0-maint -s recursive -X rename-threshold=20%" + for avoiding troubles when merging the 1.0-maint branch into master. + +- tests: + + - fuse tests: catch ENOTSUP on freebsd + - fuse tests: test troublesome xattrs last + - fix byte range error in test, #1740 + - use monkeypatch to set env vars, but only on pytest based tests. + - point XDG_*_HOME to temp dirs for tests, #1714 + - remove all BORG_* env vars from the outer environment + + Version 1.1.0b2 (2016-10-01) ---------------------------- From 9147c7038b48aa3869983df9b2892259aa469a82 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 11 Dec 2016 15:18:52 +0100 Subject: [PATCH 0470/1387] update 1.1 beta CHANGES --- docs/changes.rst | 16 +++++++--------- docs/faq.rst | 2 ++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 8cc4051a..3308b762 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -135,20 +135,15 @@ Version 1.1.0b3 (not released yet) Bug fixes: -- borg recreate: - - - fix rechunking dropping all chunks on the floor - - fix crash on checkpoint - - don't rechunkify unless explicitly told so - - repo-only location: fix only one archive being processed -- borg info:fixed bug when called without arguments, #1914 +- borg recreate: don't rechunkify unless explicitly told so +- borg info: fixed bug when called without arguments, #1914 - borg init: fix free space check crashing if disk is full, #1821 - borg debug delete/get obj: fix wrong reference to exception New features: -- add blake2b key modes (use blake2b as MAC) -- crypto: link against system libb2, if possible, otherwise use bundled code +- add blake2b key modes (use blake2b as MAC). This links against system libb2, + if possible, otherwise uses bundled code - automatically remove stale locks - set BORG_HOSTNAME_IS_UNIQUE env var to enable stale lock killing. If set, stale locks in both cache and repository are deleted. #562 @@ -172,6 +167,9 @@ Other changes: - re-use existing checkpoint functionality - archiver tests: add check_cache tool - lints refcounts +- fixed cache sync performance regression from 1.1.0b1 onwards, #1940 +- syncing the cache without chunks.archive.d (see :ref:`disable_archive_chunks`) + now avoids any merges and is thus faster, #1940 - borg check --verify-data: faster due to linear on-disk-order scan - borg debug-xxx commands removed, we use "debug xxx" subcommands now, #1627 - improve metadata handling speed diff --git a/docs/faq.rst b/docs/faq.rst index 9c9793bc..cad34e20 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -237,6 +237,8 @@ serialized way in a single script, you need to give them --lock-wait N (with N being a bit more than the time the server needs to terminate broken down connections and release the lock). +.. _disable_archive_chunks: + The borg cache eats way too much disk space, what can I do? ----------------------------------------------------------- From c6017abfb7bd32f3cd598ad8df6123bd02d803b3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 13 Dec 2016 19:27:01 +0100 Subject: [PATCH 0471/1387] get back pytest's pretty assertion failures, fixes #1938 --- conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conftest.py b/conftest.py index d5423660..d622e6df 100644 --- a/conftest.py +++ b/conftest.py @@ -4,6 +4,8 @@ import sys import pytest +# needed to get pretty assertion failures in unit tests: +pytest.register_assert_rewrite('borg') # This is a hack to fix path problems because "borg" (the package) is in the source root. # When importing the conftest an "import borg" can incorrectly import the borg from the From 292ff42655ff4db95b3405952491cd42399ed3ea Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 9 Dec 2016 03:54:20 +0100 Subject: [PATCH 0472/1387] refactor common regex part into optional_user_re --- borg/helpers.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/borg/helpers.py b/borg/helpers.py index 2eae2ac1..ff7d1607 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -773,6 +773,17 @@ class Location: """ proto = user = host = port = path = archive = None + # user must not contain "@", ":" or "/". + # Quoting adduser error message: + # "To avoid problems, the username should consist only of letters, digits, + # underscores, periods, at signs and dashes, and not start with a dash + # (as defined by IEEE Std 1003.1-2001)." + # We use "@" as separator between username and hostname, so we must + # disallow it within the pure username part. + optional_user_re = r""" + (?:(?P[^@:/]+)@)? + """ + # path must not contain :: (it ends at :: or string end), but may contain single colons. # to avoid ambiguities with other regexes, it must also not start with ":". path_re = r""" @@ -791,7 +802,7 @@ class Location: # regexes for misc. kinds of supported location specifiers: ssh_re = re.compile(r""" (?Pssh):// # ssh:// - (?:(?P[^@:/]+)@)? # user@ (optional) - username must not contain : or / + """ + optional_user_re + r""" # user@ (optional) (?P[^:/]+)(?::(?P\d+))? # host or host:port """ + path_re + optional_archive_re, re.VERBOSE) # path or path::archive @@ -802,7 +813,7 @@ class Location: # note: scp_re is also use for local pathes scp_re = re.compile(r""" ( - (?:(?P[^@:/]+)@)? # user@ (optional) - username must not contain : or / + """ + optional_user_re + r""" # user@ (optional) (?P[^:/]+): # host: (don't match / in host to disambiguate from file:) )? # user@host: part is optional """ + path_re + optional_archive_re, re.VERBOSE) # path with optional archive From 60bbd7a944f101e3414b4c2feb941ba1ceb9bdb1 Mon Sep 17 00:00:00 2001 From: TW Date: Wed, 14 Dec 2016 01:29:43 +0100 Subject: [PATCH 0473/1387] update CHANGES (1.0-maint) (#1954) --- docs/changes.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 7a79c61c..4ff900e7 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -80,7 +80,8 @@ Bug fixes: - rebuild manifest if it's corrupted - skip corrupted chunks during manifest rebuild -- fix TypeError in errorhandler, #1903, #1894 +- fix TypeError in integrity error handler, #1903, #1894 +- fix location parser for archives with @ char (regression introduced in 1.0.8), #1930 Other changes: @@ -90,7 +91,10 @@ Other changes: - clarify extract is relative to current directory - FAQ: fix link to changelog - markup fixes -- tests: test_get_(cache|keys)_dir: clean env state, #1897 +- tests: + + - test_get_(cache|keys)_dir: clean env state, #1897 + - get back pytest's pretty assertion failures, #1938 - setup.py build_usage: - fixed build_usage not processing all commands From 5a40870416a85ab08c3dca0d9b62808450cf2101 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 14 Dec 2016 00:00:09 +0100 Subject: [PATCH 0474/1387] add a borg debug/key dummy command, fixes #1932 the problem was that there neither was a do_debug implementation for the case someone just enters "borg debug", nor did the parser inherit from common_parser (so accessing .umask triggered an exception before setup_logging() was called, which triggered another exception when log output should have been emitted). same for do_key ("borg key"). added a generic handler that just prints the subcommand help. --- borg/archiver.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index bd338225..cb02d4d8 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -992,6 +992,11 @@ class Archiver: parser.error('No help available on %s' % (args.topic,)) return self.exit_code + def do_subcommand_help(self, parser, args): + """display infos about subcommand""" + parser.print_help() + return EXIT_SUCCESS + def preprocess_args(self, args): deprecations = [ # ('--old', '--new', 'Warning: "--old" has been deprecated. Use "--new" instead.'), @@ -1148,13 +1153,14 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False)) - subparser = subparsers.add_parser('key', + subparser = subparsers.add_parser('key', parents=[common_parser], description="Manage a keyfile or repokey of a repository", epilog="", formatter_class=argparse.RawDescriptionHelpFormatter, help='manage repository key') key_parsers = subparser.add_subparsers(title='required arguments', metavar='') + subparser.set_defaults(func=functools.partial(self.do_subcommand_help, subparser)) key_export_epilog = textwrap.dedent(""" If repository encryption is used, the repository is inaccessible @@ -1681,13 +1687,14 @@ class Archiver: in case you ever run into some severe malfunction. Use them only if you know what you are doing or if a trusted developer tells you what to do.""") - subparser = subparsers.add_parser('debug', + subparser = subparsers.add_parser('debug', parents=[common_parser], description='debugging command (not intended for normal use)', epilog=debug_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, help='debugging command (not intended for normal use)') debug_parsers = subparser.add_subparsers(title='required arguments', metavar='') + subparser.set_defaults(func=functools.partial(self.do_subcommand_help, subparser)) debug_info_epilog = textwrap.dedent(""" This command displays some system information that might be useful for bug From 82cd1fd39267d0411816147c1a9243af854df690 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 14 Dec 2016 03:42:38 +0100 Subject: [PATCH 0475/1387] run setup.py build_usage --- docs/usage/list.rst.inc | 2 ++ docs/usage/recreate.rst.inc | 15 ++++----------- docs/usage/serve.rst.inc | 2 +- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index ce6bd804..c848e592 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -95,3 +95,5 @@ The following keys are available for --format: - archiveid - archivename - extra: prepends {source} with " -> " for soft links and " link to " for hard links + + - health: either "healthy" (file ok) or "broken" (if file has all-zero replacement chunks) diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index 8b07f2f3..02f06a8c 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -64,6 +64,8 @@ Description Recreate the contents of existing archives. +This is an *experimental* feature. Do *not* use this on your only backup. + --exclude, --exclude-from and PATH have the exact same semantics as in "borg create". If PATHs are specified the resulting archive will only contain files from these PATHs. @@ -80,15 +82,6 @@ There is no risk of data loss by this. used to have upgraded Borg 0.xx or Attic archives deduplicate with Borg 1.x archives. -borg recreate is signal safe. Send either SIGINT (Ctrl-C on most terminals) or -SIGTERM to request termination. - -Use the *exact same* command line to resume the operation later - changing excludes -or paths will lead to inconsistencies (changed excludes will only apply to newly -processed files/dirs). Changing compression leads to incorrect size information -(which does not cause any data loss, but can be misleading). -Changing chunker params between invocations might lead to data loss. - USE WITH CAUTION. Depending on the PATHs and patterns given, recreate can be used to permanently delete files from archives. @@ -103,5 +96,5 @@ With --target the original archive is not replaced, instead a new archive is cre When rechunking space usage can be substantial, expect at least the entire deduplicated size of the archives using the previous chunker params. -When recompressing approximately 1 % of the repository size or 512 MB -(whichever is greater) of additional space is used. +When recompressing expect approx. (throughput / checkpoint-interval) in space usage, +assuming all chunks are recompressed. diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index 933f72b9..351af5e4 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -10,7 +10,7 @@ borg serve optional arguments ``--restrict-to-path PATH`` - | restrict repository access to PATH + | restrict repository access to PATH. Can be specified multiple times to allow the client access to several directories. Access to all sub-directories is granted implicitly; PATH doesn't need to directly point to a repository. ``--append-only`` | only allow appending to repository segment files From 34e19ccb6aebd48704299d21909b175c89c663aa Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 14 Dec 2016 15:20:08 +0100 Subject: [PATCH 0476/1387] mention failed operation in per-file warnings on the one hand one can say it's ugly global state, on the other it's totally handy! just have to keep that in mind for MT, but it's rather obvious. --- src/borg/archive.py | 34 +++++++++++++++++++++++----------- src/borg/archiver.py | 6 +++--- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index ee3b0976..c39a1166 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -114,29 +114,40 @@ class BackupOSError(Exception): Any unwrapped IO error is critical and aborts execution (for example repository IO failure). """ - def __init__(self, os_error): + def __init__(self, op, os_error): + self.op = op self.os_error = os_error self.errno = os_error.errno self.strerror = os_error.strerror self.filename = os_error.filename def __str__(self): - return str(self.os_error) + if self.op: + return '%s: %s' % (self.op, self.os_error) + else: + return str(self.os_error) class BackupIO: + op = '' + + def __call__(self, op=''): + self.op = op + return self + def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): if exc_type and issubclass(exc_type, OSError): - raise BackupOSError(exc_val) from exc_val + raise BackupOSError(self.op, exc_val) from exc_val backup_io = BackupIO() def backup_io_iter(iterator): + backup_io.op = 'read' while True: try: with backup_io: @@ -477,13 +488,13 @@ Number of files: {0.stats.nfiles}'''.format( pass mode = item.mode if stat.S_ISREG(mode): - with backup_io: + with backup_io('makedirs'): if not os.path.exists(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) # Hard link? if 'source' in item: source = os.path.join(dest, *item.source.split(os.sep)[stripped_components:]) - with backup_io: + with backup_io('link'): if os.path.exists(path): os.unlink(path) if item.source not in hardlink_masters: @@ -496,20 +507,20 @@ Number of files: {0.stats.nfiles}'''.format( os.link(link_target, path) return # Extract chunks, since the item which had the chunks was not extracted - with backup_io: + with backup_io('open'): fd = open(path, 'wb') with fd: ids = [c.id for c in item.chunks] for _, data in self.pipeline.fetch_many(ids, is_preloaded=True): if pi: pi.show(increase=len(data), info=[remove_surrogates(item.path)]) - with backup_io: + with backup_io('write'): if sparse and self.zeros.startswith(data): # all-zero chunk: create a hole in a sparse file fd.seek(len(data), 1) else: fd.write(data) - with backup_io: + with backup_io('truncate'): pos = fd.tell() fd.truncate(pos) fd.flush() @@ -556,6 +567,7 @@ Number of files: {0.stats.nfiles}'''.format( Does not access the repository. """ + backup_io.op = 'attrs' uid = gid = None if not self.numeric_owner: uid = user2uid(item.user) @@ -707,7 +719,7 @@ Number of files: {0.stats.nfiles}'''.format( def stat_ext_attrs(self, st, path): attrs = {} - with backup_io: + with backup_io('extended stat'): xattrs = xattr.get_all(path, follow_symlinks=False) bsdflags = get_flags(path, st) acl_get(path, attrs, st, self.numeric_owner) @@ -744,7 +756,7 @@ Number of files: {0.stats.nfiles}'''.format( return 'b' # block device def process_symlink(self, path, st): - with backup_io: + with backup_io('readlink'): source = os.readlink(path) item = Item(path=make_path_safe(path), source=source) item.update(self.stat_attrs(st, path)) @@ -856,7 +868,7 @@ Number of files: {0.stats.nfiles}'''.format( else: compress = self.compression_decider1.decide(path) self.file_compression_logger.debug('%s -> compression %s', path, compress['name']) - with backup_io: + with backup_io('open'): fh = Archive._open_rb(path) with os.fdopen(fh, 'rb') as fd: self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd, fh)), compress=compress) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 7c51d034..3576b292 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -365,7 +365,7 @@ class Archiver: try: st = os.lstat(path) except OSError as e: - self.print_warning('%s: %s', path, e) + self.print_warning('%s: stat: %s', path, e) return if (st.st_ino, st.st_dev) in skip_inodes: return @@ -380,7 +380,7 @@ class Archiver: self.print_file_status('x', path) return except OSError as e: - self.print_warning('%s: %s', path, e) + self.print_warning('%s: flags: %s', path, e) return if stat.S_ISREG(st.st_mode): if not dry_run: @@ -407,7 +407,7 @@ class Archiver: entries = helpers.scandir_inorder(path) except OSError as e: status = 'E' - self.print_warning('%s: %s', path, e) + self.print_warning('%s: scandir: %s', path, e) else: for dirent in entries: normpath = os.path.normpath(dirent.path) From 04340ae8b1d6ae89d90f804ca19c685746d446f9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 15 Dec 2016 03:02:06 +0100 Subject: [PATCH 0477/1387] pytest: only rewrite the testsuite, fixes #1938 do not rewrite the borg application code, just the test code, so the bytecode tested is identical / very close to the bytecode used in practice. --- conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index d622e6df..197af6fd 100644 --- a/conftest.py +++ b/conftest.py @@ -5,7 +5,7 @@ import sys import pytest # needed to get pretty assertion failures in unit tests: -pytest.register_assert_rewrite('borg') +pytest.register_assert_rewrite('borg.testsuite') # This is a hack to fix path problems because "borg" (the package) is in the source root. # When importing the conftest an "import borg" can incorrectly import the borg from the From 6b6ddecd9301cbd3a657bb6d78e58554f2aa7882 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 17 Dec 2016 00:25:49 +0100 Subject: [PATCH 0478/1387] document windows 10 linux subsystem install also add note about remote repos being broken on cygwin. --- README.rst | 1 + docs/installation.rst | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 07e8b0d3..6655e77a 100644 --- a/README.rst +++ b/README.rst @@ -75,6 +75,7 @@ Main features * FreeBSD * OpenBSD and NetBSD (no xattrs/ACLs support or binaries yet) * Cygwin (not supported, no binaries yet) + * Linux Subsystem of Windows 10 (not supported) **Free and Open Source Software** * security and functionality can be audited independently diff --git a/docs/installation.rst b/docs/installation.rst index d3bc7300..bcd5d479 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -257,12 +257,21 @@ and commands to make fuse work for using the mount command. sysctl vfs.usermount=1 +Windows 10's Linux Subsystem +++++++++++++++++++++++++++++ + +.. note:: + Running under Windows 10's Linux Subsystem is experimental and has not been tested much yet. + +Just follow the Ubuntu Linux installation steps. You can omit the FUSE stuff, it won't work anyway. + + Cygwin ++++++ .. note:: Running under Cygwin is experimental and has only been tested with Cygwin - (x86-64) v2.5.2. + (x86-64) v2.5.2. Remote repositories are known broken, local repositories should work. Use the Cygwin installer to install the dependencies:: From 61370082d63af8abd2ad40ecf962df17b2476f66 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 17 Dec 2016 00:37:00 +0100 Subject: [PATCH 0479/1387] catch errno.ENOSYS for mknod (win 10 lxsys) mknod raises this when running as non-root under Windows 10's Linux Subsystem. --- borg/testsuite/archiver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 67b5e581..815c8943 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -311,7 +311,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): except PermissionError: have_root = False except OSError as e: - if e.errno != errno.EINVAL: + # Note: ENOSYS "Function not implemented" happens as non-root on Win 10 Linux Subsystem. + if e.errno not in (errno.EINVAL, errno.ENOSYS): raise have_root = False return have_root From 420c984f05ff8bc1b16c3b4b661bdaebe285a421 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 30 Oct 2016 18:18:05 +0100 Subject: [PATCH 0480/1387] fix wrong duration if clock jumps during create --- borg/archive.py | 9 ++++++--- borg/archiver.py | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index ebfa091f..f93e0a8c 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -1,5 +1,5 @@ from contextlib import contextmanager -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta from getpass import getuser from itertools import groupby import errno @@ -186,7 +186,7 @@ class Archive: def __init__(self, repository, key, manifest, name, cache=None, create=False, checkpoint_interval=300, numeric_owner=False, noatime=False, noctime=False, progress=False, - chunker_params=CHUNKER_PARAMS, start=None, end=None): + chunker_params=CHUNKER_PARAMS, start=None, start_monotonic=None, end=None): self.cwd = os.getcwd() self.key = key self.repository = repository @@ -200,9 +200,12 @@ class Archive: self.numeric_owner = numeric_owner self.noatime = noatime self.noctime = noctime + assert (start is None) == (start_monotonic is None), 'Logic error: if start is given, start_monotonic must be given as well and vice versa.' if start is None: start = datetime.utcnow() + start_monotonic = time.monotonic() self.start = start + self.start_monotonic = start_monotonic if end is None: end = datetime.utcnow() self.end = end @@ -302,7 +305,7 @@ Number of files: {0.stats.nfiles}'''.format( raise self.AlreadyExists(name) self.items_buffer.flush(flush=True) if timestamp is None: - self.end = datetime.utcnow() + self.end = self.start + timedelta(seconds=time.monotonic() - self.start_monotonic) start = self.start end = self.end else: diff --git a/borg/archiver.py b/borg/archiver.py index cb02d4d8..5ee61297 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -5,7 +5,6 @@ from operator import attrgetter import argparse import functools import inspect -import io import os import re import shlex @@ -13,6 +12,7 @@ import signal import stat import sys import textwrap +import time import traceback import collections @@ -263,7 +263,6 @@ class Archiver: if args.progress: archive.stats.show_progress(final=True) if args.stats: - archive.end = datetime.utcnow() log_multi(DASHES, str(archive), DASHES, @@ -276,6 +275,7 @@ class Archiver: self.ignore_inode = args.ignore_inode dry_run = args.dry_run t0 = datetime.utcnow() + t0_monotonic = time.monotonic() if not dry_run: key.compressor = Compressor(**args.compression) with Cache(repository, key, manifest, do_files=args.cache_files, lock_wait=self.lock_wait) as cache: @@ -283,7 +283,7 @@ class Archiver: create=True, checkpoint_interval=args.checkpoint_interval, numeric_owner=args.numeric_owner, noatime=args.noatime, noctime=args.noctime, progress=args.progress, - chunker_params=args.chunker_params, start=t0) + chunker_params=args.chunker_params, start=t0, start_monotonic=t0_monotonic) create_inner(archive, cache) else: create_inner(None, None) From 2dc558a02e24461f0cda9869cb49bcd73038a815 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 30 Oct 2016 18:18:23 +0100 Subject: [PATCH 0481/1387] fix create progress not updating if clock jumps --- borg/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/borg/helpers.py b/borg/helpers.py index ff7d1607..a8106aee 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -219,7 +219,7 @@ class Statistics: return format_file_size(self.csize) def show_progress(self, item=None, final=False, stream=None, dt=None): - now = time.time() + now = time.monotonic() if dt is None or now - self.last_progress > dt: self.last_progress = now columns, lines = get_terminal_size() From a8d921a54cbb9a5e7b86668bd04a050c23110ad4 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Dec 2016 11:37:56 +0100 Subject: [PATCH 0482/1387] base archive timestamps on end time The assumption is that if the clock jumps during the Borg run that it was jump-corrected and is now correct, while the start timestamp would be wrong. --- borg/archive.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/borg/archive.py b/borg/archive.py index f93e0a8c..5001d310 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -305,7 +305,8 @@ Number of files: {0.stats.nfiles}'''.format( raise self.AlreadyExists(name) self.items_buffer.flush(flush=True) if timestamp is None: - self.end = self.start + timedelta(seconds=time.monotonic() - self.start_monotonic) + self.end = datetime.utcnow() + self.start = self.end - timedelta(seconds=time.monotonic() - self.start_monotonic) start = self.start end = self.end else: From f5d6093ccc0b41ea7465dfded9091b0d95d01403 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Dec 2016 11:46:20 +0100 Subject: [PATCH 0483/1387] fix checkpoints when clock jumps --- borg/archive.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index 5001d310..64be0f4d 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -215,7 +215,7 @@ class Archive: self.chunker = Chunker(self.key.chunk_seed, *chunker_params) if name in manifest.archives: raise self.AlreadyExists(name) - self.last_checkpoint = time.time() + self.last_checkpoint = time.monotonic() i = 0 while True: self.checkpoint_name = '%s.checkpoint%s' % (name, i and ('.%d' % i) or '') @@ -290,9 +290,9 @@ Number of files: {0.stats.nfiles}'''.format( if self.show_progress: self.stats.show_progress(item=item, dt=0.2) self.items_buffer.add(item) - if time.time() - self.last_checkpoint > self.checkpoint_interval: + if time.monotonic() - self.last_checkpoint > self.checkpoint_interval: self.write_checkpoint() - self.last_checkpoint = time.time() + self.last_checkpoint = time.monotonic() def write_checkpoint(self): self.save(self.checkpoint_name) From 445365b3ff19830aefe2c7b1332cba05703fcb97 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Dec 2016 12:00:25 +0100 Subject: [PATCH 0484/1387] update changes --- docs/changes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 4ff900e7..71677bcb 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -82,6 +82,9 @@ Bug fixes: - skip corrupted chunks during manifest rebuild - fix TypeError in integrity error handler, #1903, #1894 - fix location parser for archives with @ char (regression introduced in 1.0.8), #1930 +- fix wrong duration/timestamps if system clock jumped during a create +- fix progress display not updating if system clock jumps backwards +- fix checkpoint interval being incorrect if system clock jumps Other changes: From baa8baafdbd342bdaec0e39aa59fc160bea09e2c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Dec 2016 12:55:16 +0100 Subject: [PATCH 0485/1387] create: fix duration if --timestamp is given --- borg/archive.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index 64be0f4d..641fd08d 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -304,15 +304,17 @@ Number of files: {0.stats.nfiles}'''.format( if name in self.manifest.archives: raise self.AlreadyExists(name) self.items_buffer.flush(flush=True) + duration = timedelta(seconds=time.monotonic() - self.start_monotonic) if timestamp is None: self.end = datetime.utcnow() - self.start = self.end - timedelta(seconds=time.monotonic() - self.start_monotonic) + self.start = self.end - duration start = self.start end = self.end else: self.end = timestamp - start = timestamp - end = timestamp # we only have 1 value + self.start = timestamp - duration + end = timestamp + start = self.start metadata = StableDict({ 'version': 1, 'name': name, From 63ce627a35a198ca9b04a404645e691eff84ab0a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Dec 2016 13:59:37 +0100 Subject: [PATCH 0486/1387] fix in-file checkpoints when clock jumps --- src/borg/archive.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index c5d1ef85..2c7dfa96 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -793,9 +793,9 @@ Number of files: {0.stats.nfiles}'''.format( item.chunks.append(chunk_processor(data)) if self.show_progress: self.stats.show_progress(item=item, dt=0.2) - if self.checkpoint_interval and time.time() - self.last_checkpoint > self.checkpoint_interval: + if self.checkpoint_interval and time.monotonic() - self.last_checkpoint > self.checkpoint_interval: from_chunk, part_number = self.write_part_file(item, from_chunk, part_number) - self.last_checkpoint = time.time() + self.last_checkpoint = time.monotonic() else: if part_number > 1: if item.chunks[from_chunk:]: @@ -803,7 +803,7 @@ Number of files: {0.stats.nfiles}'''.format( # chunks (if any) into a part item also (so all parts can be concatenated to get # the complete file): from_chunk, part_number = self.write_part_file(item, from_chunk, part_number) - self.last_checkpoint = time.time() + self.last_checkpoint = time.monotonic() # if we created part files, we have referenced all chunks from the part files, # but we also will reference the same chunks also from the final, complete file: From 84d13751d56cc07b7ac7777ae717e7ccafa3d8b6 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Dec 2016 14:33:40 +0100 Subject: [PATCH 0487/1387] Fix processing of remote ~/ and ~user/ paths --- src/borg/remote.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 9a5221bf..9dc85f1e 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -263,14 +263,26 @@ class RepositoryServer: # pragma: no cover # not a known old format, send newest negotiate this version knows return {'server_version': BORG_VERSION} - def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False): + def _resolve_path(self, path): if isinstance(path, bytes): path = os.fsdecode(path) - if path.startswith('/~'): # /~/x = path x relative to home dir, /~username/x = relative to "user" home dir - path = os.path.join(get_home_dir(), path[2:]) # XXX check this (see also 1.0-maint), is it correct for ~u? + # Leading slash is always present with URI (ssh://), but not with short-form (who@host:path). + if path.startswith('/~/'): # /~/x = path x relative to home dir + path = os.path.join(get_home_dir(), path[3:]) + elif path.startswith('~/'): + path = os.path.join(get_home_dir(), path[2:]) + elif path.startswith('/~'): # /~username/x = relative to "user" home dir + path = os.path.expanduser(path[1:]) + elif path.startswith('~'): + path = os.path.expanduser(path) elif path.startswith('/./'): # /./x = path x relative to cwd path = path[3:] - path = os.path.realpath(path) + return os.path.realpath(path) + + def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False): + logging.debug('Resolving repository path %r', path) + path = self._resolve_path(path) + logging.debug('Resolved repository path to %r', path) if self.restrict_to_paths: # if --restrict-to-path P is given, we make sure that we only operate in/below path P. # for the prefix check, it is important that the compared pathes both have trailing slashes, From af923e261bacbaf0bfb4be77a57d3604cf7e9291 Mon Sep 17 00:00:00 2001 From: enkore Date: Sat, 17 Dec 2016 15:23:47 +0100 Subject: [PATCH 0488/1387] conftest: pytest 2 compat --- conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 197af6fd..596a4b21 100644 --- a/conftest.py +++ b/conftest.py @@ -5,7 +5,8 @@ import sys import pytest # needed to get pretty assertion failures in unit tests: -pytest.register_assert_rewrite('borg.testsuite') +if hasattr(pytest, 'register_assert_rewrite'): + pytest.register_assert_rewrite('borg.testsuite') # This is a hack to fix path problems because "borg" (the package) is in the source root. # When importing the conftest an "import borg" can incorrectly import the borg from the From 0c959cb67a232abfaafcdabba91299a04e364df6 Mon Sep 17 00:00:00 2001 From: enkore Date: Sat, 17 Dec 2016 15:58:51 +0100 Subject: [PATCH 0489/1387] pytest 2 compat --- conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index a1822767..b23f30d1 100644 --- a/conftest.py +++ b/conftest.py @@ -3,7 +3,8 @@ import os import pytest # needed to get pretty assertion failures in unit tests: -pytest.register_assert_rewrite('borg.testsuite') +if hasattr(pytest, 'register_assert_rewrite'): + pytest.register_assert_rewrite('borg.testsuite') from borg.logger import setup_logging From e28b470cfb72a99528e8f665ca89332248d0918d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Dec 2016 18:00:03 +0100 Subject: [PATCH 0490/1387] Fix subsubparsers for Python <3.4.3 This works around http://bugs.python.org/issue9351 Since Debian and Ubuntu ship 3.4.2 and 3.4.0 respectively. --- borg/archiver.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 5ee61297..2cdfe37f 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1160,7 +1160,7 @@ class Archiver: help='manage repository key') key_parsers = subparser.add_subparsers(title='required arguments', metavar='') - subparser.set_defaults(func=functools.partial(self.do_subcommand_help, subparser)) + subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser)) key_export_epilog = textwrap.dedent(""" If repository encryption is used, the repository is inaccessible @@ -1694,7 +1694,7 @@ class Archiver: help='debugging command (not intended for normal use)') debug_parsers = subparser.add_subparsers(title='required arguments', metavar='') - subparser.set_defaults(func=functools.partial(self.do_subcommand_help, subparser)) + subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser)) debug_info_epilog = textwrap.dedent(""" This command displays some system information that might be useful for bug @@ -1902,11 +1902,13 @@ class Archiver: def run(self, args): os.umask(args.umask) # early, before opening files self.lock_wait = args.lock_wait - setup_logging(level=args.log_level, is_serve=args.func == self.do_serve) # do not use loggers before this! + # This works around http://bugs.python.org/issue9351 + func = getattr(args, 'func', None) or getattr(args, 'fallback_func') + setup_logging(level=args.log_level, is_serve=func == self.do_serve) # do not use loggers before this! check_extension_modules() if is_slow_msgpack(): logger.warning("Using a pure-python msgpack! This will result in lower performance.") - return args.func(args) + return func(args) def sig_info_handler(sig_no, stack): # pragma: no cover From 28ad779a6f64b484d57ed92097db5cf884ad1842 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 11 Dec 2016 13:32:09 +0100 Subject: [PATCH 0491/1387] Add tertiary authentication for metadata (TAM) Signed-off-by: Thomas Waldmann --- borg/archive.py | 4 +- borg/archiver.py | 84 +++++++++++++++++---- borg/crypto.pyx | 33 ++++++++- borg/helpers.py | 41 +++++++--- borg/key.py | 148 ++++++++++++++++++++++++++++++++++--- borg/testsuite/archiver.py | 72 ++++++++++++++++-- borg/testsuite/crypto.py | 53 +++++++++++++ borg/testsuite/helpers.py | 14 +++- borg/testsuite/key.py | 122 +++++++++++++++++++++++++++++- docs/changes.rst | 69 ++++++++++++++++- docs/usage.rst | 3 + 11 files changed, 591 insertions(+), 52 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index 641fd08d..216abc57 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -231,7 +231,7 @@ class Archive: def _load_meta(self, id): data = self.key.decrypt(id, self.repository.get(id)) - metadata = msgpack.unpackb(data) + metadata = msgpack.unpackb(data, unicode_errors='surrogateescape') if metadata[b'version'] != 1: raise Exception('Unknown archive metadata version') return metadata @@ -325,7 +325,7 @@ Number of files: {0.stats.nfiles}'''.format( 'time': start.isoformat(), 'time_end': end.isoformat(), }) - data = msgpack.packb(metadata, unicode_errors='surrogateescape') + data = self.key.pack_and_authenticate_metadata(metadata, context=b'archive') self.id = self.key.id_hash(data) self.cache.add_chunk(self.id, data, self.stats) self.manifest.archives[name] = {'id': self.id, 'time': metadata['time']} diff --git a/borg/archiver.py b/borg/archiver.py index 2cdfe37f..037f3680 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -30,7 +30,7 @@ from .compress import Compressor from .upgrader import AtticRepositoryUpgrader, BorgRepositoryUpgrader from .repository import Repository from .cache import Cache -from .key import key_creator, RepoKey, PassphraseKey +from .key import key_creator, tam_required_file, tam_required, RepoKey, PassphraseKey from .keymanager import KeyManager from .archive import backup_io, BackupOSError, Archive, ArchiveChecker, CHUNKER_PARAMS, is_special from .remote import RepositoryServer, RemoteRepository, cache_if_remote @@ -51,7 +51,7 @@ def argument(args, str_or_bool): return str_or_bool -def with_repository(fake=False, create=False, lock=True, exclusive=False, manifest=True, cache=False): +def with_repository(fake=False, invert_fake=False, create=False, lock=True, exclusive=False, manifest=True, cache=False): """ Method decorator for subcommand-handling methods: do_XYZ(self, args, repository, …) @@ -68,7 +68,7 @@ def with_repository(fake=False, create=False, lock=True, exclusive=False, manife def wrapper(self, args, **kwargs): location = args.location # note: 'location' must be always present in args append_only = getattr(args, 'append_only', False) - if argument(args, fake): + if argument(args, fake) ^ invert_fake: return method(self, args, repository=None, **kwargs) elif location.proto == 'ssh': repository = RemoteRepository(location, create=create, exclusive=argument(args, exclusive), @@ -135,6 +135,8 @@ class Archiver: repository.commit() with Cache(repository, key, manifest, warn_if_unencrypted=False): pass + tam_file = tam_required_file(repository) + open(tam_file, 'w').close() return self.exit_code @with_repository(exclusive=True, manifest=False) @@ -161,6 +163,7 @@ class Archiver: def do_change_passphrase(self, args, repository, manifest, key): """Change repository key file passphrase""" key.change_passphrase() + logger.info('Key updated') return EXIT_SUCCESS @with_repository(lock=False, exclusive=False, manifest=False, cache=False) @@ -209,6 +212,7 @@ class Archiver: key_new.id_key = key_old.id_key key_new.chunk_seed = key_old.chunk_seed key_new.change_passphrase() # option to change key protection passphrase, save + logger.info('Key updated') return EXIT_SUCCESS @with_repository(fake='dry_run', exclusive=True) @@ -705,21 +709,43 @@ class Archiver: DASHES) return self.exit_code - def do_upgrade(self, args): + @with_repository(fake='tam', invert_fake=True, manifest=False, exclusive=True) + def do_upgrade(self, args, repository, manifest=None, key=None): """upgrade a repository from a previous version""" - # mainly for upgrades from Attic repositories, - # but also supports borg 0.xx -> 1.0 upgrade. + if args.tam: + manifest, key = Manifest.load(repository, force_tam_not_required=args.force) - repo = AtticRepositoryUpgrader(args.location.path, create=False) - try: - repo.upgrade(args.dry_run, inplace=args.inplace, progress=args.progress) - except NotImplementedError as e: - print("warning: %s" % e) - repo = BorgRepositoryUpgrader(args.location.path, create=False) - try: - repo.upgrade(args.dry_run, inplace=args.inplace, progress=args.progress) - except NotImplementedError as e: - print("warning: %s" % e) + if not manifest.tam_verified: + # The standard archive listing doesn't include the archive ID like in borg 1.1.x + print('Manifest contents:') + for archive_info in manifest.list_archive_infos(sort_by='ts'): + print(format_archive(archive_info), '[%s]' % bin_to_hex(archive_info.id)) + manifest.write() + repository.commit() + if not key.tam_required: + key.tam_required = True + key.change_passphrase(key._passphrase) + print('Updated key') + if hasattr(key, 'find_key'): + print('Key location:', key.find_key()) + if not tam_required(repository): + tam_file = tam_required_file(repository) + open(tam_file, 'w').close() + print('Updated security database') + else: + # mainly for upgrades from Attic repositories, + # but also supports borg 0.xx -> 1.0 upgrade. + + repo = AtticRepositoryUpgrader(args.location.path, create=False) + try: + repo.upgrade(args.dry_run, inplace=args.inplace, progress=args.progress) + except NotImplementedError as e: + print("warning: %s" % e) + repo = BorgRepositoryUpgrader(args.location.path, create=False) + try: + repo.upgrade(args.dry_run, inplace=args.inplace, progress=args.progress) + except NotImplementedError as e: + print("warning: %s" % e) return self.exit_code def do_debug_info(self, args): @@ -1613,6 +1639,28 @@ class Archiver: upgrade_epilog = textwrap.dedent(""" Upgrade an existing Borg repository. + + Borg 1.x.y upgrades + ------------------- + + Use ``borg upgrade --tam REPO`` to require manifest authentication + introduced with Borg 1.0.9 to address security issues. This means + that modifying the repository after doing this with a version prior + to 1.0.9 will raise a validation error, so only perform this upgrade + after updating all clients using the repository to 1.0.9 or newer. + + This upgrade should be done on each client for safety reasons. + + If a repository is accidentally modified with a pre-1.0.9 client after + this upgrade, use ``borg upgrade --tam --force REPO`` to remedy it. + + See + https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability + for details. + + Attic and Borg 0.xx to Borg 1.x + ------------------------------- + This currently supports converting an Attic repository to Borg and also helps with converting Borg 0.xx to 1.0. @@ -1665,6 +1713,10 @@ class Archiver: default=False, action='store_true', help="""rewrite repository in place, with no chance of going back to older versions of the repository.""") + subparser.add_argument('--force', dest='force', action='store_true', + help="""Force upgrade""") + subparser.add_argument('--tam', dest='tam', action='store_true', + help="""Enable manifest authentication (in key and cache) (Borg 1.0.9 and later)""") subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False), help='path to the repository to be upgraded') diff --git a/borg/crypto.pyx b/borg/crypto.pyx index be692742..f7beb343 100644 --- a/borg/crypto.pyx +++ b/borg/crypto.pyx @@ -2,9 +2,13 @@ This could be replaced by PyCrypto maybe? """ +import hashlib +import hmac +from math import ceil + from libc.stdlib cimport malloc, free -API_VERSION = 2 +API_VERSION = 3 cdef extern from "openssl/rand.h": int RAND_bytes(unsigned char *buf, int num) @@ -171,3 +175,30 @@ cdef class AES: return out[:ptl] finally: free(out) + + +def hkdf_hmac_sha512(ikm, salt, info, output_length): + """ + Compute HKDF-HMAC-SHA512 with input key material *ikm*, *salt* and *info* to produce *output_length* bytes. + + This is the "HMAC-based Extract-and-Expand Key Derivation Function (HKDF)" (RFC 5869) + instantiated with HMAC-SHA512. + + *output_length* must not be greater than 64 * 255 bytes. + """ + digest_length = 64 + assert output_length <= (255 * digest_length), 'output_length must be <= 255 * 64 bytes' + # Step 1. HKDF-Extract (ikm, salt) -> prk + if salt is None: + salt = bytes(64) + prk = hmac.HMAC(salt, ikm, hashlib.sha512).digest() + + # Step 2. HKDF-Expand (prk, info, output_length) -> output key + n = ceil(output_length / digest_length) + t_n = b'' + output = b'' + for i in range(n): + msg = t_n + info + (i + 1).to_bytes(1, 'little') + t_n = hmac.HMAC(prk, msg, hashlib.sha512).digest() + output += t_n + return output[:output_length] diff --git a/borg/helpers.py b/borg/helpers.py index a8106aee..df5a213a 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -86,7 +86,7 @@ def check_extension_modules(): raise ExtensionModuleError if chunker.API_VERSION != 2: raise ExtensionModuleError - if crypto.API_VERSION != 2: + if crypto.API_VERSION != 3: raise ExtensionModuleError if platform.API_VERSION != 3: raise ExtensionModuleError @@ -103,10 +103,11 @@ class Manifest: self.key = key self.repository = repository self.item_keys = frozenset(item_keys) if item_keys is not None else ITEM_KEYS + self.tam_verified = False @classmethod - def load(cls, repository, key=None): - from .key import key_factory + def load(cls, repository, key=None, force_tam_not_required=False): + from .key import key_factory, tam_required_file, tam_required from .repository import Repository from .archive import ITEM_KEYS try: @@ -117,8 +118,8 @@ class Manifest: key = key_factory(repository, cdata) manifest = cls(key, repository) data = key.decrypt(None, cdata) + m, manifest.tam_verified = key.unpack_and_verify_manifest(data, force_tam_not_required=force_tam_not_required) manifest.id = key.id_hash(data) - m = msgpack.unpackb(data) if not m.get(b'version') == 1: raise ValueError('Invalid manifest version') manifest.archives = dict((k.decode('utf-8'), v) for k, v in m[b'archives'].items()) @@ -128,19 +129,27 @@ class Manifest: manifest.config = m[b'config'] # valid item keys are whatever is known in the repo or every key we know manifest.item_keys = frozenset(m.get(b'item_keys', [])) | ITEM_KEYS + if manifest.config.get(b'tam_required', False) and manifest.tam_verified and not tam_required(repository): + logger.debug('Manifest is TAM verified and says TAM is required, updating security database...') + file = tam_required_file(repository) + open(file, 'w').close() return manifest, key def write(self): + if self.key.tam_required: + self.config[b'tam_required'] = True self.timestamp = datetime.utcnow().isoformat() - data = msgpack.packb(StableDict({ + m = { 'version': 1, - 'archives': self.archives, + 'archives': StableDict((name, StableDict(archive)) for name, archive in self.archives.items()), 'timestamp': self.timestamp, - 'config': self.config, - 'item_keys': tuple(self.item_keys), - })) + 'config': StableDict(self.config), + 'item_keys': tuple(sorted(self.item_keys)), + } + self.tam_verified = True + data = self.key.pack_and_authenticate_metadata(m) self.id = self.key.id_hash(data) - self.repository.put(self.MANIFEST_ID, self.key.encrypt(data)) + self.repository.put(self.MANIFEST_ID, self.key.encrypt(data, none_compression=True)) def list_archive_infos(self, sort_by=None, reverse=False): # inexpensive Archive.list_archives replacement if we just need .name, .id, .ts @@ -249,6 +258,18 @@ def get_keys_dir(): return keys_dir +def get_security_dir(repository_id=None): + """Determine where to store local security information.""" + xdg_config = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')) + security_dir = os.environ.get('BORG_SECURITY_DIR', os.path.join(xdg_config, 'borg', 'security')) + if repository_id: + security_dir = os.path.join(security_dir, repository_id) + if not os.path.exists(security_dir): + os.makedirs(security_dir) + os.chmod(security_dir, stat.S_IRWXU) + return security_dir + + def get_cache_dir(): """Determine where to repository keys and cache""" xdg_cache = os.environ.get('XDG_CACHE_HOME', os.path.join(os.path.expanduser('~'), '.cache')) diff --git a/borg/key.py b/borg/key.py index e4fcd03d..318f8d0e 100644 --- a/borg/key.py +++ b/borg/key.py @@ -5,15 +5,17 @@ import os import sys import textwrap from hmac import HMAC, compare_digest -from hashlib import sha256, pbkdf2_hmac +from hashlib import sha256, sha512, pbkdf2_hmac -from .helpers import IntegrityError, get_keys_dir, Error, yes, bin_to_hex +import msgpack + +from .helpers import StableDict, IntegrityError, get_keys_dir, get_security_dir, Error, yes, bin_to_hex from .logger import create_logger logger = create_logger() from .crypto import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks -from .compress import Compressor -import msgpack +from .crypto import hkdf_hmac_sha512 +from .compress import Compressor, CNONE PREFIX = b'\0' * 8 @@ -30,6 +32,10 @@ class UnsupportedPayloadError(Error): """Unsupported payload type {}. A newer version is required to access this repository.""" +class UnsupportedManifestError(Error): + """Unsupported manifest envelope. A newer version is required to access this repository.""" + + class KeyfileNotFoundError(Error): """No key file for repository {} found in {}.""" @@ -38,6 +44,32 @@ class RepoKeyNotFoundError(Error): """No key entry found in the config of repository {}.""" +class TAMRequiredError(IntegrityError): + __doc__ = textwrap.dedent(""" + Manifest is unauthenticated, but authentication is required for this repository. + + This either means that you are under attack, or that you modified this repository + with a Borg version older than 1.0.9 after TAM authentication was enabled. + + In the latter case, use "borg upgrade --tam --force '{}'" to re-authenticate the manifest. + """).strip() + traceback = False + + +class TAMInvalid(IntegrityError): + __doc__ = IntegrityError.__doc__ + traceback = False + + def __init__(self): + # Error message becomes: "Data integrity error: Manifest authentication did not verify" + super().__init__('Manifest authentication did not verify') + + +class TAMUnsupportedSuiteError(IntegrityError): + """Could not verify manifest: Unsupported suite {!r}; a newer version is needed.""" + traceback = False + + def key_creator(repository, args): if args.encryption == 'keyfile': return KeyfileKey.create(repository, args) @@ -63,6 +95,16 @@ def key_factory(repository, manifest_data): raise UnsupportedPayloadError(key_type) +def tam_required_file(repository): + security_dir = get_security_dir(bin_to_hex(repository.id)) + return os.path.join(security_dir, 'tam_required') + + +def tam_required(repository): + file = tam_required_file(repository) + return os.path.isfile(file) + + class KeyBase: TYPE = None # override in subclasses @@ -71,23 +113,90 @@ class KeyBase: self.repository = repository self.target = None # key location file path / repo obj self.compressor = Compressor('none') + self.tam_required = True def id_hash(self, data): """Return HMAC hash using the "id" HMAC key """ - def encrypt(self, data): + def encrypt(self, data, none_compression=False): pass def decrypt(self, id, data): pass + def _tam_key(self, salt, context): + return hkdf_hmac_sha512( + ikm=self.id_key + self.enc_key + self.enc_hmac_key, + salt=salt, + info=b'borg-metadata-authentication-' + context, + output_length=64 + ) + + def pack_and_authenticate_metadata(self, metadata_dict, context=b'manifest'): + metadata_dict = StableDict(metadata_dict) + tam = metadata_dict['tam'] = StableDict({ + 'type': 'HKDF_HMAC_SHA512', + 'hmac': bytes(64), + 'salt': os.urandom(64), + }) + packed = msgpack.packb(metadata_dict, unicode_errors='surrogateescape') + tam_key = self._tam_key(tam['salt'], context) + tam['hmac'] = HMAC(tam_key, packed, sha512).digest() + return msgpack.packb(metadata_dict, unicode_errors='surrogateescape') + + def unpack_and_verify_manifest(self, data, force_tam_not_required=False): + """Unpack msgpacked *data* and return (object, did_verify).""" + if data.startswith(b'\xc1' * 4): + # This is a manifest from the future, we can't read it. + raise UnsupportedManifestError() + tam_required = self.tam_required + if force_tam_not_required and tam_required: + logger.warning('Manifest authentication DISABLED.') + tam_required = False + data = bytearray(data) + # Since we don't trust these bytes we use the slower Python unpacker, + # which is assumed to have a lower probability of security issues. + unpacked = msgpack.fallback.unpackb(data, object_hook=StableDict, unicode_errors='surrogateescape') + if b'tam' not in unpacked: + if tam_required: + raise TAMRequiredError(self.repository._location.canonical_path()) + else: + logger.debug('TAM not found and not required') + return unpacked, False + tam = unpacked.pop(b'tam', None) + if not isinstance(tam, dict): + raise TAMInvalid() + tam_type = tam.get(b'type', b'').decode('ascii', 'replace') + if tam_type != 'HKDF_HMAC_SHA512': + if tam_required: + raise TAMUnsupportedSuiteError(repr(tam_type)) + else: + logger.debug('Ignoring TAM made with unsupported suite, since TAM is not required: %r', tam_type) + return unpacked, False + tam_hmac = tam.get(b'hmac') + tam_salt = tam.get(b'salt') + if not isinstance(tam_salt, bytes) or not isinstance(tam_hmac, bytes): + raise TAMInvalid() + offset = data.index(tam_hmac) + data[offset:offset + 64] = bytes(64) + tam_key = self._tam_key(tam_salt, context=b'manifest') + calculated_hmac = HMAC(tam_key, data, sha512).digest() + if not compare_digest(calculated_hmac, tam_hmac): + raise TAMInvalid() + logger.debug('TAM-verified manifest') + return unpacked, True + class PlaintextKey(KeyBase): TYPE = 0x02 chunk_seed = 0 + def __init__(self, repository): + super().__init__(repository) + self.tam_required = False + @classmethod def create(cls, repository, args): logger.info('Encryption NOT enabled.\nUse the "--encryption=repokey|keyfile" to enable encryption.') @@ -100,8 +209,12 @@ class PlaintextKey(KeyBase): def id_hash(self, data): return sha256(data).digest() - def encrypt(self, data): - return b''.join([self.TYPE_STR, self.compressor.compress(data)]) + def encrypt(self, data, none_compression=False): + if none_compression: + compressed = CNONE().compress(data) + else: + compressed = self.compressor.compress(data) + return b''.join([self.TYPE_STR, compressed]) def decrypt(self, id, data): if data[0] != self.TYPE: @@ -112,6 +225,9 @@ class PlaintextKey(KeyBase): raise IntegrityError('Chunk %s: id verification failed' % bin_to_hex(id)) return data + def _tam_key(self, salt, context): + return salt + context + class AESKeyBase(KeyBase): """Common base class shared by KeyfileKey and PassphraseKey @@ -133,8 +249,11 @@ class AESKeyBase(KeyBase): """ return HMAC(self.id_key, data, sha256).digest() - def encrypt(self, data): - data = self.compressor.compress(data) + def encrypt(self, data, none_compression=False): + if none_compression: + data = CNONE().compress(data) + else: + data = self.compressor.compress(data) self.enc_cipher.reset() data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(data))) hmac = HMAC(self.enc_hmac_key, data, sha256).digest() @@ -269,6 +388,7 @@ class PassphraseKey(AESKeyBase): key.decrypt(None, manifest_data) num_blocks = num_aes_blocks(len(manifest_data) - 41) key.init_ciphers(PREFIX + long_to_bytes(key.extract_nonce(manifest_data) + num_blocks)) + key._passphrase = passphrase return key except IntegrityError: passphrase = Passphrase.getpass(prompt) @@ -284,6 +404,7 @@ class PassphraseKey(AESKeyBase): def init(self, repository, passphrase): self.init_from_random_data(passphrase.kdf(repository.id, self.iterations, 100)) self.init_ciphers() + self.tam_required = False class KeyfileKeyBase(AESKeyBase): @@ -307,6 +428,7 @@ class KeyfileKeyBase(AESKeyBase): raise PassphraseWrong num_blocks = num_aes_blocks(len(manifest_data) - 41) key.init_ciphers(PREFIX + long_to_bytes(key.extract_nonce(manifest_data) + num_blocks)) + key._passphrase = passphrase return key def find_key(self): @@ -327,6 +449,7 @@ class KeyfileKeyBase(AESKeyBase): self.enc_hmac_key = key[b'enc_hmac_key'] self.id_key = key[b'id_key'] self.chunk_seed = key[b'chunk_seed'] + self.tam_required = key.get(b'tam_required', tam_required(self.repository)) return True return False @@ -363,15 +486,16 @@ class KeyfileKeyBase(AESKeyBase): 'enc_hmac_key': self.enc_hmac_key, 'id_key': self.id_key, 'chunk_seed': self.chunk_seed, + 'tam_required': self.tam_required, } data = self.encrypt_key_file(msgpack.packb(key), passphrase) key_data = '\n'.join(textwrap.wrap(b2a_base64(data).decode('ascii'))) return key_data - def change_passphrase(self): - passphrase = Passphrase.new(allow_empty=True) + def change_passphrase(self, passphrase=None): + if passphrase is None: + passphrase = Passphrase.new(allow_empty=True) self.save(self.target, passphrase) - logger.info('Key updated') @classmethod def create(cls, repository, args): diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 815c8943..2daa9219 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -2,6 +2,8 @@ from binascii import unhexlify, b2a_base64 from configparser import ConfigParser import errno import os +from datetime import datetime +from datetime import timedelta from io import StringIO import random import stat @@ -14,6 +16,7 @@ import unittest from unittest.mock import patch from hashlib import sha256 +import msgpack import pytest from .. import xattr @@ -21,13 +24,15 @@ from ..archive import Archive, ChunkBuffer, CHUNK_MAX_EXP, flags_noatime, flags_ from ..archiver import Archiver from ..cache import Cache from ..crypto import bytes_to_long, num_aes_blocks -from ..helpers import Manifest, PatternMatcher, parse_pattern, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, bin_to_hex -from ..key import RepoKey, KeyfileKey, Passphrase +from ..helpers import Manifest, PatternMatcher, parse_pattern, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, bin_to_hex, \ + get_security_dir +from ..key import RepoKey, KeyfileKey, Passphrase, TAMRequiredError from ..keymanager import RepoIdMismatch, NotABorgKeyFile from ..remote import RemoteRepository, PathNotAllowed from ..repository import Repository from . import BaseTestCase, changedir, environment_variable, no_selinux from .platform import fakeroot_detected +from . import key try: import llfuse @@ -1143,8 +1148,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): def verify_uniqueness(): with Repository(self.repository_path) as repository: - for key, _ in repository.open_index(repository.get_transaction_id()).iteritems(): - data = repository.get(key) + for id, _ in repository.open_index(repository.get_transaction_id()).iteritems(): + data = repository.get(id) hash = sha256(data).digest() if hash not in seen: seen.add(hash) @@ -1253,7 +1258,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): repo_key = RepoKey(repository) repo_key.load(None, Passphrase.env_passphrase()) - backup_key = KeyfileKey(None) + backup_key = KeyfileKey(key.KeyTestCase.MockRepository()) backup_key.load(export_file, Passphrase.env_passphrase()) assert repo_key.enc_key == backup_key.enc_key @@ -1501,6 +1506,63 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.cmd('check', self.repository_location, exit_code=0) +class ManifestAuthenticationTest(ArchiverTestCaseBase): + def test_fresh_init_tam_required(self): + self.cmd('init', self.repository_location) + repository = Repository(self.repository_path, exclusive=True) + with repository: + manifest, key = Manifest.load(repository) + repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({ + 'version': 1, + 'archives': {}, + 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), + }))) + repository.commit() + + with pytest.raises(TAMRequiredError): + self.cmd('list', self.repository_location) + + def test_not_required(self): + self.cmd('init', self.repository_location) + self.create_src_archive('archive1234') + repository = Repository(self.repository_path, exclusive=True) + with repository: + shutil.rmtree(get_security_dir(bin_to_hex(repository.id))) + _, key = Manifest.load(repository) + key.tam_required = False + key.change_passphrase(key._passphrase) + + manifest = msgpack.unpackb(key.decrypt(None, repository.get(Manifest.MANIFEST_ID))) + del manifest[b'tam'] + repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb(manifest))) + repository.commit() + output = self.cmd('list', '--debug', self.repository_location) + assert 'archive1234' in output + assert 'TAM not found and not required' in output + # Run upgrade + self.cmd('upgrade', '--tam', self.repository_location) + # Manifest must be authenticated now + output = self.cmd('list', '--debug', self.repository_location) + assert 'archive1234' in output + assert 'TAM-verified manifest' in output + # Try to spoof / modify pre-1.0.9 + with repository: + _, key = Manifest.load(repository) + repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({ + 'version': 1, + 'archives': {}, + 'config': {}, + 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), + }))) + repository.commit() + # Fails + with pytest.raises(TAMRequiredError): + self.cmd('list', self.repository_location) + # Force upgrade + self.cmd('upgrade', '--tam', '--force', self.repository_location) + self.cmd('list', self.repository_location) + + @pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteArchiverTestCase(ArchiverTestCase): prefix = '__testsuite__:' diff --git a/borg/testsuite/crypto.py b/borg/testsuite/crypto.py index c6810194..e80a38b3 100644 --- a/borg/testsuite/crypto.py +++ b/borg/testsuite/crypto.py @@ -2,6 +2,7 @@ from binascii import hexlify from ..crypto import AES, bytes_to_long, bytes_to_int, long_to_bytes from ..crypto import increment_iv, bytes16_to_int, int_to_bytes16 +from ..crypto import hkdf_hmac_sha512 from . import BaseTestCase @@ -50,3 +51,55 @@ class CryptoTestCase(BaseTestCase): pdata = aes.decrypt(cdata) self.assert_equal(data, pdata) self.assert_equal(bytes_to_long(aes.iv, 8), 2) + + # These test vectors come from https://www.kullo.net/blog/hkdf-sha-512-test-vectors/ + # who claims to have verified these against independent Python and C++ implementations. + + def test_hkdf_hmac_sha512(self): + ikm = b'\x0b' * 22 + salt = bytes.fromhex('000102030405060708090a0b0c') + info = bytes.fromhex('f0f1f2f3f4f5f6f7f8f9') + l = 42 + + okm = hkdf_hmac_sha512(ikm, salt, info, l) + assert okm == bytes.fromhex('832390086cda71fb47625bb5ceb168e4c8e26a1a16ed34d9fc7fe92c1481579338da362cb8d9f925d7cb') + + def test_hkdf_hmac_sha512_2(self): + ikm = bytes.fromhex('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252627' + '28292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f') + salt = bytes.fromhex('606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868' + '788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf') + info = bytes.fromhex('b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7' + 'd8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff') + l = 82 + + okm = hkdf_hmac_sha512(ikm, salt, info, l) + assert okm == bytes.fromhex('ce6c97192805b346e6161e821ed165673b84f400a2b514b2fe23d84cd189ddf1b695b48cbd1c838844' + '1137b3ce28f16aa64ba33ba466b24df6cfcb021ecff235f6a2056ce3af1de44d572097a8505d9e7a93') + + def test_hkdf_hmac_sha512_3(self): + ikm = bytes.fromhex('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b') + salt = None + info = b'' + l = 42 + + okm = hkdf_hmac_sha512(ikm, salt, info, l) + assert okm == bytes.fromhex('f5fa02b18298a72a8c23898a8703472c6eb179dc204c03425c970e3b164bf90fff22d04836d0e2343bac') + + def test_hkdf_hmac_sha512_4(self): + ikm = bytes.fromhex('0b0b0b0b0b0b0b0b0b0b0b') + salt = bytes.fromhex('000102030405060708090a0b0c') + info = bytes.fromhex('f0f1f2f3f4f5f6f7f8f9') + l = 42 + + okm = hkdf_hmac_sha512(ikm, salt, info, l) + assert okm == bytes.fromhex('7413e8997e020610fbf6823f2ce14bff01875db1ca55f68cfcf3954dc8aff53559bd5e3028b080f7c068') + + def test_hkdf_hmac_sha512_5(self): + ikm = bytes.fromhex('0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c') + salt = None + info = b'' + l = 42 + + okm = hkdf_hmac_sha512(ikm, salt, info, l) + assert okm == bytes.fromhex('1407d46013d98bc6decefcfee55f0f90b0c7f63d68eb1a80eaf07e953cfc0a3a5240a155d6e4daa965bb') diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py index c6dadbec..47d49f99 100644 --- a/borg/testsuite/helpers.py +++ b/borg/testsuite/helpers.py @@ -11,7 +11,7 @@ import msgpack.fallback import time from ..helpers import Location, format_file_size, format_timedelta, format_line, PlaceholderError, make_path_safe, \ - prune_within, prune_split, get_cache_dir, get_keys_dir, Statistics, is_slow_msgpack, \ + prune_within, prune_split, get_cache_dir, get_keys_dir, get_security_dir, Statistics, is_slow_msgpack, \ yes, TRUISH, FALSISH, DEFAULTISH, \ StableDict, int_to_bigint, bigint_to_int, parse_timestamp, CompressionSpec, ChunkerParams, \ ProgressIndicatorPercent, ProgressIndicatorEndless, load_excludes, parse_pattern, \ @@ -654,6 +654,18 @@ def test_get_keys_dir(monkeypatch): assert get_keys_dir() == '/var/tmp' +def test_get_security_dir(monkeypatch): + """test that get_security_dir respects environment""" + monkeypatch.delenv('BORG_SECURITY_DIR', raising=False) + monkeypatch.delenv('XDG_CONFIG_HOME', raising=False) + assert get_security_dir() == os.path.join(os.path.expanduser('~'), '.config', 'borg', 'security') + assert get_security_dir(repository_id='1234') == os.path.join(os.path.expanduser('~'), '.config', 'borg', 'security', '1234') + monkeypatch.setenv('XDG_CONFIG_HOME', '/var/tmp/.config') + assert get_security_dir() == os.path.join('/var/tmp/.config', 'borg', 'security') + monkeypatch.setenv('BORG_SECURITY_DIR', '/var/tmp') + assert get_security_dir() == '/var/tmp' + + @pytest.fixture() def stats(): stats = Statistics() diff --git a/borg/testsuite/key.py b/borg/testsuite/key.py index 4c57d1f0..2bb9d86f 100644 --- a/borg/testsuite/key.py +++ b/borg/testsuite/key.py @@ -4,9 +4,14 @@ import shutil import tempfile from binascii import hexlify, unhexlify +import msgpack + +import pytest + from ..crypto import bytes_to_long, num_aes_blocks from ..key import PlaintextKey, PassphraseKey, KeyfileKey -from ..helpers import Location +from ..key import UnsupportedManifestError, TAMRequiredError, TAMUnsupportedSuiteError, TAMInvalid +from ..helpers import Location, StableDict from . import BaseTestCase @@ -42,6 +47,9 @@ class KeyTestCase(BaseTestCase): class _Location: orig = '/some/place' + def canonical_path(self): + return self.orig + _location = _Location() id = bytes(32) @@ -101,3 +109,115 @@ class KeyTestCase(BaseTestCase): data = b'foo' self.assert_equal(hexlify(key.id_hash(data)), b'818217cf07d37efad3860766dcdf1d21e401650fed2d76ed1d797d3aae925990') self.assert_equal(data, key2.decrypt(key2.id_hash(data), key.encrypt(data))) + + +class TestTAM: + @pytest.fixture + def key(self, monkeypatch): + monkeypatch.setenv('BORG_PASSPHRASE', 'test') + return KeyfileKey.create(KeyTestCase.MockRepository(), KeyTestCase.MockArgs()) + + def test_unpack_future(self, key): + blob = b'\xc1\xc1\xc1\xc1foobar' + with pytest.raises(UnsupportedManifestError): + key.unpack_and_verify_manifest(blob) + + blob = b'\xc1\xc1\xc1' + with pytest.raises(msgpack.UnpackException): + key.unpack_and_verify_manifest(blob) + + def test_missing_when_required(self, key): + blob = msgpack.packb({}) + with pytest.raises(TAMRequiredError): + key.unpack_and_verify_manifest(blob) + + def test_missing(self, key): + blob = msgpack.packb({}) + key.tam_required = False + unpacked, verified = key.unpack_and_verify_manifest(blob) + assert unpacked == {} + assert not verified + + def test_unknown_type_when_required(self, key): + blob = msgpack.packb({ + 'tam': { + 'type': 'HMAC_VOLLBIT', + }, + }) + with pytest.raises(TAMUnsupportedSuiteError): + key.unpack_and_verify_manifest(blob) + + def test_unknown_type(self, key): + blob = msgpack.packb({ + 'tam': { + 'type': 'HMAC_VOLLBIT', + }, + }) + key.tam_required = False + unpacked, verified = key.unpack_and_verify_manifest(blob) + assert unpacked == {} + assert not verified + + @pytest.mark.parametrize('tam, exc', ( + ({}, TAMUnsupportedSuiteError), + ({'type': b'\xff'}, TAMUnsupportedSuiteError), + (None, TAMInvalid), + (1234, TAMInvalid), + )) + def test_invalid(self, key, tam, exc): + blob = msgpack.packb({ + 'tam': tam, + }) + with pytest.raises(exc): + key.unpack_and_verify_manifest(blob) + + @pytest.mark.parametrize('hmac, salt', ( + ({}, bytes(64)), + (bytes(64), {}), + (None, bytes(64)), + (bytes(64), None), + )) + def test_wrong_types(self, key, hmac, salt): + data = { + 'tam': { + 'type': 'HKDF_HMAC_SHA512', + 'hmac': hmac, + 'salt': salt + }, + } + tam = data['tam'] + if hmac is None: + del tam['hmac'] + if salt is None: + del tam['salt'] + blob = msgpack.packb(data) + with pytest.raises(TAMInvalid): + key.unpack_and_verify_manifest(blob) + + def test_round_trip(self, key): + data = {'foo': 'bar'} + blob = key.pack_and_authenticate_metadata(data) + assert blob.startswith(b'\x82') + + unpacked = msgpack.unpackb(blob) + assert unpacked[b'tam'][b'type'] == b'HKDF_HMAC_SHA512' + + unpacked, verified = key.unpack_and_verify_manifest(blob) + assert verified + assert unpacked[b'foo'] == b'bar' + assert b'tam' not in unpacked + + @pytest.mark.parametrize('which', (b'hmac', b'salt')) + def test_tampered(self, key, which): + data = {'foo': 'bar'} + blob = key.pack_and_authenticate_metadata(data) + assert blob.startswith(b'\x82') + + unpacked = msgpack.unpackb(blob, object_hook=StableDict) + assert len(unpacked[b'tam'][which]) == 64 + unpacked[b'tam'][which] = unpacked[b'tam'][which][0:32] + bytes(32) + assert len(unpacked[b'tam'][which]) == 64 + blob = msgpack.packb(unpacked) + + with pytest.raises(TAMInvalid): + key.unpack_and_verify_manifest(blob) diff --git a/docs/changes.rst b/docs/changes.rst index 71677bcb..c24ea18e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,7 +1,62 @@ Important notes =============== -This section is used for infos about e.g. security and corruption issues. +This section is used for infos about security and corruption issues. + +.. _tam_vuln: + +Pre-1.0.9 manifest spoofing vulnerability +----------------------------------------- + +A flaw in the cryptographic authentication scheme in Borg allowed an attacker +to spoof the manifest. The attack requires an attacker to be able to + +1. insert files (with no additional headers) into backups +2. gain write access to the repository + +This vulnerability does not disclose plaintext to the attacker, nor does it +affect the authenticity of existing archives. + +The vulnerability allows an attacker to create a spoofed manifest (the list of archives). +Creating plausible fake archives may be feasible for small archives, but is unlikely +for large archives. + +The fix adds a separate authentication tag to the manifest. For compatibility +with prior versions this authentication tag is *not* required by default +for existing repositories. Repositories created with 1.0.9 and later require it. + +Steps you should take: + +1. Upgrade all clients to 1.0.9 or later. +2. Run ``borg upgrade --tam `` *on every client* for *each* repository. +3. This will list all archives, including archive IDs, for easy comparison with your logs. +4. Done. + +Prior versions can access and modify repositories with this measure enabled, however, +to 1.0.9 or later their modifications are indiscernible from an attack and will +raise an error until the below procedure is followed. We are aware that this can +be be annoying in some circumstances, but don't see a way to fix the vulnerability +otherwise. + +In case a version prior to 1.0.9 is used to modify a repository where above procedure +was completed, and now you get an error message from other clients: + +1. ``borg upgrade --tam --force `` once with *any* client suffices. + +This attack is mitigated by: + +- Noting/logging ``borg list``, ``borg info``, or ``borg create --stats``, which + contain the archive IDs. + +We are not aware of others having discovered, disclosed or exploited this vulnerability. + +Vulnerability time line: + +* 2016-11-14: Vulnerability and fix discovered during review of cryptography by Marian Beermann (@enkore) +* 2016-11-20: First patch +* 2016-12-18: Released fixed versions: 1.0.9, 1.1.0b3 + +.. _attic013_check_corruption: Pre-1.0.9 potential data loss ----------------------------- @@ -71,8 +126,14 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= -Version 1.0.9 (not released yet) --------------------------------- +Version 1.0.9 (2016-12-18) +-------------------------- + +Security fixes: + +- A flaw in the cryptographic authentication scheme in Borg allowed an attacker + to spoof the manifest. See :ref:`tam_vuln` above for the steps you should + take. Bug fixes: @@ -96,7 +157,7 @@ Other changes: - markup fixes - tests: - - test_get_(cache|keys)_dir: clean env state, #1897 + - test_get\_(cache|keys)_dir: clean env state, #1897 - get back pytest's pretty assertion failures, #1938 - setup.py build_usage: diff --git a/docs/usage.rst b/docs/usage.rst index cb034394..ec1a790f 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -181,6 +181,9 @@ Some automatic "answerers" (if set, they automatically answer confirmation quest Directories: BORG_KEYS_DIR Default to '~/.config/borg/keys'. This directory contains keys for encrypted repositories. + BORG_SECURITY_DIR + Default to '~/.config/borg/security'. This directory is used by Borg to track various + pieces of security-related data. BORG_CACHE_DIR Default to '~/.cache/borg'. This directory contains the local cache and might need a lot of space for dealing with big repositories). From f2f50efc2873636dc7acfcd222e21fe40fe667a5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 15 Dec 2016 20:02:37 +0100 Subject: [PATCH 0492/1387] check: handle duplicate archive items neatly Signed-off-by: Thomas Waldmann --- borg/archive.py | 14 ++++++++++++-- borg/testsuite/archiver.py | 27 +++++++++++++++++++++++++++ docs/changes.rst | 3 +++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index 216abc57..baf21233 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -935,8 +935,18 @@ class ArchiveChecker: except (TypeError, ValueError, StopIteration): continue if valid_archive(archive): - logger.info('Found archive %s', archive[b'name'].decode('utf-8')) - manifest.archives[archive[b'name'].decode('utf-8')] = {b'id': chunk_id, b'time': archive[b'time']} + name = archive[b'name'].decode() + logger.info('Found archive %s', name) + if name in manifest.archives: + i = 1 + while True: + new_name = '%s.%d' % (name, i) + if new_name not in manifest.archives: + break + i += 1 + logger.warning('Duplicate archive name %s, storing as %s', name, new_name) + name = new_name + manifest.archives[name] = {b'id': chunk_id, b'time': archive[b'time']} logger.info('Manifest rebuild complete.') return manifest diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 2daa9219..5af3e393 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1468,6 +1468,33 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.assert_in('archive2', output) self.cmd('check', self.repository_location, exit_code=0) + def test_manifest_rebuild_duplicate_archive(self): + archive, repository = self.open_archive('archive1') + key = archive.key + with repository: + manifest = repository.get(Manifest.MANIFEST_ID) + corrupted_manifest = manifest + b'corrupted!' + repository.put(Manifest.MANIFEST_ID, corrupted_manifest) + + archive = msgpack.packb({ + 'cmdline': [], + 'items': [], + 'hostname': 'foo', + 'username': 'bar', + 'name': 'archive1', + 'time': '2016-12-15T18:49:51.849711', + 'version': 1, + }) + archive_id = key.id_hash(archive) + repository.put(archive_id, key.encrypt(archive)) + repository.commit() + self.cmd('check', self.repository_location, exit_code=1) + self.cmd('check', '--repair', self.repository_location, exit_code=0) + output = self.cmd('list', self.repository_location) + self.assert_in('archive1', output) + self.assert_in('archive1.1', output) + self.assert_in('archive2', output) + def test_extra_chunks(self): self.cmd('check', self.repository_location, exit_code=0) with Repository(self.repository_location, exclusive=True) as repository: diff --git a/docs/changes.rst b/docs/changes.rst index c24ea18e..b99d82ed 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -134,6 +134,9 @@ Security fixes: - A flaw in the cryptographic authentication scheme in Borg allowed an attacker to spoof the manifest. See :ref:`tam_vuln` above for the steps you should take. +- borg check: When rebuilding the manifest (which should only be needed very rarely) + duplicate archive names would be handled on a "first come first serve" basis, allowing + an attacker to apparently replace archives. Bug fixes: From 1c55930840a7958ff8e5dd300a92229dcb10dfed Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 17 Dec 2016 15:09:03 +0100 Subject: [PATCH 0493/1387] ran build_usage --- docs/usage/upgrade.rst.inc | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index 6c44edf7..c31be16b 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -8,7 +8,7 @@ borg upgrade usage: borg upgrade [-h] [--critical] [--error] [--warning] [--info] [--debug] [--lock-wait N] [--show-rc] [--no-files-cache] [--umask M] - [--remote-path PATH] [-p] [-n] [-i] + [--remote-path PATH] [-p] [-n] [-i] [--force] [--tam] [REPOSITORY] upgrade a repository from a previous version @@ -34,11 +34,36 @@ borg upgrade -n, --dry-run do not change repository -i, --inplace rewrite repository in place, with no chance of going back to older versions of the repository. + --force Force upgrade + --tam Enable manifest authentication (in key and cache) + (Borg 1.0.9 and later) Description ~~~~~~~~~~~ Upgrade an existing Borg repository. + +Borg 1.x.y upgrades +------------------- + +Use ``borg upgrade --tam REPO`` to require manifest authentication +introduced with Borg 1.0.9 to address security issues. This means +that modifying the repository after doing this with a version prior +to 1.0.9 will raise a validation error, so only perform this upgrade +after updating all clients using the repository to 1.0.9 or newer. + +This upgrade should be done on each client for safety reasons. + +If a repository is accidentally modified with a pre-1.0.9 client after +this upgrade, use ``borg upgrade --tam --force REPO`` to remedy it. + +See +https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability +for details. + +Attic and Borg 0.xx to Borg 1.x +------------------------------- + This currently supports converting an Attic repository to Borg and also helps with converting Borg 0.xx to 1.0. From ec4f42c9f85aed182e840f30f9bb01717c7ec592 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Dec 2016 21:45:19 +0100 Subject: [PATCH 0494/1387] init: explain manifest auth compatibility --- borg/archiver.py | 18 +++++++++++++++--- borg/key.py | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 037f3680..9206a4b9 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -127,7 +127,8 @@ class Archiver: @with_repository(create=True, exclusive=True, manifest=False) def do_init(self, args, repository): """Initialize an empty repository""" - logger.info('Initializing repository at "%s"' % args.location.canonical_path()) + path = args.location.canonical_path() + logger.info('Initializing repository at "%s"' % path) key = key_creator(repository, args) manifest = Manifest(key, repository) manifest.key = key @@ -135,8 +136,19 @@ class Archiver: repository.commit() with Cache(repository, key, manifest, warn_if_unencrypted=False): pass - tam_file = tam_required_file(repository) - open(tam_file, 'w').close() + if key.tam_required: + tam_file = tam_required_file(repository) + open(tam_file, 'w').close() + logger.warning( + '\n' + 'By default repositories initialized with this version will produce security\n' + 'errors if written to with an older version (up to and including Borg 1.0.8).\n' + '\n' + 'If you want to use these older versions, you can disable the check by runnning:\n' + 'borg upgrade --disable-tam \'%s\'\n' + '\n' + 'See https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability ' + 'for details about the security implications.', path) return self.exit_code @with_repository(exclusive=True, manifest=False) diff --git a/borg/key.py b/borg/key.py index 318f8d0e..3540ea58 100644 --- a/borg/key.py +++ b/borg/key.py @@ -46,7 +46,7 @@ class RepoKeyNotFoundError(Error): class TAMRequiredError(IntegrityError): __doc__ = textwrap.dedent(""" - Manifest is unauthenticated, but authentication is required for this repository. + Manifest is unauthenticated, but it is required for this repository. This either means that you are under attack, or that you modified this repository with a Borg version older than 1.0.9 after TAM authentication was enabled. From 4d6141a607c875d74460c4f235b6175037bf6989 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Dec 2016 23:28:01 +0100 Subject: [PATCH 0495/1387] upgrade: --disable-tam --- borg/archiver.py | 28 +++++++++++++++++++++++++--- borg/helpers.py | 15 +++++++++++---- borg/testsuite/archiver.py | 37 ++++++++++++++++++++++++++++--------- 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 9206a4b9..8a243f89 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -48,6 +48,8 @@ def argument(args, str_or_bool): """If bool is passed, return it. If str is passed, retrieve named attribute from args.""" if isinstance(str_or_bool, str): return getattr(args, str_or_bool) + if isinstance(str_or_bool, (list, tuple)): + return any(getattr(args, item) for item in str_or_bool) return str_or_bool @@ -721,29 +723,43 @@ class Archiver: DASHES) return self.exit_code - @with_repository(fake='tam', invert_fake=True, manifest=False, exclusive=True) + @with_repository(fake=('tam', 'disable_tam'), invert_fake=True, manifest=False, exclusive=True) def do_upgrade(self, args, repository, manifest=None, key=None): """upgrade a repository from a previous version""" if args.tam: manifest, key = Manifest.load(repository, force_tam_not_required=args.force) - if not manifest.tam_verified: + if not manifest.tam_verified or not manifest.config.get(b'tam_required', False): # The standard archive listing doesn't include the archive ID like in borg 1.1.x print('Manifest contents:') for archive_info in manifest.list_archive_infos(sort_by='ts'): print(format_archive(archive_info), '[%s]' % bin_to_hex(archive_info.id)) + manifest.config[b'tam_required'] = True manifest.write() repository.commit() if not key.tam_required: key.tam_required = True key.change_passphrase(key._passphrase) - print('Updated key') + print('Key updated') if hasattr(key, 'find_key'): print('Key location:', key.find_key()) if not tam_required(repository): tam_file = tam_required_file(repository) open(tam_file, 'w').close() print('Updated security database') + elif args.disable_tam: + manifest, key = Manifest.load(repository, force_tam_not_required=True) + if tam_required(repository): + os.unlink(tam_required_file(repository)) + if key.tam_required: + key.tam_required = False + key.change_passphrase(key._passphrase) + print('Key updated') + if hasattr(key, 'find_key'): + print('Key location:', key.find_key()) + manifest.config[b'tam_required'] = False + manifest.write() + repository.commit() else: # mainly for upgrades from Attic repositories, # but also supports borg 0.xx -> 1.0 upgrade. @@ -1666,6 +1682,10 @@ class Archiver: If a repository is accidentally modified with a pre-1.0.9 client after this upgrade, use ``borg upgrade --tam --force REPO`` to remedy it. + If you routinely do this you might not want to enable this upgrade + (which will leave you exposed to the security issue). You can + reverse the upgrade by issuing ``borg upgrade --disable-tam REPO``. + See https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability for details. @@ -1729,6 +1749,8 @@ class Archiver: help="""Force upgrade""") subparser.add_argument('--tam', dest='tam', action='store_true', help="""Enable manifest authentication (in key and cache) (Borg 1.0.9 and later)""") + subparser.add_argument('--disable-tam', dest='disable_tam', action='store_true', + help="""Disable manifest authentication (in key and cache)""") subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False), help='path to the repository to be upgraded') diff --git a/borg/helpers.py b/borg/helpers.py index df5a213a..b38ec945 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -129,10 +129,17 @@ class Manifest: manifest.config = m[b'config'] # valid item keys are whatever is known in the repo or every key we know manifest.item_keys = frozenset(m.get(b'item_keys', [])) | ITEM_KEYS - if manifest.config.get(b'tam_required', False) and manifest.tam_verified and not tam_required(repository): - logger.debug('Manifest is TAM verified and says TAM is required, updating security database...') - file = tam_required_file(repository) - open(file, 'w').close() + + if manifest.tam_verified: + manifest_required = manifest.config.get(b'tam_required', False) + security_required = tam_required(repository) + if manifest_required and not security_required: + logger.debug('Manifest is TAM verified and says TAM is required, updating security database...') + file = tam_required_file(repository) + open(file, 'w').close() + if not manifest_required and security_required: + logger.debug('Manifest is TAM verified and says TAM is *not* required, updating security database...') + os.unlink(tam_required_file(repository)) return manifest, key def write(self): diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 5af3e393..6968ec33 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1534,6 +1534,17 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): class ManifestAuthenticationTest(ArchiverTestCaseBase): + def spoof_manifest(self, repository): + with repository: + _, key = Manifest.load(repository) + repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({ + 'version': 1, + 'archives': {}, + 'config': {}, + 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), + }))) + repository.commit() + def test_fresh_init_tam_required(self): self.cmd('init', self.repository_location) repository = Repository(self.repository_path, exclusive=True) @@ -1573,15 +1584,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): assert 'archive1234' in output assert 'TAM-verified manifest' in output # Try to spoof / modify pre-1.0.9 - with repository: - _, key = Manifest.load(repository) - repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({ - 'version': 1, - 'archives': {}, - 'config': {}, - 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), - }))) - repository.commit() + self.spoof_manifest(repository) # Fails with pytest.raises(TAMRequiredError): self.cmd('list', self.repository_location) @@ -1589,6 +1592,22 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): self.cmd('upgrade', '--tam', '--force', self.repository_location) self.cmd('list', self.repository_location) + def test_disable(self): + self.cmd('init', self.repository_location) + self.create_src_archive('archive1234') + self.cmd('upgrade', '--disable-tam', self.repository_location) + repository = Repository(self.repository_path, exclusive=True) + self.spoof_manifest(repository) + assert not self.cmd('list', self.repository_location) + + def test_disable2(self): + self.cmd('init', self.repository_location) + self.create_src_archive('archive1234') + repository = Repository(self.repository_path, exclusive=True) + self.spoof_manifest(repository) + self.cmd('upgrade', '--disable-tam', self.repository_location) + assert not self.cmd('list', self.repository_location) + @pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteArchiverTestCase(ArchiverTestCase): From 580599cf32dbddf9eb85bd9da8166ab6ac66e099 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 19 Dec 2016 04:21:13 +0100 Subject: [PATCH 0496/1387] ran build_usage --- docs/usage/upgrade.rst.inc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index c31be16b..b4793c2c 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -9,6 +9,7 @@ borg upgrade usage: borg upgrade [-h] [--critical] [--error] [--warning] [--info] [--debug] [--lock-wait N] [--show-rc] [--no-files-cache] [--umask M] [--remote-path PATH] [-p] [-n] [-i] [--force] [--tam] + [--disable-tam] [REPOSITORY] upgrade a repository from a previous version @@ -37,6 +38,7 @@ borg upgrade --force Force upgrade --tam Enable manifest authentication (in key and cache) (Borg 1.0.9 and later) + --disable-tam Disable manifest authentication (in key and cache) Description ~~~~~~~~~~~ @@ -57,6 +59,10 @@ This upgrade should be done on each client for safety reasons. If a repository is accidentally modified with a pre-1.0.9 client after this upgrade, use ``borg upgrade --tam --force REPO`` to remedy it. +If you routinely do this you might not want to enable this upgrade +(which will leave you exposed to the security issue). You can +reverse the upgrade by issuing ``borg upgrade --disable-tam REPO``. + See https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability for details. From 3362ec319e6ebdb423e2e407f8a98bf024c1a8d3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 19 Dec 2016 16:06:54 +0100 Subject: [PATCH 0497/1387] quickstart: use prune with --list so people are better aware of what's happening, avoiding pitfalls with wrong or missing --prefix. --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 78966eb5..58ce6c96 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -126,7 +126,7 @@ certain number of old archives:: # archives of THIS machine. The '{hostname}-' prefix is very important to # limit prune's operation to this machine's archives and not apply to # other machine's archives also. - borg prune -v $REPOSITORY --prefix '{hostname}-' \ + borg prune -v --list $REPOSITORY --prefix '{hostname}-' \ --keep-daily=7 --keep-weekly=4 --keep-monthly=6 Pitfalls with shell variables and environment variables From c54a9121ae5ea207123f4ea7631e23234534dddb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 20 Dec 2016 00:49:24 +0100 Subject: [PATCH 0498/1387] CHANGES: fix 1.0.9 release date --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index b99d82ed..209893bb 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -126,7 +126,7 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= -Version 1.0.9 (2016-12-18) +Version 1.0.9 (2016-12-20) -------------------------- Security fixes: From 5e1cb9d89963325b44f270554fdc982ca87cce8a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Dec 2016 00:51:25 +0100 Subject: [PATCH 0499/1387] Add tertiary authentication for metadata (TAM) --- docs/changes.rst | 69 ++++++++++++++++- setup.py | 2 +- src/borg/archive.py | 4 +- src/borg/archiver.py | 84 +++++++++++++++++---- src/borg/crypto.pyx | 33 ++++++++- src/borg/helpers.py | 30 +++++--- src/borg/item.pyx | 3 +- src/borg/key.py | 132 ++++++++++++++++++++++++++++++--- src/borg/selftest.py | 2 +- src/borg/testsuite/archiver.py | 69 ++++++++++++++++- src/borg/testsuite/crypto.py | 54 +++++++++++++- src/borg/testsuite/helpers.py | 1 + src/borg/testsuite/key.py | 119 ++++++++++++++++++++++++++++- 13 files changed, 550 insertions(+), 52 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index e6ebcb45..928a9c2e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,7 +1,62 @@ Important notes =============== -This section is used for infos about e.g. security and corruption issues. +This section is used for infos about security and corruption issues. + +.. _tam_vuln: + +Pre-1.0.9 manifest spoofing vulnerability +----------------------------------------- + +A flaw in the cryptographic authentication scheme in Borg allowed an attacker +to spoof the manifest. The attack requires an attacker to be able to + +1. insert files (with no additional headers) into backups +2. gain write access to the repository + +This vulnerability does not disclose plaintext to the attacker, nor does it +affect the authenticity of existing archives. + +The vulnerability allows an attacker to create a spoofed manifest (the list of archives). +Creating plausible fake archives may be feasible for small archives, but is unlikely +for large archives. + +The fix adds a separate authentication tag to the manifest. For compatibility +with prior versions this authentication tag is *not* required by default +for existing repositories. Repositories created with 1.0.9 and later require it. + +Steps you should take: + +1. Upgrade all clients to 1.0.9 or later. +2. Run ``borg upgrade --tam `` *on every client* for *each* repository. +3. This will list all archives, including archive IDs, for easy comparison with your logs. +4. Done. + +Prior versions can access and modify repositories with this measure enabled, however, +to 1.0.9 or later their modifications are indiscernible from an attack and will +raise an error until the below procedure is followed. We are aware that this can +be be annoying in some circumstances, but don't see a way to fix the vulnerability +otherwise. + +In case a version prior to 1.0.9 is used to modify a repository where above procedure +was completed, and now you get an error message from other clients: + +1. ``borg upgrade --tam --force `` once with *any* client suffices. + +This attack is mitigated by: + +- Noting/logging ``borg list``, ``borg info``, or ``borg create --stats``, which + contain the archive IDs. + +We are not aware of others having discovered, disclosed or exploited this vulnerability. + +Vulnerability time line: + +* 2016-11-14: Vulnerability and fix discovered during review of cryptography by Marian Beermann (@enkore) +* 2016-11-20: First patch +* 2016-12-18: Released fixed versions: 1.0.9, 1.1.0b3 + +.. _attic013_check_corruption: Pre-1.0.9 potential data loss ----------------------------- @@ -71,8 +126,14 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= -Version 1.0.9 (not released yet) --------------------------------- +Version 1.0.9 (2016-12-18) +-------------------------- + +Security fixes: + +- A flaw in the cryptographic authentication scheme in Borg allowed an attacker + to spoof the manifest. See :ref:`tam_vuln` above for the steps you should + take. Bug fixes: @@ -96,7 +157,7 @@ Other changes: - markup fixes - tests: - - test_get_(cache|keys)_dir: clean env state, #1897 + - test_get\_(cache|keys)_dir: clean env state, #1897 - get back pytest's pretty assertion failures, #1938 - setup.py build_usage: diff --git a/setup.py b/setup.py index 6ef6d543..c6e27f4d 100644 --- a/setup.py +++ b/setup.py @@ -109,7 +109,7 @@ except ImportError: platform_darwin_source = platform_darwin_source.replace('.pyx', '.c') from distutils.command.build_ext import build_ext if not on_rtd and not all(os.path.exists(path) for path in [ - compress_source, crypto_source, chunker_source, hashindex_source, + compress_source, crypto_source, chunker_source, hashindex_source, item_source, platform_posix_source, platform_linux_source, platform_freebsd_source, platform_darwin_source]): raise ImportError('The GIT version of Borg needs Cython. Install Cython or use a released version.') diff --git a/src/borg/archive.py b/src/borg/archive.py index 2c7dfa96..3793e7e7 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -307,7 +307,7 @@ class Archive: def _load_meta(self, id): _, data = self.key.decrypt(id, self.repository.get(id)) - metadata = ArchiveItem(internal_dict=msgpack.unpackb(data)) + metadata = ArchiveItem(internal_dict=msgpack.unpackb(data, unicode_errors='surrogateescape')) if metadata.version != 1: raise Exception('Unknown archive metadata version') return metadata @@ -409,7 +409,7 @@ Number of files: {0.stats.nfiles}'''.format( } metadata.update(additional_metadata or {}) metadata = ArchiveItem(metadata) - data = msgpack.packb(metadata.as_dict(), unicode_errors='surrogateescape') + data = self.key.pack_and_authenticate_metadata(metadata.as_dict(), context=b'archive') self.id = self.key.id_hash(data) self.cache.add_chunk(self.id, Chunk(data), self.stats) self.manifest.archives[name] = (self.id, metadata.time) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f6017e13..cfe497b3 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -45,7 +45,7 @@ from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm from .helpers import ErrorIgnoringTextIOWrapper from .helpers import ProgressIndicatorPercent from .item import Item -from .key import key_creator, RepoKey, PassphraseKey +from .key import key_creator, tam_required_file, tam_required, RepoKey, PassphraseKey from .keymanager import KeyManager from .platform import get_flags, umount from .remote import RepositoryServer, RemoteRepository, cache_if_remote @@ -64,7 +64,7 @@ def argument(args, str_or_bool): return str_or_bool -def with_repository(fake=False, create=False, lock=True, exclusive=False, manifest=True, cache=False): +def with_repository(fake=False, invert_fake=False, create=False, lock=True, exclusive=False, manifest=True, cache=False): """ Method decorator for subcommand-handling methods: do_XYZ(self, args, repository, …) @@ -81,7 +81,7 @@ def with_repository(fake=False, create=False, lock=True, exclusive=False, manife def wrapper(self, args, **kwargs): location = args.location # note: 'location' must be always present in args append_only = getattr(args, 'append_only', False) - if argument(args, fake): + if argument(args, fake) ^ invert_fake: return method(self, args, repository=None, **kwargs) elif location.proto == 'ssh': repository = RemoteRepository(location, create=create, exclusive=argument(args, exclusive), @@ -194,6 +194,8 @@ class Archiver: repository.commit() with Cache(repository, key, manifest, warn_if_unencrypted=False): pass + tam_file = tam_required_file(repository) + open(tam_file, 'w').close() return self.exit_code @with_repository(exclusive=True, manifest=False) @@ -224,6 +226,7 @@ class Archiver: def do_change_passphrase(self, args, repository, manifest, key): """Change repository key file passphrase""" key.change_passphrase() + logger.info('Key updated') return EXIT_SUCCESS @with_repository(lock=False, exclusive=False, manifest=False, cache=False) @@ -272,6 +275,7 @@ class Archiver: key_new.id_key = key_old.id_key key_new.chunk_seed = key_old.chunk_seed key_new.change_passphrase() # option to change key protection passphrase, save + logger.info('Key updated') return EXIT_SUCCESS @with_repository(fake='dry_run', exclusive=True) @@ -1046,21 +1050,43 @@ class Archiver: DASHES, logger=logging.getLogger('borg.output.stats')) return self.exit_code - def do_upgrade(self, args): + @with_repository(fake='tam', invert_fake=True, manifest=False, exclusive=True) + def do_upgrade(self, args, repository, manifest=None, key=None): """upgrade a repository from a previous version""" - # mainly for upgrades from Attic repositories, - # but also supports borg 0.xx -> 1.0 upgrade. + if args.tam: + manifest, key = Manifest.load(repository, force_tam_not_required=args.force) - repo = AtticRepositoryUpgrader(args.location.path, create=False) - try: - repo.upgrade(args.dry_run, inplace=args.inplace, progress=args.progress) - except NotImplementedError as e: - print("warning: %s" % e) - repo = BorgRepositoryUpgrader(args.location.path, create=False) - try: - repo.upgrade(args.dry_run, inplace=args.inplace, progress=args.progress) - except NotImplementedError as e: - print("warning: %s" % e) + if not manifest.tam_verified: + # The standard archive listing doesn't include the archive ID like in borg 1.1.x + print('Manifest contents:') + for archive_info in manifest.archives.list(sort_by=['ts']): + print(format_archive(archive_info), '[%s]' % bin_to_hex(archive_info.id)) + manifest.write() + repository.commit() + if not key.tam_required: + key.tam_required = True + key.change_passphrase(key._passphrase) + print('Updated key') + if hasattr(key, 'find_key'): + print('Key location:', key.find_key()) + if not tam_required(repository): + tam_file = tam_required_file(repository) + open(tam_file, 'w').close() + print('Updated security database') + else: + # mainly for upgrades from Attic repositories, + # but also supports borg 0.xx -> 1.0 upgrade. + + repo = AtticRepositoryUpgrader(args.location.path, create=False) + try: + repo.upgrade(args.dry_run, inplace=args.inplace, progress=args.progress) + except NotImplementedError as e: + print("warning: %s" % e) + repo = BorgRepositoryUpgrader(args.location.path, create=False) + try: + repo.upgrade(args.dry_run, inplace=args.inplace, progress=args.progress) + except NotImplementedError as e: + print("warning: %s" % e) return self.exit_code @with_repository(cache=True, exclusive=True) @@ -2303,6 +2329,28 @@ class Archiver: upgrade_epilog = textwrap.dedent(""" Upgrade an existing Borg repository. + + Borg 1.x.y upgrades + ------------------- + + Use ``borg upgrade --tam REPO`` to require manifest authentication + introduced with Borg 1.0.9 to address security issues. This means + that modifying the repository after doing this with a version prior + to 1.0.9 will raise a validation error, so only perform this upgrade + after updating all clients using the repository to 1.0.9 or newer. + + This upgrade should be done on each client for safety reasons. + + If a repository is accidentally modified with a pre-1.0.9 client after + this upgrade, use ``borg upgrade --tam --force REPO`` to remedy it. + + See + https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability + for details. + + Attic and Borg 0.xx to Borg 1.x + ------------------------------- + This currently supports converting an Attic repository to Borg and also helps with converting Borg 0.xx to 1.0. @@ -2355,6 +2403,10 @@ class Archiver: default=False, action='store_true', help="""rewrite repository in place, with no chance of going back to older versions of the repository.""") + subparser.add_argument('--force', dest='force', action='store_true', + help="""Force upgrade""") + subparser.add_argument('--tam', dest='tam', action='store_true', + help="""Enable manifest authentication (in key and cache) (Borg 1.0.9 and later)""") subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False), help='path to the repository to be upgraded') diff --git a/src/borg/crypto.pyx b/src/borg/crypto.pyx index 7fa8b891..c3cda4ac 100644 --- a/src/borg/crypto.pyx +++ b/src/borg/crypto.pyx @@ -1,9 +1,13 @@ """A thin OpenSSL wrapper""" +import hashlib +import hmac +from math import ceil + from libc.stdlib cimport malloc, free from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release -API_VERSION = 3 +API_VERSION = 4 cdef extern from "blake2-libselect.h": @@ -247,3 +251,30 @@ def blake2b_256(key, data): raise Exception('blake2b_final() failed') return md + + +def hkdf_hmac_sha512(ikm, salt, info, output_length): + """ + Compute HKDF-HMAC-SHA512 with input key material *ikm*, *salt* and *info* to produce *output_length* bytes. + + This is the "HMAC-based Extract-and-Expand Key Derivation Function (HKDF)" (RFC 5869) + instantiated with HMAC-SHA512. + + *output_length* must not be greater than 64 * 255 bytes. + """ + digest_length = 64 + assert output_length <= (255 * digest_length), 'output_length must be <= 255 * 64 bytes' + # Step 1. HKDF-Extract (ikm, salt) -> prk + if salt is None: + salt = bytes(64) + prk = hmac.HMAC(salt, ikm, hashlib.sha512).digest() + + # Step 2. HKDF-Expand (prk, info, output_length) -> output key + n = ceil(output_length / digest_length) + t_n = b'' + output = b'' + for i in range(n): + msg = t_n + info + (i + 1).to_bytes(1, 'little') + t_n = hmac.HMAC(prk, msg, hashlib.sha512).digest() + output += t_n + return output[:output_length] diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 0c98f3a3..32120251 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -93,7 +93,7 @@ def check_extension_modules(): raise ExtensionModuleError if compress.API_VERSION != 2: raise ExtensionModuleError - if crypto.API_VERSION != 3: + if crypto.API_VERSION != 4: raise ExtensionModuleError if platform.API_VERSION != platform.OS_API_VERSION != 5: raise ExtensionModuleError @@ -192,15 +192,16 @@ class Manifest: self.key = key self.repository = repository self.item_keys = frozenset(item_keys) if item_keys is not None else ITEM_KEYS + self.tam_verified = False @property def id_str(self): return bin_to_hex(self.id) @classmethod - def load(cls, repository, key=None): + def load(cls, repository, key=None, force_tam_not_required=False): from .item import ManifestItem - from .key import key_factory + from .key import key_factory, tam_required_file, tam_required from .repository import Repository try: cdata = repository.get(cls.MANIFEST_ID) @@ -209,9 +210,10 @@ class Manifest: if not key: key = key_factory(repository, cdata) manifest = cls(key, repository) - _, data = key.decrypt(None, cdata) + data = key.decrypt(None, cdata).data + manifest_dict, manifest.tam_verified = key.unpack_and_verify_manifest(data, force_tam_not_required=force_tam_not_required) + m = ManifestItem(internal_dict=manifest_dict) manifest.id = key.id_hash(data) - m = ManifestItem(internal_dict=msgpack.unpackb(data)) if m.get('version') != 1: raise ValueError('Invalid manifest version') manifest.archives.set_raw_dict(m.archives) @@ -219,21 +221,28 @@ class Manifest: manifest.config = m.config # valid item keys are whatever is known in the repo or every key we know manifest.item_keys = ITEM_KEYS | frozenset(key.decode() for key in m.get('item_keys', [])) + if manifest.config.get(b'tam_required', False) and manifest.tam_verified and not tam_required(repository): + logger.debug('Manifest is TAM verified and says TAM is required, updating security database...') + file = tam_required_file(repository) + open(file, 'w').close() return manifest, key def write(self): from .item import ManifestItem + if self.key.tam_required: + self.config[b'tam_required'] = True self.timestamp = datetime.utcnow().isoformat() manifest = ManifestItem( version=1, - archives=self.archives.get_raw_dict(), + archives=StableDict(self.archives.get_raw_dict()), timestamp=self.timestamp, - config=self.config, - item_keys=tuple(self.item_keys), + config=StableDict(self.config), + item_keys=tuple(sorted(self.item_keys)), ) - data = msgpack.packb(manifest.as_dict()) + self.tam_verified = True + data = self.key.pack_and_authenticate_metadata(manifest.as_dict()) self.id = self.key.id_hash(data) - self.repository.put(self.MANIFEST_ID, self.key.encrypt(Chunk(data))) + self.repository.put(self.MANIFEST_ID, self.key.encrypt(Chunk(data, compression={'name': 'none'}))) def prune_within(archives, within): @@ -292,7 +301,6 @@ def get_keys_dir(): def get_security_dir(repository_id=None): """Determine where to store local security information.""" - xdg_config = os.environ.get('XDG_CONFIG_HOME', os.path.join(get_home_dir(), '.config')) security_dir = os.environ.get('BORG_SECURITY_DIR', os.path.join(xdg_config, 'borg', 'security')) if repository_id: diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 802322a8..bdcb9a53 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -213,7 +213,7 @@ class Key(PropDict): If a Key shall be serialized, give as_dict() method output to msgpack packer. """ - VALID_KEYS = {'version', 'repository_id', 'enc_key', 'enc_hmac_key', 'id_key', 'chunk_seed'} # str-typed keys + VALID_KEYS = {'version', 'repository_id', 'enc_key', 'enc_hmac_key', 'id_key', 'chunk_seed', 'tam_required'} # str-typed keys __slots__ = ("_dict", ) # avoid setting attributes not supported by properties @@ -223,6 +223,7 @@ class Key(PropDict): enc_hmac_key = PropDict._make_property('enc_hmac_key', bytes) id_key = PropDict._make_property('id_key', bytes) chunk_seed = PropDict._make_property('chunk_seed', int) + tam_required = PropDict._make_property('tam_required', bool) class ArchiveItem(PropDict): diff --git a/src/borg/key.py b/src/borg/key.py index 3a8168db..a03d21c1 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -4,8 +4,8 @@ import os import sys import textwrap from binascii import a2b_base64, b2a_base64, hexlify, unhexlify -from hashlib import sha256, pbkdf2_hmac -from hmac import compare_digest +from hashlib import sha256, sha512, pbkdf2_hmac +from hmac import HMAC, compare_digest import msgpack @@ -14,11 +14,11 @@ logger = create_logger() from .constants import * # NOQA from .compress import Compressor, get_compressor -from .crypto import AES, bytes_to_long, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256 -from .helpers import Chunk +from .crypto import AES, bytes_to_long, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512 +from .helpers import Chunk, StableDict from .helpers import Error, IntegrityError from .helpers import yes -from .helpers import get_keys_dir +from .helpers import get_keys_dir, get_security_dir from .helpers import bin_to_hex from .helpers import CompressionDecider2, CompressionSpec from .item import Key, EncryptedKey @@ -41,6 +41,10 @@ class UnsupportedPayloadError(Error): """Unsupported payload type {}. A newer version is required to access this repository.""" +class UnsupportedManifestError(Error): + """Unsupported manifest envelope. A newer version is required to access this repository.""" + + class KeyfileNotFoundError(Error): """No key file for repository {} found in {}.""" @@ -57,6 +61,32 @@ class RepoKeyNotFoundError(Error): """No key entry found in the config of repository {}.""" +class TAMRequiredError(IntegrityError): + __doc__ = textwrap.dedent(""" + Manifest is unauthenticated, but authentication is required for this repository. + + This either means that you are under attack, or that you modified this repository + with a Borg version older than 1.0.9 after TAM authentication was enabled. + + In the latter case, use "borg upgrade --tam --force '{}'" to re-authenticate the manifest. + """).strip() + traceback = False + + +class TAMInvalid(IntegrityError): + __doc__ = IntegrityError.__doc__ + traceback = False + + def __init__(self): + # Error message becomes: "Data integrity error: Manifest authentication did not verify" + super().__init__('Manifest authentication did not verify') + + +class TAMUnsupportedSuiteError(IntegrityError): + """Could not verify manifest: Unsupported suite {!r}; a newer version is needed.""" + traceback = False + + def key_creator(repository, args): if args.encryption == 'keyfile': return KeyfileKey.create(repository, args) @@ -94,6 +124,16 @@ def key_factory(repository, manifest_data): raise UnsupportedPayloadError(key_type) +def tam_required_file(repository): + security_dir = get_security_dir(bin_to_hex(repository.id)) + return os.path.join(security_dir, 'tam_required') + + +def tam_required(repository): + file = tam_required_file(repository) + return os.path.isfile(file) + + class KeyBase: TYPE = None # override in subclasses @@ -103,11 +143,11 @@ class KeyBase: self.target = None # key location file path / repo obj self.compression_decider2 = CompressionDecider2(CompressionSpec('none')) self.compressor = Compressor('none') # for decompression + self.tam_required = True def id_hash(self, data): """Return HMAC hash using the "id" HMAC key """ - def compress(self, chunk): compr_args, chunk = self.compression_decider2.decide(chunk) compressor = Compressor(**compr_args) @@ -127,6 +167,68 @@ class KeyBase: if not compare_digest(id_computed, id): raise IntegrityError('Chunk %s: id verification failed' % bin_to_hex(id)) + def _tam_key(self, salt, context): + return hkdf_hmac_sha512( + ikm=self.id_key + self.enc_key + self.enc_hmac_key, + salt=salt, + info=b'borg-metadata-authentication-' + context, + output_length=64 + ) + + def pack_and_authenticate_metadata(self, metadata_dict, context=b'manifest'): + metadata_dict = StableDict(metadata_dict) + tam = metadata_dict['tam'] = StableDict({ + 'type': 'HKDF_HMAC_SHA512', + 'hmac': bytes(64), + 'salt': os.urandom(64), + }) + packed = msgpack.packb(metadata_dict, unicode_errors='surrogateescape') + tam_key = self._tam_key(tam['salt'], context) + tam['hmac'] = HMAC(tam_key, packed, sha512).digest() + return msgpack.packb(metadata_dict, unicode_errors='surrogateescape') + + def unpack_and_verify_manifest(self, data, force_tam_not_required=False): + """Unpack msgpacked *data* and return (object, did_verify).""" + if data.startswith(b'\xc1' * 4): + # This is a manifest from the future, we can't read it. + raise UnsupportedManifestError() + tam_required = self.tam_required + if force_tam_not_required and tam_required: + logger.warning('Manifest authentication DISABLED.') + tam_required = False + data = bytearray(data) + # Since we don't trust these bytes we use the slower Python unpacker, + # which is assumed to have a lower probability of security issues. + unpacked = msgpack.fallback.unpackb(data, object_hook=StableDict, unicode_errors='surrogateescape') + if b'tam' not in unpacked: + if tam_required: + raise TAMRequiredError(self.repository._location.canonical_path()) + else: + logger.debug('TAM not found and not required') + return unpacked, False + tam = unpacked.pop(b'tam', None) + if not isinstance(tam, dict): + raise TAMInvalid() + tam_type = tam.get(b'type', b'').decode('ascii', 'replace') + if tam_type != 'HKDF_HMAC_SHA512': + if tam_required: + raise TAMUnsupportedSuiteError(repr(tam_type)) + else: + logger.debug('Ignoring TAM made with unsupported suite, since TAM is not required: %r', tam_type) + return unpacked, False + tam_hmac = tam.get(b'hmac') + tam_salt = tam.get(b'salt') + if not isinstance(tam_salt, bytes) or not isinstance(tam_hmac, bytes): + raise TAMInvalid() + offset = data.index(tam_hmac) + data[offset:offset + 64] = bytes(64) + tam_key = self._tam_key(tam_salt, context=b'manifest') + calculated_hmac = HMAC(tam_key, data, sha512).digest() + if not compare_digest(calculated_hmac, tam_hmac): + raise TAMInvalid() + logger.debug('TAM-verified manifest') + return unpacked, True + class PlaintextKey(KeyBase): TYPE = 0x02 @@ -134,6 +236,10 @@ class PlaintextKey(KeyBase): chunk_seed = 0 + def __init__(self, repository): + super().__init__(repository) + self.tam_required = False + @classmethod def create(cls, repository, args): logger.info('Encryption NOT enabled.\nUse the "--encryption=repokey|keyfile" to enable encryption.') @@ -161,6 +267,9 @@ class PlaintextKey(KeyBase): self.assert_id(id, data) return Chunk(data) + def _tam_key(self, salt, context): + return salt + context + def random_blake2b_256_key(): # This might look a bit curious, but is the same construction used in the keyed mode of BLAKE2b. @@ -373,6 +482,7 @@ class PassphraseKey(ID_HMAC_SHA_256, AESKeyBase): key.decrypt(None, manifest_data) num_blocks = num_aes_blocks(len(manifest_data) - 41) key.init_ciphers(key.extract_nonce(manifest_data) + num_blocks) + key._passphrase = passphrase return key except IntegrityError: passphrase = Passphrase.getpass(prompt) @@ -388,6 +498,7 @@ class PassphraseKey(ID_HMAC_SHA_256, AESKeyBase): def init(self, repository, passphrase): self.init_from_random_data(passphrase.kdf(repository.id, self.iterations, 100)) self.init_ciphers() + self.tam_required = False class KeyfileKeyBase(AESKeyBase): @@ -411,6 +522,7 @@ class KeyfileKeyBase(AESKeyBase): raise PassphraseWrong num_blocks = num_aes_blocks(len(manifest_data) - 41) key.init_ciphers(key.extract_nonce(manifest_data) + num_blocks) + key._passphrase = passphrase return key def find_key(self): @@ -432,6 +544,7 @@ class KeyfileKeyBase(AESKeyBase): self.enc_hmac_key = key.enc_hmac_key self.id_key = key.id_key self.chunk_seed = key.chunk_seed + self.tam_required = key.get('tam_required', tam_required(self.repository)) return True return False @@ -469,15 +582,16 @@ class KeyfileKeyBase(AESKeyBase): enc_hmac_key=self.enc_hmac_key, id_key=self.id_key, chunk_seed=self.chunk_seed, + tam_required=self.tam_required, ) data = self.encrypt_key_file(msgpack.packb(key.as_dict()), passphrase) key_data = '\n'.join(textwrap.wrap(b2a_base64(data).decode('ascii'))) return key_data - def change_passphrase(self): - passphrase = Passphrase.new(allow_empty=True) + def change_passphrase(self, passphrase=None): + if passphrase is None: + passphrase = Passphrase.new(allow_empty=True) self.save(self.target, passphrase) - logger.info('Key updated') @classmethod def create(cls, repository, args): diff --git a/src/borg/selftest.py b/src/borg/selftest.py index 40d8d06e..d2ea9a76 100644 --- a/src/borg/selftest.py +++ b/src/borg/selftest.py @@ -30,7 +30,7 @@ SELFTEST_CASES = [ ChunkerTestCase, ] -SELFTEST_COUNT = 30 +SELFTEST_COUNT = 35 class SelfTestResult(TestResult): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 68c8f9e2..a9c2c7ba 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -3,6 +3,8 @@ from configparser import ConfigParser import errno import os import inspect +from datetime import datetime +from datetime import timedelta from io import StringIO import logging import random @@ -17,6 +19,7 @@ import unittest from unittest.mock import patch from hashlib import sha256 +import msgpack import pytest try: import llfuse @@ -34,7 +37,7 @@ from ..helpers import Chunk, Manifest from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from ..helpers import bin_to_hex from ..item import Item -from ..key import KeyfileKeyBase, RepoKey, KeyfileKey, Passphrase +from ..key import KeyfileKeyBase, RepoKey, KeyfileKey, Passphrase, TAMRequiredError from ..keymanager import RepoIdMismatch, NotABorgKeyFile from ..remote import RemoteRepository, PathNotAllowed from ..repository import Repository @@ -42,6 +45,7 @@ from . import has_lchflags, has_llfuse from . import BaseTestCase, changedir, environment_variable, no_selinux from . import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, is_utime_fully_supported from .platform import fakeroot_detected +from . import key src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) @@ -1645,8 +1649,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): def verify_uniqueness(): with Repository(self.repository_path) as repository: - for key, _ in repository.open_index(repository.get_transaction_id()).iteritems(): - data = repository.get(key) + for id, _ in repository.open_index(repository.get_transaction_id()).iteritems(): + data = repository.get(id) hash = sha256(data).digest() if hash not in seen: seen.add(hash) @@ -1947,7 +1951,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): repo_key = RepoKey(repository) repo_key.load(None, Passphrase.env_passphrase()) - backup_key = KeyfileKey(None) + backup_key = KeyfileKey(key.TestKey.MockRepository()) backup_key.load(export_file, Passphrase.env_passphrase()) assert repo_key.enc_key == backup_key.enc_key @@ -2251,6 +2255,63 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.cmd('list', self.repository_location + '::0.13', exit_code=0) +class ManifestAuthenticationTest(ArchiverTestCaseBase): + def test_fresh_init_tam_required(self): + self.cmd('init', self.repository_location) + repository = Repository(self.repository_path, exclusive=True) + with repository: + manifest, key = Manifest.load(repository) + repository.put(Manifest.MANIFEST_ID, key.encrypt(Chunk(msgpack.packb({ + 'version': 1, + 'archives': {}, + 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), + })))) + repository.commit() + + with pytest.raises(TAMRequiredError): + self.cmd('list', self.repository_location) + + def test_not_required(self): + self.cmd('init', self.repository_location) + self.create_src_archive('archive1234') + repository = Repository(self.repository_path, exclusive=True) + with repository: + shutil.rmtree(get_security_dir(bin_to_hex(repository.id))) + _, key = Manifest.load(repository) + key.tam_required = False + key.change_passphrase(key._passphrase) + + manifest = msgpack.unpackb(key.decrypt(None, repository.get(Manifest.MANIFEST_ID)).data) + del manifest[b'tam'] + repository.put(Manifest.MANIFEST_ID, key.encrypt(Chunk(msgpack.packb(manifest)))) + repository.commit() + output = self.cmd('list', '--debug', self.repository_location) + assert 'archive1234' in output + assert 'TAM not found and not required' in output + # Run upgrade + self.cmd('upgrade', '--tam', self.repository_location) + # Manifest must be authenticated now + output = self.cmd('list', '--debug', self.repository_location) + assert 'archive1234' in output + assert 'TAM-verified manifest' in output + # Try to spoof / modify pre-1.0.9 + with repository: + _, key = Manifest.load(repository) + repository.put(Manifest.MANIFEST_ID, key.encrypt(Chunk(msgpack.packb({ + 'version': 1, + 'archives': {}, + 'config': {}, + 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), + })))) + repository.commit() + # Fails + with pytest.raises(TAMRequiredError): + self.cmd('list', self.repository_location) + # Force upgrade + self.cmd('upgrade', '--tam', '--force', self.repository_location) + self.cmd('list', self.repository_location) + + @pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteArchiverTestCase(ArchiverTestCase): prefix = '__testsuite__:' diff --git a/src/borg/testsuite/crypto.py b/src/borg/testsuite/crypto.py index aa138a76..92cb06a4 100644 --- a/src/borg/testsuite/crypto.py +++ b/src/borg/testsuite/crypto.py @@ -2,7 +2,7 @@ from binascii import hexlify, unhexlify from ..crypto import AES, bytes_to_long, bytes_to_int, long_to_bytes, hmac_sha256, blake2b_256 from ..crypto import increment_iv, bytes16_to_int, int_to_bytes16 - +from ..crypto import hkdf_hmac_sha512 from . import BaseTestCase # Note: these tests are part of the self test, do not use or import py.test functionality here. @@ -96,3 +96,55 @@ class CryptoTestCase(BaseTestCase): key = unhexlify('e944973af2256d4d670c12dd75304c319f58f4e40df6fb18ef996cb47e063676') data = memoryview(b'1234567890' * 100) assert blake2b_256(key, data) == unhexlify('97ede832378531dd0f4c668685d166e797da27b47d8cd441e885b60abd5e0cb2') + + # These test vectors come from https://www.kullo.net/blog/hkdf-sha-512-test-vectors/ + # who claims to have verified these against independent Python and C++ implementations. + + def test_hkdf_hmac_sha512(self): + ikm = b'\x0b' * 22 + salt = bytes.fromhex('000102030405060708090a0b0c') + info = bytes.fromhex('f0f1f2f3f4f5f6f7f8f9') + l = 42 + + okm = hkdf_hmac_sha512(ikm, salt, info, l) + assert okm == bytes.fromhex('832390086cda71fb47625bb5ceb168e4c8e26a1a16ed34d9fc7fe92c1481579338da362cb8d9f925d7cb') + + def test_hkdf_hmac_sha512_2(self): + ikm = bytes.fromhex('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252627' + '28292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f') + salt = bytes.fromhex('606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868' + '788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf') + info = bytes.fromhex('b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7' + 'd8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff') + l = 82 + + okm = hkdf_hmac_sha512(ikm, salt, info, l) + assert okm == bytes.fromhex('ce6c97192805b346e6161e821ed165673b84f400a2b514b2fe23d84cd189ddf1b695b48cbd1c838844' + '1137b3ce28f16aa64ba33ba466b24df6cfcb021ecff235f6a2056ce3af1de44d572097a8505d9e7a93') + + def test_hkdf_hmac_sha512_3(self): + ikm = bytes.fromhex('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b') + salt = None + info = b'' + l = 42 + + okm = hkdf_hmac_sha512(ikm, salt, info, l) + assert okm == bytes.fromhex('f5fa02b18298a72a8c23898a8703472c6eb179dc204c03425c970e3b164bf90fff22d04836d0e2343bac') + + def test_hkdf_hmac_sha512_4(self): + ikm = bytes.fromhex('0b0b0b0b0b0b0b0b0b0b0b') + salt = bytes.fromhex('000102030405060708090a0b0c') + info = bytes.fromhex('f0f1f2f3f4f5f6f7f8f9') + l = 42 + + okm = hkdf_hmac_sha512(ikm, salt, info, l) + assert okm == bytes.fromhex('7413e8997e020610fbf6823f2ce14bff01875db1ca55f68cfcf3954dc8aff53559bd5e3028b080f7c068') + + def test_hkdf_hmac_sha512_5(self): + ikm = bytes.fromhex('0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c') + salt = None + info = b'' + l = 42 + + okm = hkdf_hmac_sha512(ikm, salt, info, l) + assert okm == bytes.fromhex('1407d46013d98bc6decefcfee55f0f90b0c7f63d68eb1a80eaf07e953cfc0a3a5240a155d6e4daa965bb') diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 1bc1ad7c..6277d98d 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -659,6 +659,7 @@ def test_get_keys_dir(monkeypatch): def test_get_security_dir(monkeypatch): """test that get_security_dir respects environment""" + monkeypatch.delenv('BORG_SECURITY_DIR', raising=False) monkeypatch.delenv('XDG_CONFIG_HOME', raising=False) assert get_security_dir() == os.path.join(os.path.expanduser('~'), '.config', 'borg', 'security') assert get_security_dir(repository_id='1234') == os.path.join(os.path.expanduser('~'), '.config', 'borg', 'security', '1234') diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index 99e3a6c8..6d916a28 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -5,14 +5,16 @@ import os.path from binascii import hexlify, unhexlify import pytest +import msgpack from ..crypto import bytes_to_long, num_aes_blocks from ..helpers import Location -from ..helpers import Chunk +from ..helpers import Chunk, StableDict from ..helpers import IntegrityError from ..helpers import get_security_dir from ..key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, AuthenticatedKey from ..key import Passphrase, PasswordRetriesExceeded, bin_to_hex +from ..key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError class TestKey: @@ -74,6 +76,9 @@ class TestKey: class _Location: orig = '/some/place' + def canonical_path(self): + return self.orig + _location = _Location() id = bytes(32) id_str = bin_to_hex(id) @@ -277,3 +282,115 @@ class TestPassphrase: def test_passphrase_repr(self): assert "secret" not in repr(Passphrase("secret")) + + +class TestTAM: + @pytest.fixture + def key(self, monkeypatch): + monkeypatch.setenv('BORG_PASSPHRASE', 'test') + return KeyfileKey.create(TestKey.MockRepository(), TestKey.MockArgs()) + + def test_unpack_future(self, key): + blob = b'\xc1\xc1\xc1\xc1foobar' + with pytest.raises(UnsupportedManifestError): + key.unpack_and_verify_manifest(blob) + + blob = b'\xc1\xc1\xc1' + with pytest.raises(msgpack.UnpackException): + key.unpack_and_verify_manifest(blob) + + def test_missing_when_required(self, key): + blob = msgpack.packb({}) + with pytest.raises(TAMRequiredError): + key.unpack_and_verify_manifest(blob) + + def test_missing(self, key): + blob = msgpack.packb({}) + key.tam_required = False + unpacked, verified = key.unpack_and_verify_manifest(blob) + assert unpacked == {} + assert not verified + + def test_unknown_type_when_required(self, key): + blob = msgpack.packb({ + 'tam': { + 'type': 'HMAC_VOLLBIT', + }, + }) + with pytest.raises(TAMUnsupportedSuiteError): + key.unpack_and_verify_manifest(blob) + + def test_unknown_type(self, key): + blob = msgpack.packb({ + 'tam': { + 'type': 'HMAC_VOLLBIT', + }, + }) + key.tam_required = False + unpacked, verified = key.unpack_and_verify_manifest(blob) + assert unpacked == {} + assert not verified + + @pytest.mark.parametrize('tam, exc', ( + ({}, TAMUnsupportedSuiteError), + ({'type': b'\xff'}, TAMUnsupportedSuiteError), + (None, TAMInvalid), + (1234, TAMInvalid), + )) + def test_invalid(self, key, tam, exc): + blob = msgpack.packb({ + 'tam': tam, + }) + with pytest.raises(exc): + key.unpack_and_verify_manifest(blob) + + @pytest.mark.parametrize('hmac, salt', ( + ({}, bytes(64)), + (bytes(64), {}), + (None, bytes(64)), + (bytes(64), None), + )) + def test_wrong_types(self, key, hmac, salt): + data = { + 'tam': { + 'type': 'HKDF_HMAC_SHA512', + 'hmac': hmac, + 'salt': salt + }, + } + tam = data['tam'] + if hmac is None: + del tam['hmac'] + if salt is None: + del tam['salt'] + blob = msgpack.packb(data) + with pytest.raises(TAMInvalid): + key.unpack_and_verify_manifest(blob) + + def test_round_trip(self, key): + data = {'foo': 'bar'} + blob = key.pack_and_authenticate_metadata(data) + assert blob.startswith(b'\x82') + + unpacked = msgpack.unpackb(blob) + assert unpacked[b'tam'][b'type'] == b'HKDF_HMAC_SHA512' + + unpacked, verified = key.unpack_and_verify_manifest(blob) + assert verified + assert unpacked[b'foo'] == b'bar' + assert b'tam' not in unpacked + + @pytest.mark.parametrize('which', (b'hmac', b'salt')) + def test_tampered(self, key, which): + data = {'foo': 'bar'} + blob = key.pack_and_authenticate_metadata(data) + assert blob.startswith(b'\x82') + + unpacked = msgpack.unpackb(blob, object_hook=StableDict) + assert len(unpacked[b'tam'][which]) == 64 + unpacked[b'tam'][which] = unpacked[b'tam'][which][0:32] + bytes(32) + assert len(unpacked[b'tam'][which]) == 64 + blob = msgpack.packb(unpacked) + + with pytest.raises(TAMInvalid): + key.unpack_and_verify_manifest(blob) From d15fb241bd2ca701bc52e3a9c2925cff21b6b238 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Dec 2016 01:48:33 +0100 Subject: [PATCH 0500/1387] check: handle duplicate archive items neatly # Conflicts: # src/borg/archive.py --- docs/changes.rst | 3 +++ src/borg/archive.py | 14 ++++++++++++-- src/borg/testsuite/archiver.py | 27 +++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 928a9c2e..7da0c2f1 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -134,6 +134,9 @@ Security fixes: - A flaw in the cryptographic authentication scheme in Borg allowed an attacker to spoof the manifest. See :ref:`tam_vuln` above for the steps you should take. +- borg check: When rebuilding the manifest (which should only be needed very rarely) + duplicate archive names would be handled on a "first come first serve" basis, allowing + an attacker to apparently replace archives. Bug fixes: diff --git a/src/borg/archive.py b/src/borg/archive.py index 3793e7e7..90759dae 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1197,8 +1197,18 @@ class ArchiveChecker: continue if valid_archive(archive): archive = ArchiveItem(internal_dict=archive) - logger.info('Found archive %s', archive.name) - manifest.archives[archive.name] = (chunk_id, archive.time) + name = archive.name + logger.info('Found archive %s', name) + if name in manifest.archives: + i = 1 + while True: + new_name = '%s.%d' % (name, i) + if new_name not in manifest.archives: + break + i += 1 + logger.warning('Duplicate archive name %s, storing as %s', name, new_name) + name = new_name + manifest.archives[name] = (chunk_id, archive.time) logger.info('Manifest rebuild complete.') return manifest diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index a9c2c7ba..5637446c 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2180,6 +2180,33 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.assert_in('archive2', output) self.cmd('check', self.repository_location, exit_code=0) + def test_manifest_rebuild_duplicate_archive(self): + archive, repository = self.open_archive('archive1') + key = archive.key + with repository: + manifest = repository.get(Manifest.MANIFEST_ID) + corrupted_manifest = manifest + b'corrupted!' + repository.put(Manifest.MANIFEST_ID, corrupted_manifest) + + archive = msgpack.packb({ + 'cmdline': [], + 'items': [], + 'hostname': 'foo', + 'username': 'bar', + 'name': 'archive1', + 'time': '2016-12-15T18:49:51.849711', + 'version': 1, + }) + archive_id = key.id_hash(archive) + repository.put(archive_id, key.encrypt(Chunk(archive))) + repository.commit() + self.cmd('check', self.repository_location, exit_code=1) + self.cmd('check', '--repair', self.repository_location, exit_code=0) + output = self.cmd('list', self.repository_location) + self.assert_in('archive1', output) + self.assert_in('archive1.1', output) + self.assert_in('archive2', output) + def test_extra_chunks(self): self.cmd('check', self.repository_location, exit_code=0) with Repository(self.repository_location, exclusive=True) as repository: From c7c8c0fb57b54c0b87ffba5691e89938cdaa6cec Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Dec 2016 23:30:34 +0100 Subject: [PATCH 0501/1387] init: explain manifest auth compatibility # Conflicts: # src/borg/archiver.py --- src/borg/archiver.py | 18 +++++++++++++++--- src/borg/key.py | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index cfe497b3..55b20ad3 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -182,7 +182,8 @@ class Archiver: @with_repository(create=True, exclusive=True, manifest=False) def do_init(self, args, repository): """Initialize an empty repository""" - logger.info('Initializing repository at "%s"' % args.location.canonical_path()) + path = args.location.canonical_path() + logger.info('Initializing repository at "%s"' % path) try: key = key_creator(repository, args) except (EOFError, KeyboardInterrupt): @@ -194,8 +195,19 @@ class Archiver: repository.commit() with Cache(repository, key, manifest, warn_if_unencrypted=False): pass - tam_file = tam_required_file(repository) - open(tam_file, 'w').close() + if key.tam_required: + tam_file = tam_required_file(repository) + open(tam_file, 'w').close() + logger.warning( + '\n' + 'By default repositories initialized with this version will produce security\n' + 'errors if written to with an older version (up to and including Borg 1.0.8).\n' + '\n' + 'If you want to use these older versions, you can disable the check by runnning:\n' + 'borg upgrade --disable-tam \'%s\'\n' + '\n' + 'See https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability ' + 'for details about the security implications.', path) return self.exit_code @with_repository(exclusive=True, manifest=False) diff --git a/src/borg/key.py b/src/borg/key.py index a03d21c1..a623dd94 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -63,7 +63,7 @@ class RepoKeyNotFoundError(Error): class TAMRequiredError(IntegrityError): __doc__ = textwrap.dedent(""" - Manifest is unauthenticated, but authentication is required for this repository. + Manifest is unauthenticated, but it is required for this repository. This either means that you are under attack, or that you modified this repository with a Borg version older than 1.0.9 after TAM authentication was enabled. From df40b3840c56501754d3bf855b3502ff6368da54 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Dec 2016 23:32:42 +0100 Subject: [PATCH 0502/1387] upgrade: --disable-tam # Conflicts: # src/borg/helpers.py # src/borg/testsuite/archiver.py --- src/borg/archiver.py | 28 ++++++++++++++++++++++--- src/borg/helpers.py | 15 ++++++++++---- src/borg/testsuite/archiver.py | 37 +++++++++++++++++++++++++--------- 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 55b20ad3..1c650f05 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -61,6 +61,8 @@ def argument(args, str_or_bool): """If bool is passed, return it. If str is passed, retrieve named attribute from args.""" if isinstance(str_or_bool, str): return getattr(args, str_or_bool) + if isinstance(str_or_bool, (list, tuple)): + return any(getattr(args, item) for item in str_or_bool) return str_or_bool @@ -1062,29 +1064,43 @@ class Archiver: DASHES, logger=logging.getLogger('borg.output.stats')) return self.exit_code - @with_repository(fake='tam', invert_fake=True, manifest=False, exclusive=True) + @with_repository(fake=('tam', 'disable_tam'), invert_fake=True, manifest=False, exclusive=True) def do_upgrade(self, args, repository, manifest=None, key=None): """upgrade a repository from a previous version""" if args.tam: manifest, key = Manifest.load(repository, force_tam_not_required=args.force) - if not manifest.tam_verified: + if not manifest.tam_verified or not manifest.config.get(b'tam_required', False): # The standard archive listing doesn't include the archive ID like in borg 1.1.x print('Manifest contents:') for archive_info in manifest.archives.list(sort_by=['ts']): print(format_archive(archive_info), '[%s]' % bin_to_hex(archive_info.id)) + manifest.config[b'tam_required'] = True manifest.write() repository.commit() if not key.tam_required: key.tam_required = True key.change_passphrase(key._passphrase) - print('Updated key') + print('Key updated') if hasattr(key, 'find_key'): print('Key location:', key.find_key()) if not tam_required(repository): tam_file = tam_required_file(repository) open(tam_file, 'w').close() print('Updated security database') + elif args.disable_tam: + manifest, key = Manifest.load(repository, force_tam_not_required=True) + if tam_required(repository): + os.unlink(tam_required_file(repository)) + if key.tam_required: + key.tam_required = False + key.change_passphrase(key._passphrase) + print('Key updated') + if hasattr(key, 'find_key'): + print('Key location:', key.find_key()) + manifest.config[b'tam_required'] = False + manifest.write() + repository.commit() else: # mainly for upgrades from Attic repositories, # but also supports borg 0.xx -> 1.0 upgrade. @@ -2356,6 +2372,10 @@ class Archiver: If a repository is accidentally modified with a pre-1.0.9 client after this upgrade, use ``borg upgrade --tam --force REPO`` to remedy it. + If you routinely do this you might not want to enable this upgrade + (which will leave you exposed to the security issue). You can + reverse the upgrade by issuing ``borg upgrade --disable-tam REPO``. + See https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability for details. @@ -2419,6 +2439,8 @@ class Archiver: help="""Force upgrade""") subparser.add_argument('--tam', dest='tam', action='store_true', help="""Enable manifest authentication (in key and cache) (Borg 1.0.9 and later)""") + subparser.add_argument('--disable-tam', dest='disable_tam', action='store_true', + help="""Disable manifest authentication (in key and cache)""") subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False), help='path to the repository to be upgraded') diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 32120251..9b10a984 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -221,10 +221,17 @@ class Manifest: manifest.config = m.config # valid item keys are whatever is known in the repo or every key we know manifest.item_keys = ITEM_KEYS | frozenset(key.decode() for key in m.get('item_keys', [])) - if manifest.config.get(b'tam_required', False) and manifest.tam_verified and not tam_required(repository): - logger.debug('Manifest is TAM verified and says TAM is required, updating security database...') - file = tam_required_file(repository) - open(file, 'w').close() + + if manifest.tam_verified: + manifest_required = manifest.config.get(b'tam_required', False) + security_required = tam_required(repository) + if manifest_required and not security_required: + logger.debug('Manifest is TAM verified and says TAM is required, updating security database...') + file = tam_required_file(repository) + open(file, 'w').close() + if not manifest_required and security_required: + logger.debug('Manifest is TAM verified and says TAM is *not* required, updating security database...') + os.unlink(tam_required_file(repository)) return manifest, key def write(self): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 5637446c..b3c702d7 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2283,6 +2283,17 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): class ManifestAuthenticationTest(ArchiverTestCaseBase): + def spoof_manifest(self, repository): + with repository: + _, key = Manifest.load(repository) + repository.put(Manifest.MANIFEST_ID, key.encrypt(Chunk(msgpack.packb({ + 'version': 1, + 'archives': {}, + 'config': {}, + 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), + })))) + repository.commit() + def test_fresh_init_tam_required(self): self.cmd('init', self.repository_location) repository = Repository(self.repository_path, exclusive=True) @@ -2322,15 +2333,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): assert 'archive1234' in output assert 'TAM-verified manifest' in output # Try to spoof / modify pre-1.0.9 - with repository: - _, key = Manifest.load(repository) - repository.put(Manifest.MANIFEST_ID, key.encrypt(Chunk(msgpack.packb({ - 'version': 1, - 'archives': {}, - 'config': {}, - 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), - })))) - repository.commit() + self.spoof_manifest(repository) # Fails with pytest.raises(TAMRequiredError): self.cmd('list', self.repository_location) @@ -2338,6 +2341,22 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): self.cmd('upgrade', '--tam', '--force', self.repository_location) self.cmd('list', self.repository_location) + def test_disable(self): + self.cmd('init', self.repository_location) + self.create_src_archive('archive1234') + self.cmd('upgrade', '--disable-tam', self.repository_location) + repository = Repository(self.repository_path, exclusive=True) + self.spoof_manifest(repository) + assert not self.cmd('list', self.repository_location) + + def test_disable2(self): + self.cmd('init', self.repository_location) + self.create_src_archive('archive1234') + repository = Repository(self.repository_path, exclusive=True) + self.spoof_manifest(repository) + self.cmd('upgrade', '--disable-tam', self.repository_location) + assert not self.cmd('list', self.repository_location) + @pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteArchiverTestCase(ArchiverTestCase): From 91991988e10b08fbc5d0517b4c6079a6b0462e74 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Dec 2016 18:00:03 +0100 Subject: [PATCH 0503/1387] Fix subsubparsers for Python <3.4.3 This works around http://bugs.python.org/issue9351 Since Debian and Ubuntu ship 3.4.2 and 3.4.0 respectively. --- borg/archiver.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 5ee61297..2cdfe37f 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1160,7 +1160,7 @@ class Archiver: help='manage repository key') key_parsers = subparser.add_subparsers(title='required arguments', metavar='') - subparser.set_defaults(func=functools.partial(self.do_subcommand_help, subparser)) + subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser)) key_export_epilog = textwrap.dedent(""" If repository encryption is used, the repository is inaccessible @@ -1694,7 +1694,7 @@ class Archiver: help='debugging command (not intended for normal use)') debug_parsers = subparser.add_subparsers(title='required arguments', metavar='') - subparser.set_defaults(func=functools.partial(self.do_subcommand_help, subparser)) + subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser)) debug_info_epilog = textwrap.dedent(""" This command displays some system information that might be useful for bug @@ -1902,11 +1902,13 @@ class Archiver: def run(self, args): os.umask(args.umask) # early, before opening files self.lock_wait = args.lock_wait - setup_logging(level=args.log_level, is_serve=args.func == self.do_serve) # do not use loggers before this! + # This works around http://bugs.python.org/issue9351 + func = getattr(args, 'func', None) or getattr(args, 'fallback_func') + setup_logging(level=args.log_level, is_serve=func == self.do_serve) # do not use loggers before this! check_extension_modules() if is_slow_msgpack(): logger.warning("Using a pure-python msgpack! This will result in lower performance.") - return args.func(args) + return func(args) def sig_info_handler(sig_no, stack): # pragma: no cover From 880578da064f1ddcd53fdee2c356529c75bcbca7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 19 Dec 2016 16:06:54 +0100 Subject: [PATCH 0504/1387] quickstart: use prune with --list so people are better aware of what's happening, avoiding pitfalls with wrong or missing --prefix. --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 78966eb5..58ce6c96 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -126,7 +126,7 @@ certain number of old archives:: # archives of THIS machine. The '{hostname}-' prefix is very important to # limit prune's operation to this machine's archives and not apply to # other machine's archives also. - borg prune -v $REPOSITORY --prefix '{hostname}-' \ + borg prune -v --list $REPOSITORY --prefix '{hostname}-' \ --keep-daily=7 --keep-weekly=4 --keep-monthly=6 Pitfalls with shell variables and environment variables From c9cc97e05be23d2af3decfa89977d2bdbcfec488 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 20 Dec 2016 00:49:24 +0100 Subject: [PATCH 0505/1387] CHANGES: fix 1.0.9 release date --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 7da0c2f1..4208dad4 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -126,7 +126,7 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= -Version 1.0.9 (2016-12-18) +Version 1.0.9 (2016-12-20) -------------------------- Security fixes: From e0e5bc4aa448b2cbe400275d960b6dda70a522f2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Dec 2016 23:09:28 +0100 Subject: [PATCH 0506/1387] ran build_usage --- docs/usage/upgrade.rst.inc | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index 525c5ebd..6b06847a 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -20,6 +20,12 @@ optional arguments ``-i``, ``--inplace`` | rewrite repository in place, with no chance of going back to older | versions of the repository. + ``--force`` + | Force upgrade + ``--tam`` + | Enable manifest authentication (in key and cache) (Borg 1.0.9 and later) + ``--disable-tam`` + | Disable manifest authentication (in key and cache) `Common options`_ | @@ -28,6 +34,32 @@ Description ~~~~~~~~~~~ Upgrade an existing Borg repository. + +Borg 1.x.y upgrades +------------------- + +Use ``borg upgrade --tam REPO`` to require manifest authentication +introduced with Borg 1.0.9 to address security issues. This means +that modifying the repository after doing this with a version prior +to 1.0.9 will raise a validation error, so only perform this upgrade +after updating all clients using the repository to 1.0.9 or newer. + +This upgrade should be done on each client for safety reasons. + +If a repository is accidentally modified with a pre-1.0.9 client after +this upgrade, use ``borg upgrade --tam --force REPO`` to remedy it. + +If you routinely do this you might not want to enable this upgrade +(which will leave you exposed to the security issue). You can +reverse the upgrade by issuing ``borg upgrade --disable-tam REPO``. + +See +https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability +for details. + +Attic and Borg 0.xx to Borg 1.x +------------------------------- + This currently supports converting an Attic repository to Borg and also helps with converting Borg 0.xx to 1.0. From a0abc3eb7526d550ce093a5c139550300128f336 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Dec 2016 23:26:22 +0100 Subject: [PATCH 0507/1387] CHANGES: move 1.1.0b3 to correct position --- docs/changes.rst | 146 +++++++++++++++++++++++------------------------ 1 file changed, 72 insertions(+), 74 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 4208dad4..053147ff 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -126,6 +126,78 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= +Version 1.1.0b3 (not released yet) +---------------------------------- + +Bug fixes: + +- borg recreate: don't rechunkify unless explicitly told so +- borg info: fixed bug when called without arguments, #1914 +- borg init: fix free space check crashing if disk is full, #1821 +- borg debug delete/get obj: fix wrong reference to exception + +New features: + +- add blake2b key modes (use blake2b as MAC). This links against system libb2, + if possible, otherwise uses bundled code +- automatically remove stale locks - set BORG_HOSTNAME_IS_UNIQUE env var + to enable stale lock killing. If set, stale locks in both cache and + repository are deleted. #562 +- borg info : print general repo information, #1680 +- borg check --first / --last / --sort / --prefix, #1663 +- borg mount --first / --last / --sort / --prefix, #1542 +- implement "health" item formatter key, #1749 +- BORG_SECURITY_DIR to remember security related infos outside the cache. + Key type, location and manifest timestamp checks now survive cache + deletion. This also means that you can now delete your cache and avoid + previous warnings, since Borg can still tell it's safe. +- implement BORG_NEW_PASSPHRASE, #1768 + +Other changes: + +- borg recreate: + + - remove special-cased --dry-run + - update --help + - remove bloat: interruption blah, autocommit blah, resuming blah + - re-use existing checkpoint functionality + - archiver tests: add check_cache tool - lints refcounts + +- fixed cache sync performance regression from 1.1.0b1 onwards, #1940 +- syncing the cache without chunks.archive.d (see :ref:`disable_archive_chunks`) + now avoids any merges and is thus faster, #1940 +- borg check --verify-data: faster due to linear on-disk-order scan +- borg debug-xxx commands removed, we use "debug xxx" subcommands now, #1627 +- improve metadata handling speed +- shortcut hashindex_set by having hashindex_lookup hint about address +- improve / add progress displays, #1721 +- check for index vs. segment files object count mismatch +- make RPC protocol more extensible: use named parameters. +- RemoteRepository: misc. code cleanups / refactors +- clarify cache/repository README file + +- docs: + + - quickstart: add a comment about other (remote) filesystems + - quickstart: only give one possible ssh url syntax, all others are + documented in usage chapter. + - mention file:// + - document repo URLs / archive location + - clarify borg diff help, #980 + - deployment: synthesize alternative --restrict-to-path example + - improve cache / index docs, esp. files cache docs, #1825 + - document using "git merge 1.0-maint -s recursive -X rename-threshold=20%" + for avoiding troubles when merging the 1.0-maint branch into master. + +- tests: + + - fuse tests: catch ENOTSUP on freebsd + - fuse tests: test troublesome xattrs last + - fix byte range error in test, #1740 + - use monkeypatch to set env vars, but only on pytest based tests. + - point XDG_*_HOME to temp dirs for tests, #1714 + - remove all BORG_* env vars from the outer environment + Version 1.0.9 (2016-12-20) -------------------------- @@ -226,80 +298,6 @@ Other changes: - add windows virtual machine with cygwin - Vagrantfile cleanup / code deduplication - -Version 1.1.0b3 (not released yet) ----------------------------------- - -Bug fixes: - -- borg recreate: don't rechunkify unless explicitly told so -- borg info: fixed bug when called without arguments, #1914 -- borg init: fix free space check crashing if disk is full, #1821 -- borg debug delete/get obj: fix wrong reference to exception - -New features: - -- add blake2b key modes (use blake2b as MAC). This links against system libb2, - if possible, otherwise uses bundled code -- automatically remove stale locks - set BORG_HOSTNAME_IS_UNIQUE env var - to enable stale lock killing. If set, stale locks in both cache and - repository are deleted. #562 -- borg info : print general repo information, #1680 -- borg check --first / --last / --sort / --prefix, #1663 -- borg mount --first / --last / --sort / --prefix, #1542 -- implement "health" item formatter key, #1749 -- BORG_SECURITY_DIR to remember security related infos outside the cache. - Key type, location and manifest timestamp checks now survive cache - deletion. This also means that you can now delete your cache and avoid - previous warnings, since Borg can still tell it's safe. -- implement BORG_NEW_PASSPHRASE, #1768 - -Other changes: - -- borg recreate: - - - remove special-cased --dry-run - - update --help - - remove bloat: interruption blah, autocommit blah, resuming blah - - re-use existing checkpoint functionality - - archiver tests: add check_cache tool - lints refcounts - -- fixed cache sync performance regression from 1.1.0b1 onwards, #1940 -- syncing the cache without chunks.archive.d (see :ref:`disable_archive_chunks`) - now avoids any merges and is thus faster, #1940 -- borg check --verify-data: faster due to linear on-disk-order scan -- borg debug-xxx commands removed, we use "debug xxx" subcommands now, #1627 -- improve metadata handling speed -- shortcut hashindex_set by having hashindex_lookup hint about address -- improve / add progress displays, #1721 -- check for index vs. segment files object count mismatch -- make RPC protocol more extensible: use named parameters. -- RemoteRepository: misc. code cleanups / refactors -- clarify cache/repository README file - -- docs: - - - quickstart: add a comment about other (remote) filesystems - - quickstart: only give one possible ssh url syntax, all others are - documented in usage chapter. - - mention file:// - - document repo URLs / archive location - - clarify borg diff help, #980 - - deployment: synthesize alternative --restrict-to-path example - - improve cache / index docs, esp. files cache docs, #1825 - - document using "git merge 1.0-maint -s recursive -X rename-threshold=20%" - for avoiding troubles when merging the 1.0-maint branch into master. - -- tests: - - - fuse tests: catch ENOTSUP on freebsd - - fuse tests: test troublesome xattrs last - - fix byte range error in test, #1740 - - use monkeypatch to set env vars, but only on pytest based tests. - - point XDG_*_HOME to temp dirs for tests, #1714 - - remove all BORG_* env vars from the outer environment - - Version 1.1.0b2 (2016-10-01) ---------------------------- From 742bfa33c48122e212e369ecdfb44f1219f93e09 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 21 Dec 2016 00:36:31 +0100 Subject: [PATCH 0508/1387] flake8 --- src/borg/key.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/borg/key.py b/src/borg/key.py index a623dd94..b1f2b8b1 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -148,6 +148,7 @@ class KeyBase: def id_hash(self, data): """Return HMAC hash using the "id" HMAC key """ + def compress(self, chunk): compr_args, chunk = self.compression_decider2.decide(chunk) compressor = Compressor(**compr_args) From dc5f954afa6b48f8fca15ad2fb2bc419ad12b852 Mon Sep 17 00:00:00 2001 From: Nathan Musoke Date: Thu, 8 Dec 2016 23:39:34 +1300 Subject: [PATCH 0509/1387] Update link to security contact Information about security contact has moved. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 6655e77a..2a08408d 100644 --- a/README.rst +++ b/README.rst @@ -125,7 +125,7 @@ Links * `Web-Chat (IRC) `_ and `Mailing List `_ * `License `_ -* `Security contact `_ +* `Security contact `_ Compatibility notes ------------------- From fd587331d7f1d78b7db5a366d2ee5ffa0f92d148 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 22 Dec 2016 15:21:42 +0100 Subject: [PATCH 0510/1387] Update link to security contact Currently this move was only done in master, so point to latest. Might be generally a good idea in case it changes and no release was done since the responsibility changed. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2a08408d..41765b80 100644 --- a/README.rst +++ b/README.rst @@ -125,7 +125,7 @@ Links * `Web-Chat (IRC) `_ and `Mailing List `_ * `License `_ -* `Security contact `_ +* `Security contact `_ Compatibility notes ------------------- From 7d48878cfeea27e0f7955ddc08de9d23d1d469fd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 24 Dec 2016 02:53:44 +0100 Subject: [PATCH 0511/1387] borg init --encryption - remove default use --encryption=repokey for all the tests except if they have special needs. --- src/borg/archiver.py | 46 ++++++--- src/borg/testsuite/archiver.py | 164 +++++++++++++++++---------------- 2 files changed, 114 insertions(+), 96 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e5bc3a8f..e8071dc8 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1629,21 +1629,22 @@ class Archiver: This command initializes an empty repository. A repository is a filesystem directory containing the deduplicated data from zero or more archives. - Encryption can be enabled at repository init time (the default). + Encryption can be enabled at repository init time. - It is not recommended to disable encryption. Repository encryption protects you - e.g. against the case that an attacker has access to your backup repository. + It is not recommended to work without encryption. Repository encryption protects + you e.g. against the case that an attacker has access to your backup repository. But be careful with the key / the passphrase: - If you want "passphrase-only" security, use the repokey mode. The key will - be stored inside the repository (in its "config" file). In above mentioned - attack scenario, the attacker will have the key (but not the passphrase). + If you want "passphrase-only" security, use one of the repokey modes. The + key will be stored inside the repository (in its "config" file). In above + mentioned attack scenario, the attacker will have the key (but not the + passphrase). - If you want "passphrase and having-the-key" security, use the keyfile mode. - The key will be stored in your home directory (in .config/borg/keys). In - the attack scenario, the attacker who has just access to your repo won't have - the key (and also not the passphrase). + If you want "passphrase and having-the-key" security, use one of the keyfile + modes. The key will be stored in your home directory (in .config/borg/keys). + In the attack scenario, the attacker who has just access to your repo won't + have the key (and also not the passphrase). Make a backup copy of the key file (keyfile mode) or repo config file (repokey mode) and keep it at a safe place, so you still have the key in @@ -1674,15 +1675,30 @@ class Archiver: repokey and keyfile use AES-CTR-256 for encryption and HMAC-SHA256 for authentication in an encrypt-then-MAC (EtM) construction. The chunk ID hash is HMAC-SHA256 as well (with a separate key). + These modes are compatible with borg 1.0.x. - repokey-blake2 and keyfile-blake2 use the same authenticated encryption, but - use a keyed BLAKE2b-256 hash for the chunk ID hash. + repokey-blake2 and keyfile-blake2 are also authenticated encryption modes, + but use BLAKE2b-256 instead of HMAC-SHA256 for authentication. The chunk ID + hash is a keyed BLAKE2b-256 hash. + These modes are new and not compatible with borg 1.0.x. "authenticated" mode uses no encryption, but authenticates repository contents - through the same keyed BLAKE2b-256 hash as the other blake2 modes. - The key is stored like repokey. + through the same keyed BLAKE2b-256 hash as the other blake2 modes (it uses it + as chunk ID hash). The key is stored like repokey. + This mode is new and not compatible with borg 1.0.x. + + "none" mode uses no encryption and no authentication. It uses sha256 as chunk + ID hash. Not recommended, rather consider using an authenticated or + authenticated/encrypted mode. + This mode is compatible with borg 1.0.x. Hardware acceleration will be used automatically. + + On modern Intel/AMD CPUs (except very cheap ones), AES is usually hw + accelerated. BLAKE2b is faster than sha256 on Intel/AMD 64bit CPUs. + + On modern ARM CPUs, NEON provides hw acceleration for sha256 making it faster + than BLAKE2b-256 there. """) subparser = subparsers.add_parser('init', parents=[common_parser], add_help=False, description=self.do_init.__doc__, epilog=init_epilog, @@ -1694,7 +1710,7 @@ class Archiver: help='repository to create') subparser.add_argument('-e', '--encryption', dest='encryption', choices=('none', 'keyfile', 'repokey', 'keyfile-blake2', 'repokey-blake2', 'authenticated'), - default='repokey', + default=None, help='select encryption key mode (default: "%(default)s")') subparser.add_argument('-a', '--append-only', dest='append_only', action='store_true', help='create an append-only mode repository') diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index b3c702d7..2d97ce06 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -343,7 +343,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_basic_functionality(self): have_root = self.create_test_files() # fork required to test show-rc output - output = self.cmd('init', '--show-version', '--show-rc', self.repository_location, fork=True) + output = self.cmd('init', '--encryption=repokey', '--show-version', '--show-rc', self.repository_location, fork=True) self.assert_in('borgbackup version', output) self.assert_in('terminating with success status, rc 0', output) self.cmd('create', self.repository_location + '::test', 'input') @@ -404,7 +404,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_equal(filter(info_output), filter(info_output2)) def test_unix_socket(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.bind(os.path.join(self.input_path, 'unix-socket')) @@ -422,7 +422,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @pytest.mark.skipif(not are_symlinks_supported(), reason='symlinks not supported') def test_symlink_extract(self): self.create_test_files() - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') with changedir('output'): self.cmd('extract', self.repository_location + '::test') @@ -449,7 +449,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): atime, mtime = 123456780, 234567890 have_noatime = has_noatime('input/file1') os.utime('input/file1', (atime, mtime)) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') with changedir('output'): self.cmd('extract', self.repository_location + '::test') @@ -513,7 +513,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): if sparse_support: # we could create a sparse input file, so creating a backup of it and # extracting it again (as sparse) should also work: - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') with changedir(self.output_path): self.cmd('extract', '--sparse', self.repository_location + '::test') @@ -532,7 +532,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): filename = os.path.join(self.input_path, filename) with open(filename, 'wb'): pass - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') for filename in filenames: with changedir('output'): @@ -600,7 +600,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('create', self.repository_location + '_encrypted::test.2', 'input') def test_repository_move(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) repository_id = bin_to_hex(self._extract_repository_id(self.repository_path)) os.rename(self.repository_path, self.repository_path + '_new') with environment_variable(BORG_RELOCATED_REPO_ACCESS_IS_OK='yes'): @@ -619,7 +619,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert os.path.exists(os.path.join(security_dir, file)) def test_security_dir_compat(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) repository_id = bin_to_hex(self._extract_repository_id(self.repository_path)) security_dir = get_security_dir(repository_id) with open(os.path.join(security_dir, 'location'), 'w') as fd: @@ -651,7 +651,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('info', self.repository_location) def test_strip_components(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('dir/file') self.cmd('create', self.repository_location + '::test', 'input') with changedir('output'): @@ -680,7 +680,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.link(os.path.join(self.input_path, 'dir1/source2'), os.path.join(self.input_path, 'dir1/aaaa')) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') @pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported') @@ -710,7 +710,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert os.stat('input/dir1/hardlink').st_nlink == 4 def test_extract_include_exclude(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('file3', size=1024 * 80) @@ -727,7 +727,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3']) def test_extract_include_exclude_regex(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('file3', size=1024 * 80) @@ -760,7 +760,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_equal(sorted(os.listdir('output/input')), ['file3']) def test_extract_include_exclude_regex_from_file(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('file3', size=1024 * 80) @@ -800,7 +800,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_equal(sorted(os.listdir('output/input')), ['file3']) def test_extract_with_pattern(self): - self.cmd("init", self.repository_location) + self.cmd("init", '--encryption=repokey', self.repository_location) self.create_regular_file("file1", size=1024 * 80) self.create_regular_file("file2", size=1024 * 80) self.create_regular_file("file3", size=1024 * 80) @@ -833,7 +833,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"]) def test_extract_list_output(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file', size=1024 * 80) self.cmd('create', self.repository_location + '::test', 'input') @@ -858,7 +858,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in("input/file", output) def test_extract_progress(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file', size=1024 * 80) self.cmd('create', self.repository_location + '::test', 'input') @@ -867,7 +867,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert 'Extracting:' in output def _create_test_caches(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('cache1/%s' % CACHE_TAG_NAME, contents=CACHE_TAG_CONTENTS + b' extra stuff') @@ -894,7 +894,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self._assert_test_caches() def _create_test_tagged(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('tagged1/.NOBACKUP') self.create_regular_file('tagged2/00-NOBACKUP') @@ -918,7 +918,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self._assert_test_tagged() def _create_test_keep_tagged(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file0', size=1024) self.create_regular_file('tagged1/.NOBACKUP1') self.create_regular_file('tagged1/file1', size=1024) @@ -970,7 +970,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): capabilities = b'\x01\x00\x00\x02\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' self.create_regular_file('file') xattr.setxattr('input/file', 'security.capability', capabilities) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') with changedir('output'): with patch.object(os, 'fchown', patched_fchown): @@ -978,7 +978,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert xattr.getxattr('input/file', 'security.capability') == capabilities def test_path_normalization(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('dir1/dir2/file', size=1024 * 80) with changedir('input/dir1/dir2'): self.cmd('create', self.repository_location + '::test', '../../../input/dir1/../dir1/dir2/..') @@ -987,7 +987,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in(' input/dir1/dir2/file', output) def test_exclude_normalization(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) with changedir('input'): @@ -1007,13 +1007,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_repeated_files(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input', 'input') def test_overwrite(self): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('dir2/file2', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') # Overwriting regular files and directories should be supported os.mkdir('output/input') @@ -1032,7 +1032,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_rename(self): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('dir2/file2', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') self.cmd('create', self.repository_location + '::test.2', 'input') self.cmd('extract', '--dry-run', self.repository_location + '::test') @@ -1051,7 +1051,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_info(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') info_repo = self.cmd('info', self.repository_location) assert 'All archives:' in info_repo @@ -1062,7 +1062,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_comment(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test1', 'input') self.cmd('create', '--comment', 'this is the comment', self.repository_location + '::test2', 'input') self.cmd('create', '--comment', '"deleted" comment', self.repository_location + '::test3', 'input') @@ -1082,7 +1082,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_delete(self): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('dir2/file2', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') self.cmd('create', self.repository_location + '::test.2', 'input') self.cmd('create', self.repository_location + '::test.3', 'input') @@ -1103,7 +1103,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_delete_repo(self): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('dir2/file2', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') self.cmd('create', self.repository_location + '::test.2', 'input') os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'no' @@ -1115,7 +1115,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assertFalse(os.path.exists(self.repository_path)) def test_corrupted_repository(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_src_archive('test') self.cmd('extract', '--dry-run', self.repository_location + '::test') output = self.cmd('check', '--show-version', self.repository_location) @@ -1132,7 +1132,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # we currently need to be able to create a lock directory inside the repo: @pytest.mark.xfail(reason="we need to be able to create the lock directory inside the repo") def test_readonly_repository(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_src_archive('test') os.system('chmod -R ugo-w ' + self.repository_path) try: @@ -1144,13 +1144,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): @pytest.mark.skipif('BORG_TESTS_IGNORE_MODES' in os.environ, reason='modes unreliable') def test_umask(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') mode = os.stat(self.repository_path).st_mode self.assertEqual(stat.S_IMODE(mode), 0o700) def test_create_dry_run(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', '--dry-run', self.repository_location + '::test', 'input') # Make sure no archive has been created with Repository(self.repository_path) as repository: @@ -1159,13 +1159,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_progress_on(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) output = self.cmd('create', '--progress', self.repository_location + '::test4', 'input') self.assert_in("\r", output) def test_progress_off(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) output = self.cmd('create', self.repository_location + '::test5', 'input') self.assert_not_in("\r", output) @@ -1177,7 +1177,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) os.utime('input/file1', (now - 5, now - 5)) # 5 seconds ago self.create_regular_file('file2', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) output = self.cmd('create', '--list', self.repository_location + '::test', 'input') self.assert_in("A input/file1", output) self.assert_in("A input/file2", output) @@ -1198,7 +1198,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): if has_lchflags: self.create_regular_file('file3', size=1024 * 80) platform.set_flags(os.path.join(self.input_path, 'file3'), stat.UF_NODUMP) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) output = self.cmd('create', '--list', self.repository_location + '::test', 'input') self.assert_in("A input/file1", output) self.assert_in("A input/file2", output) @@ -1216,7 +1216,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) os.utime('input/file1', (now-5, now-5)) self.create_regular_file('file2', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) # no listing by default output = self.cmd('create', self.repository_location + '::test', 'input') self.assert_not_in('file1', output) @@ -1237,7 +1237,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_read_special_broken_symlink(self): os.symlink('somewhere doesnt exist', os.path.join(self.input_path, 'link')) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) archive = self.repository_location + '::test' self.cmd('create', '--read-special', archive, 'input') output = self.cmd('list', archive) @@ -1245,13 +1245,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): # def test_cmdline_compatibility(self): # self.create_regular_file('file1', size=1024 * 80) - # self.cmd('init', self.repository_location) + # self.cmd('init', '--encryption=repokey', self.repository_location) # self.cmd('create', self.repository_location + '::test', 'input') # output = self.cmd('foo', self.repository_location, '--old') # self.assert_in('"--old" has been deprecated. Use "--new" instead', output) def test_prune_repository(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test1', src_dir) self.cmd('create', self.repository_location + '::test2', src_dir) # these are not really a checkpoints, but they look like some: @@ -1290,7 +1290,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('test5', output) def test_prune_repository_save_space(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test1', src_dir) self.cmd('create', self.repository_location + '::test2', src_dir) output = self.cmd('prune', '--list', '--stats', '--dry-run', self.repository_location, '--keep-daily=2') @@ -1306,7 +1306,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('test2', output) def test_prune_repository_prefix(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::foo-2015-08-12-10:00', src_dir) self.cmd('create', self.repository_location + '::foo-2015-08-12-20:00', src_dir) self.cmd('create', self.repository_location + '::bar-2015-08-12-10:00', src_dir) @@ -1327,7 +1327,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('bar-2015-08-12-20:00', output) def test_list_prefix(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test-1', src_dir) self.cmd('create', self.repository_location + '::something-else-than-test-1', src_dir) self.cmd('create', self.repository_location + '::test-2', src_dir) @@ -1337,7 +1337,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_not_in('something-else', output) def test_list_format(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) test_archive = self.repository_location + '::test' self.cmd('create', test_archive, src_dir) output_warn = self.cmd('list', '--list-format', '-', test_archive) @@ -1349,7 +1349,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assertNotEqual(output_1, output_3) def test_list_repository_format(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test-1', src_dir) self.cmd('create', self.repository_location + '::test-2', src_dir) output_1 = self.cmd('list', self.repository_location) @@ -1363,7 +1363,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_list_hash(self): self.create_regular_file('empty_file', size=0) self.create_regular_file('amb', contents=b'a' * 1000000) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) test_archive = self.repository_location + '::test' self.cmd('create', test_archive, 'input') output = self.cmd('list', '--format', '{sha256} {path}{NL}', test_archive) @@ -1376,7 +1376,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with open(os.path.join(self.input_path, 'two_chunks'), 'wb') as fd: fd.write(b'abba' * 2000000) fd.write(b'baab' * 2000000) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) test_archive = self.repository_location + '::test' self.cmd('create', test_archive, 'input') output = self.cmd('list', '--format', '{num_chunks} {unique_chunks} {path}{NL}', test_archive) @@ -1385,7 +1385,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_list_size(self): self.create_regular_file('compressible_file', size=10000) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) test_archive = self.repository_location + '::test' self.cmd('create', '-C', 'lz4', test_archive, 'input') output = self.cmd('list', '--format', '{size} {csize} {path}{NL}', test_archive) @@ -1451,7 +1451,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert csize >= size def test_change_passphrase(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) os.environ['BORG_NEW_PASSPHRASE'] = 'newpassphrase' # here we have both BORG_PASSPHRASE and BORG_NEW_PASSPHRASE set: self.cmd('change-passphrase', self.repository_location) @@ -1459,7 +1459,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('list', self.repository_location) def test_break_lock(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('break-lock', self.repository_location) def test_usage(self): @@ -1490,7 +1490,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): noatime_used = flags_noatime != flags_normal return noatime_used and atime_before == atime_after - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_test_files() have_noatime = has_noatime('input/file1') self.cmd('create', self.repository_location + '::archive', 'input') @@ -1576,7 +1576,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(has_llfuse, 'llfuse not installed') def test_fuse_versions_view(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('test', contents=b'first') if are_hardlinks_supported(): self.create_regular_file('hardlink1', contents=b'') @@ -1598,7 +1598,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(has_llfuse, 'llfuse not installed') def test_fuse_allow_damaged_files(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_src_archive('archive') # Get rid of a chunk and repair it archive, repository = self.open_archive('archive') @@ -1623,7 +1623,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(has_llfuse, 'llfuse not installed') def test_fuse_mount_options(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_src_archive('arch11') self.create_src_archive('arch12') self.create_src_archive('arch21') @@ -1679,7 +1679,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_debug_dump_archive_items(self): self.create_test_files() - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') with changedir('output'): output = self.cmd('debug', 'dump-archive-items', self.repository_location + '::test') @@ -1689,7 +1689,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_debug_dump_repo_objs(self): self.create_test_files() - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') with changedir('output'): output = self.cmd('debug', 'dump-repo-objs', self.repository_location) @@ -1698,7 +1698,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert 'Done.' in output def test_debug_put_get_delete_obj(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) data = b'some data' hexkey = sha256(data).hexdigest() self.create_regular_file('file', contents=data) @@ -1721,7 +1721,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): raise EOFError with patch.object(KeyfileKeyBase, 'create', raise_eof): - self.cmd('init', self.repository_location, exit_code=1) + self.cmd('init', '--encryption=repokey', self.repository_location, exit_code=1) assert not os.path.exists(self.repository_location) def check_cache(self): @@ -1747,7 +1747,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert id in seen def test_check_cache(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') with self.open_repository() as repository: manifest, key = Manifest.load(repository) @@ -1759,13 +1759,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.check_cache() def test_recreate_target_rc(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) output = self.cmd('recreate', self.repository_location, '--target=asdf', exit_code=2) assert 'Need to specify single archive' in output def test_recreate_target(self): self.create_test_files() - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.check_cache() archive = self.repository_location + '::test0' self.cmd('create', archive, 'input') @@ -1786,7 +1786,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_basic(self): self.create_test_files() self.create_regular_file('dir2/file3', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) archive = self.repository_location + '::test0' self.cmd('create', archive, 'input') self.cmd('recreate', archive, 'input/dir2', '-e', 'input/dir2/file3') @@ -1817,7 +1817,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with open(os.path.join(self.input_path, 'large_file'), 'wb') as fd: fd.write(b'a' * 280) fd.write(b'b' * 280) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', '--chunker-params', '7,9,8,128', self.repository_location + '::test1', 'input') self.cmd('create', self.repository_location + '::test2', 'input', '--no-files-cache') list = self.cmd('list', self.repository_location + '::test1', 'input/large_file', @@ -1834,7 +1834,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_recompress(self): self.create_regular_file('compressible', size=10000) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input', '-C', 'none') file_list = self.cmd('list', self.repository_location + '::test', 'input/compressible', '--format', '{size} {csize} {sha256}') @@ -1850,7 +1850,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_dry_run(self): self.create_regular_file('compressible', size=10000) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') archives_before = self.cmd('list', self.repository_location + '::test') self.cmd('recreate', self.repository_location, '-n', '-e', 'input/compressible') @@ -1860,7 +1860,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_skips_nothing_to_do(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') info_before = self.cmd('info', self.repository_location + '::test') self.cmd('recreate', self.repository_location, '--chunker-params', 'default') @@ -1869,13 +1869,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert info_before == info_after # includes archive ID def test_with_lock(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) lock_path = os.path.join(self.repository_path, 'lock.exclusive') cmd = 'python3', '-c', 'import os, sys; sys.exit(42 if os.path.exists("%s") else 23)' % lock_path self.cmd('with-lock', self.repository_location, *cmd, fork=True, exit_code=42) def test_recreate_list_output(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file1', size=0) self.create_regular_file('file2', size=0) self.create_regular_file('file3', size=0) @@ -1905,7 +1905,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_not_in("x input/file5", output) def test_bad_filters(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') self.cmd('delete', '--first', '1', '--last', '1', self.repository_location, fork=True, exit_code=2) @@ -2045,7 +2045,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): def setUp(self): super().setUp() with patch.object(ChunkBuffer, 'BUFFER_SIZE', 10): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_src_archive('archive1') self.create_src_archive('archive2') @@ -2295,7 +2295,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): repository.commit() def test_fresh_init_tam_required(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) repository = Repository(self.repository_path, exclusive=True) with repository: manifest, key = Manifest.load(repository) @@ -2310,7 +2310,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): self.cmd('list', self.repository_location) def test_not_required(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_src_archive('archive1234') repository = Repository(self.repository_path, exclusive=True) with repository: @@ -2342,7 +2342,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): self.cmd('list', self.repository_location) def test_disable(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_src_archive('archive1234') self.cmd('upgrade', '--disable-tam', self.repository_location) repository = Repository(self.repository_path, exclusive=True) @@ -2350,7 +2350,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): assert not self.cmd('list', self.repository_location) def test_disable2(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_src_archive('archive1234') repository = Repository(self.repository_path, exclusive=True) self.spoof_manifest(repository) @@ -2368,14 +2368,16 @@ class RemoteArchiverTestCase(ArchiverTestCase): def test_remote_repo_restrict_to_path(self): # restricted to repo directory itself: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', self.repository_path]): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) # restricted to repo directory itself, fail for other directories with same prefix: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', self.repository_path]): - self.assert_raises(PathNotAllowed, lambda: self.cmd('init', self.repository_location + '_0')) + self.assert_raises(PathNotAllowed, + lambda: self.cmd('init', '--encryption=repokey', self.repository_location + '_0')) # restricted to a completely different path: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo']): - self.assert_raises(PathNotAllowed, lambda: self.cmd('init', self.repository_location + '_1')) + self.assert_raises(PathNotAllowed, + lambda: self.cmd('init', '--encryption=repokey', self.repository_location + '_1')) path_prefix = os.path.dirname(self.repository_path) # restrict to repo directory's parent directory: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', path_prefix]): @@ -2389,7 +2391,7 @@ class RemoteArchiverTestCase(ArchiverTestCase): pass def test_strip_components_doesnt_leak(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('dir/file', contents=b"test file contents 1") self.create_regular_file('dir/file2', contents=b"test file contents 2") self.create_regular_file('skipped-file1', contents=b"test file contents 3") @@ -2415,7 +2417,7 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): def test_basic_functionality(self): # Initialize test folder self.create_test_files() - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) # Setup files for the first snapshot self.create_regular_file('file_unchanged', size=128) @@ -2537,7 +2539,7 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): do_asserts(self.cmd('diff', self.repository_location + '::test0', 'test1b', exit_code=1), '1b') def test_sort_option(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('a_file_removed', size=8) self.create_regular_file('f_file_removed', size=16) From 0ff76bdc9df99c0d0e2d5ea6d82eb6d99178bb13 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 26 Dec 2016 15:29:22 +0100 Subject: [PATCH 0512/1387] dump a trace on SIGUSR2 --- borg/archiver.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/borg/archiver.py b/borg/archiver.py index 8a243f89..6fa6c575 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -3,6 +3,7 @@ from datetime import datetime from hashlib import sha256 from operator import attrgetter import argparse +import faulthandler import functools import inspect import os @@ -2021,6 +2022,11 @@ def sig_info_handler(sig_no, stack): # pragma: no cover break +def sig_trace_handler(sig_no, stack): # pragma: no cover + print('\nReceived SIGUSR2 at %s, dumping trace...' % datetime.now().replace(microsecond=0), file=sys.stderr) + faulthandler.dump_traceback() + + def main(): # pragma: no cover # Make sure stdout and stderr have errors='replace') to avoid unicode # issues when print()-ing unicode file names @@ -2036,6 +2042,7 @@ def main(): # pragma: no cover signal_handler('SIGHUP', raising_signal_handler(SigHup)), \ signal_handler('SIGTERM', raising_signal_handler(SigTerm)), \ signal_handler('SIGUSR1', sig_info_handler), \ + signal_handler('SIGUSR2', sig_trace_handler), \ signal_handler('SIGINFO', sig_info_handler): archiver = Archiver() msg = None From c2c31aa13a3b7d59bbd319fa24cbb7fcf33daa9a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 26 Dec 2016 15:29:30 +0100 Subject: [PATCH 0513/1387] enable faulthandler --- borg/archiver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/borg/archiver.py b/borg/archiver.py index 6fa6c575..7ad2195d 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -2038,6 +2038,9 @@ def main(): # pragma: no cover # SIGHUP is important especially for systemd systems, where logind # sends it when a session exits, in addition to any traditional use. # Output some info if we receive SIGUSR1 or SIGINFO (ctrl-t). + + # Register fault handler for SIGSEGV, SIGFPE, SIGABRT, SIGBUS and SIGILL. + faulthandler.enable() with signal_handler('SIGINT', raising_signal_handler(KeyboardInterrupt)), \ signal_handler('SIGHUP', raising_signal_handler(SigHup)), \ signal_handler('SIGTERM', raising_signal_handler(SigTerm)), \ From f043b966dae7ca6e6cb0257a215723400dbd19ca Mon Sep 17 00:00:00 2001 From: Abogical Date: Sat, 31 Dec 2016 16:53:30 +0200 Subject: [PATCH 0514/1387] It's 2017 --- LICENSE | 2 +- docs/conf.py | 2 +- src/borg/shellpattern.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 251e7027..e9940124 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2015-2016 The Borg Collective (see AUTHORS file) +Copyright (C) 2015-2017 The Borg Collective (see AUTHORS file) Copyright (C) 2010-2014 Jonas Borgström All rights reserved. diff --git a/docs/conf.py b/docs/conf.py index 1910e3a7..8e51e4ea 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,7 @@ master_doc = 'index' # General information about the project. project = 'Borg - Deduplicating Archiver' -copyright = '2010-2014 Jonas Borgström, 2015-2016 The Borg Collective (see AUTHORS file)' +copyright = '2010-2014 Jonas Borgström, 2015-2017 The Borg Collective (see AUTHORS file)' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/src/borg/shellpattern.py b/src/borg/shellpattern.py index e0ead38c..b8d95834 100644 --- a/src/borg/shellpattern.py +++ b/src/borg/shellpattern.py @@ -12,7 +12,7 @@ def translate(pat): This function is derived from the "fnmatch" module distributed with the Python standard library. - Copyright (C) 2001-2016 Python Software Foundation. All rights reserved. + Copyright (C) 2001-2017 Python Software Foundation. All rights reserved. TODO: support {alt1,alt2} shell-style alternatives From 9afebead8476566f602f4a9bd33d9cf3d64fe44b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Dec 2016 17:54:46 +0100 Subject: [PATCH 0515/1387] two fast CRC implementations CRC slice by 8 for generic CPUs outperforms zlib CRC32 on ppc and x86 (ARM untested but expected to as well). PCLMULQDQ derived from Intel's zlib patches outperforms every other CRC implementation by a huge margin. --- .gitignore | 2 + setup.py | 7 +- src/borg/_crc32/clmul.c | 518 +++++++++++++++++++++++++++++++++++ src/borg/_crc32/crc32.c | 103 +++++++ src/borg/_crc32/slice_by_8.c | 427 +++++++++++++++++++++++++++++ src/borg/archiver.py | 4 + src/borg/crc32.pyx | 41 +++ src/borg/repository.py | 2 +- src/borg/testsuite/crc32.py | 21 ++ 9 files changed, 1123 insertions(+), 2 deletions(-) create mode 100644 src/borg/_crc32/clmul.c create mode 100644 src/borg/_crc32/crc32.c create mode 100644 src/borg/_crc32/slice_by_8.c create mode 100644 src/borg/crc32.pyx create mode 100644 src/borg/testsuite/crc32.py diff --git a/.gitignore b/.gitignore index f4644237..e5ec9ae7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ hashindex.c chunker.c compress.c crypto.c +item.c +src/borg/crc32.c src/borg/platform/darwin.c src/borg/platform/freebsd.c src/borg/platform/linux.c diff --git a/setup.py b/setup.py index c6e27f4d..26367ef1 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ crypto_source = 'src/borg/crypto.pyx' chunker_source = 'src/borg/chunker.pyx' hashindex_source = 'src/borg/hashindex.pyx' item_source = 'src/borg/item.pyx' +crc32_source = 'src/borg/crc32.pyx' platform_posix_source = 'src/borg/platform/posix.pyx' platform_linux_source = 'src/borg/platform/linux.pyx' platform_darwin_source = 'src/borg/platform/darwin.pyx' @@ -86,6 +87,8 @@ try: 'src/borg/chunker.c', 'src/borg/_chunker.c', 'src/borg/hashindex.c', 'src/borg/_hashindex.c', 'src/borg/item.c', + 'src/borg/crc32.c', + 'src/borg/_crc32/crc32.c', 'src/borg/_crc32/clmul.c', 'src/borg/_crc32/slice_by_8.c', 'src/borg/platform/posix.c', 'src/borg/platform/linux.c', 'src/borg/platform/freebsd.c', @@ -103,13 +106,14 @@ except ImportError: chunker_source = chunker_source.replace('.pyx', '.c') hashindex_source = hashindex_source.replace('.pyx', '.c') item_source = item_source.replace('.pyx', '.c') + crc32_source = crc32_source.replace('.pyx', '.c') platform_posix_source = platform_posix_source.replace('.pyx', '.c') platform_linux_source = platform_linux_source.replace('.pyx', '.c') platform_freebsd_source = platform_freebsd_source.replace('.pyx', '.c') platform_darwin_source = platform_darwin_source.replace('.pyx', '.c') from distutils.command.build_ext import build_ext if not on_rtd and not all(os.path.exists(path) for path in [ - compress_source, crypto_source, chunker_source, hashindex_source, item_source, + compress_source, crypto_source, chunker_source, hashindex_source, item_source, crc32_source, platform_posix_source, platform_linux_source, platform_freebsd_source, platform_darwin_source]): raise ImportError('The GIT version of Borg needs Cython. Install Cython or use a released version.') @@ -367,6 +371,7 @@ if not on_rtd: Extension('borg.chunker', [chunker_source]), Extension('borg.hashindex', [hashindex_source]), Extension('borg.item', [item_source]), + Extension('borg.crc32', [crc32_source]), ] if sys.platform.startswith(('linux', 'freebsd', 'darwin')): ext_modules.append(Extension('borg.platform.posix', [platform_posix_source])) diff --git a/src/borg/_crc32/clmul.c b/src/borg/_crc32/clmul.c new file mode 100644 index 00000000..64fa613a --- /dev/null +++ b/src/borg/_crc32/clmul.c @@ -0,0 +1,518 @@ +/* + * Compute the CRC32 using a parallelized folding approach with the PCLMULQDQ + * instruction. + * + * A white paper describing this algorithm can be found at: + * http://www.intel.com/content/dam/www/public/us/en/documents/white-papers/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf + * + * Copyright (C) 2013 Intel Corporation. All rights reserved. + * Authors: + * Wajdi Feghali + * Jim Guilford + * Vinodh Gopal + * Erdinc Ozturk + * Jim Kukunas + * + * For conditions of distribution and use, see copyright notice in zlib.h + * + * Copyright (c) 2016 Marian Beermann (add support for initial value, restructuring) + */ + +#include +#include +#include + +#ifdef _MSC_VER +#include +#else +/* + * Newer versions of GCC and clang come with cpuid.h + * (ftr GCC 4.7 in Debian Wheezy has this) + */ +#include +#endif + +static void +cpuid(int info, unsigned* eax, unsigned* ebx, unsigned* ecx, unsigned* edx) +{ +#ifdef _MSC_VER + unsigned int registers[4]; + __cpuid(registers, info); + *eax = registers[0]; + *ebx = registers[1]; + *ecx = registers[2]; + *edx = registers[3]; +#else + /* GCC, clang */ + unsigned int _eax; + unsigned int _ebx; + unsigned int _ecx; + unsigned int _edx; + __cpuid(info, _eax, _ebx, _ecx, _edx); + *eax = _eax; + *ebx = _ebx; + *ecx = _ecx; + *edx = _edx; +#endif +} + +static int +have_clmul(void) +{ + unsigned eax, ebx, ecx, edx; + cpuid(1 /* feature bits */, &eax, &ebx, &ecx, &edx); + + int has_pclmulqdq = ecx & 0x2; /* bit 1 */ + int has_sse41 = ecx & 0x80000; /* bit 19 */ + + return has_pclmulqdq && has_sse41; +} + +CLMUL +static void +fold_1(__m128i *xmm_crc0, __m128i *xmm_crc1, __m128i *xmm_crc2, __m128i *xmm_crc3) +{ + const __m128i xmm_fold4 = _mm_set_epi32( + 0x00000001, 0x54442bd4, + 0x00000001, 0xc6e41596); + + __m128i x_tmp3; + __m128 ps_crc0, ps_crc3, ps_res; + + x_tmp3 = *xmm_crc3; + + *xmm_crc3 = *xmm_crc0; + *xmm_crc0 = _mm_clmulepi64_si128(*xmm_crc0, xmm_fold4, 0x01); + *xmm_crc3 = _mm_clmulepi64_si128(*xmm_crc3, xmm_fold4, 0x10); + ps_crc0 = _mm_castsi128_ps(*xmm_crc0); + ps_crc3 = _mm_castsi128_ps(*xmm_crc3); + ps_res = _mm_xor_ps(ps_crc0, ps_crc3); + + *xmm_crc0 = *xmm_crc1; + *xmm_crc1 = *xmm_crc2; + *xmm_crc2 = x_tmp3; + *xmm_crc3 = _mm_castps_si128(ps_res); +} + +CLMUL +static void +fold_2(__m128i *xmm_crc0, __m128i *xmm_crc1, __m128i *xmm_crc2, __m128i *xmm_crc3) +{ + const __m128i xmm_fold4 = _mm_set_epi32( + 0x00000001, 0x54442bd4, + 0x00000001, 0xc6e41596); + + __m128i x_tmp3, x_tmp2; + __m128 ps_crc0, ps_crc1, ps_crc2, ps_crc3, ps_res31, ps_res20; + + x_tmp3 = *xmm_crc3; + x_tmp2 = *xmm_crc2; + + *xmm_crc3 = *xmm_crc1; + *xmm_crc1 = _mm_clmulepi64_si128(*xmm_crc1, xmm_fold4, 0x01); + *xmm_crc3 = _mm_clmulepi64_si128(*xmm_crc3, xmm_fold4, 0x10); + ps_crc3 = _mm_castsi128_ps(*xmm_crc3); + ps_crc1 = _mm_castsi128_ps(*xmm_crc1); + ps_res31 = _mm_xor_ps(ps_crc3, ps_crc1); + + *xmm_crc2 = *xmm_crc0; + *xmm_crc0 = _mm_clmulepi64_si128(*xmm_crc0, xmm_fold4, 0x01); + *xmm_crc2 = _mm_clmulepi64_si128(*xmm_crc2, xmm_fold4, 0x10); + ps_crc0 = _mm_castsi128_ps(*xmm_crc0); + ps_crc2 = _mm_castsi128_ps(*xmm_crc2); + ps_res20 = _mm_xor_ps(ps_crc0, ps_crc2); + + *xmm_crc0 = x_tmp2; + *xmm_crc1 = x_tmp3; + *xmm_crc2 = _mm_castps_si128(ps_res20); + *xmm_crc3 = _mm_castps_si128(ps_res31); +} + +CLMUL +static void +fold_3(__m128i *xmm_crc0, __m128i *xmm_crc1, __m128i *xmm_crc2, __m128i *xmm_crc3) +{ + const __m128i xmm_fold4 = _mm_set_epi32( + 0x00000001, 0x54442bd4, + 0x00000001, 0xc6e41596); + + __m128i x_tmp3; + __m128 ps_crc0, ps_crc1, ps_crc2, ps_crc3, ps_res32, ps_res21, ps_res10; + + x_tmp3 = *xmm_crc3; + + *xmm_crc3 = *xmm_crc2; + *xmm_crc2 = _mm_clmulepi64_si128(*xmm_crc2, xmm_fold4, 0x01); + *xmm_crc3 = _mm_clmulepi64_si128(*xmm_crc3, xmm_fold4, 0x10); + ps_crc2 = _mm_castsi128_ps(*xmm_crc2); + ps_crc3 = _mm_castsi128_ps(*xmm_crc3); + ps_res32 = _mm_xor_ps(ps_crc2, ps_crc3); + + *xmm_crc2 = *xmm_crc1; + *xmm_crc1 = _mm_clmulepi64_si128(*xmm_crc1, xmm_fold4, 0x01); + *xmm_crc2 = _mm_clmulepi64_si128(*xmm_crc2, xmm_fold4, 0x10); + ps_crc1 = _mm_castsi128_ps(*xmm_crc1); + ps_crc2 = _mm_castsi128_ps(*xmm_crc2); + ps_res21 = _mm_xor_ps(ps_crc1, ps_crc2); + + *xmm_crc1 = *xmm_crc0; + *xmm_crc0 = _mm_clmulepi64_si128(*xmm_crc0, xmm_fold4, 0x01); + *xmm_crc1 = _mm_clmulepi64_si128(*xmm_crc1, xmm_fold4, 0x10); + ps_crc0 = _mm_castsi128_ps(*xmm_crc0); + ps_crc1 = _mm_castsi128_ps(*xmm_crc1); + ps_res10 = _mm_xor_ps(ps_crc0, ps_crc1); + + *xmm_crc0 = x_tmp3; + *xmm_crc1 = _mm_castps_si128(ps_res10); + *xmm_crc2 = _mm_castps_si128(ps_res21); + *xmm_crc3 = _mm_castps_si128(ps_res32); +} + +CLMUL +static void +fold_4(__m128i *xmm_crc0, __m128i *xmm_crc1, __m128i *xmm_crc2, __m128i *xmm_crc3) +{ + const __m128i xmm_fold4 = _mm_set_epi32( + 0x00000001, 0x54442bd4, + 0x00000001, 0xc6e41596); + + __m128i x_tmp0, x_tmp1, x_tmp2, x_tmp3; + __m128 ps_crc0, ps_crc1, ps_crc2, ps_crc3; + __m128 ps_t0, ps_t1, ps_t2, ps_t3; + __m128 ps_res0, ps_res1, ps_res2, ps_res3; + + x_tmp0 = *xmm_crc0; + x_tmp1 = *xmm_crc1; + x_tmp2 = *xmm_crc2; + x_tmp3 = *xmm_crc3; + + *xmm_crc0 = _mm_clmulepi64_si128(*xmm_crc0, xmm_fold4, 0x01); + x_tmp0 = _mm_clmulepi64_si128(x_tmp0, xmm_fold4, 0x10); + ps_crc0 = _mm_castsi128_ps(*xmm_crc0); + ps_t0 = _mm_castsi128_ps(x_tmp0); + ps_res0 = _mm_xor_ps(ps_crc0, ps_t0); + + *xmm_crc1 = _mm_clmulepi64_si128(*xmm_crc1, xmm_fold4, 0x01); + x_tmp1 = _mm_clmulepi64_si128(x_tmp1, xmm_fold4, 0x10); + ps_crc1 = _mm_castsi128_ps(*xmm_crc1); + ps_t1 = _mm_castsi128_ps(x_tmp1); + ps_res1 = _mm_xor_ps(ps_crc1, ps_t1); + + *xmm_crc2 = _mm_clmulepi64_si128(*xmm_crc2, xmm_fold4, 0x01); + x_tmp2 = _mm_clmulepi64_si128(x_tmp2, xmm_fold4, 0x10); + ps_crc2 = _mm_castsi128_ps(*xmm_crc2); + ps_t2 = _mm_castsi128_ps(x_tmp2); + ps_res2 = _mm_xor_ps(ps_crc2, ps_t2); + + *xmm_crc3 = _mm_clmulepi64_si128(*xmm_crc3, xmm_fold4, 0x01); + x_tmp3 = _mm_clmulepi64_si128(x_tmp3, xmm_fold4, 0x10); + ps_crc3 = _mm_castsi128_ps(*xmm_crc3); + ps_t3 = _mm_castsi128_ps(x_tmp3); + ps_res3 = _mm_xor_ps(ps_crc3, ps_t3); + + *xmm_crc0 = _mm_castps_si128(ps_res0); + *xmm_crc1 = _mm_castps_si128(ps_res1); + *xmm_crc2 = _mm_castps_si128(ps_res2); + *xmm_crc3 = _mm_castps_si128(ps_res3); +} + +static const unsigned ALIGNED_(32) pshufb_shf_table[60] = { + 0x84838281, 0x88878685, 0x8c8b8a89, 0x008f8e8d, /* shl 15 (16 - 1)/shr1 */ + 0x85848382, 0x89888786, 0x8d8c8b8a, 0x01008f8e, /* shl 14 (16 - 3)/shr2 */ + 0x86858483, 0x8a898887, 0x8e8d8c8b, 0x0201008f, /* shl 13 (16 - 4)/shr3 */ + 0x87868584, 0x8b8a8988, 0x8f8e8d8c, 0x03020100, /* shl 12 (16 - 4)/shr4 */ + 0x88878685, 0x8c8b8a89, 0x008f8e8d, 0x04030201, /* shl 11 (16 - 5)/shr5 */ + 0x89888786, 0x8d8c8b8a, 0x01008f8e, 0x05040302, /* shl 10 (16 - 6)/shr6 */ + 0x8a898887, 0x8e8d8c8b, 0x0201008f, 0x06050403, /* shl 9 (16 - 7)/shr7 */ + 0x8b8a8988, 0x8f8e8d8c, 0x03020100, 0x07060504, /* shl 8 (16 - 8)/shr8 */ + 0x8c8b8a89, 0x008f8e8d, 0x04030201, 0x08070605, /* shl 7 (16 - 9)/shr9 */ + 0x8d8c8b8a, 0x01008f8e, 0x05040302, 0x09080706, /* shl 6 (16 -10)/shr10*/ + 0x8e8d8c8b, 0x0201008f, 0x06050403, 0x0a090807, /* shl 5 (16 -11)/shr11*/ + 0x8f8e8d8c, 0x03020100, 0x07060504, 0x0b0a0908, /* shl 4 (16 -12)/shr12*/ + 0x008f8e8d, 0x04030201, 0x08070605, 0x0c0b0a09, /* shl 3 (16 -13)/shr13*/ + 0x01008f8e, 0x05040302, 0x09080706, 0x0d0c0b0a, /* shl 2 (16 -14)/shr14*/ + 0x0201008f, 0x06050403, 0x0a090807, 0x0e0d0c0b /* shl 1 (16 -15)/shr15*/ +}; + +CLMUL +static void +partial_fold(const size_t len, + __m128i *xmm_crc0, __m128i *xmm_crc1, __m128i *xmm_crc2, __m128i *xmm_crc3, + __m128i *xmm_crc_part) +{ + + const __m128i xmm_fold4 = _mm_set_epi32( + 0x00000001, 0x54442bd4, + 0x00000001, 0xc6e41596); + const __m128i xmm_mask3 = _mm_set1_epi32(0x80808080); + + __m128i xmm_shl, xmm_shr, xmm_tmp1, xmm_tmp2, xmm_tmp3; + __m128i xmm_a0_0, xmm_a0_1; + __m128 ps_crc3, psa0_0, psa0_1, ps_res; + + xmm_shl = _mm_load_si128((__m128i *)pshufb_shf_table + (len - 1)); + xmm_shr = xmm_shl; + xmm_shr = _mm_xor_si128(xmm_shr, xmm_mask3); + + xmm_a0_0 = _mm_shuffle_epi8(*xmm_crc0, xmm_shl); + + *xmm_crc0 = _mm_shuffle_epi8(*xmm_crc0, xmm_shr); + xmm_tmp1 = _mm_shuffle_epi8(*xmm_crc1, xmm_shl); + *xmm_crc0 = _mm_or_si128(*xmm_crc0, xmm_tmp1); + + *xmm_crc1 = _mm_shuffle_epi8(*xmm_crc1, xmm_shr); + xmm_tmp2 = _mm_shuffle_epi8(*xmm_crc2, xmm_shl); + *xmm_crc1 = _mm_or_si128(*xmm_crc1, xmm_tmp2); + + *xmm_crc2 = _mm_shuffle_epi8(*xmm_crc2, xmm_shr); + xmm_tmp3 = _mm_shuffle_epi8(*xmm_crc3, xmm_shl); + *xmm_crc2 = _mm_or_si128(*xmm_crc2, xmm_tmp3); + + *xmm_crc3 = _mm_shuffle_epi8(*xmm_crc3, xmm_shr); + *xmm_crc_part = _mm_shuffle_epi8(*xmm_crc_part, xmm_shl); + *xmm_crc3 = _mm_or_si128(*xmm_crc3, *xmm_crc_part); + + xmm_a0_1 = _mm_clmulepi64_si128(xmm_a0_0, xmm_fold4, 0x10); + xmm_a0_0 = _mm_clmulepi64_si128(xmm_a0_0, xmm_fold4, 0x01); + + ps_crc3 = _mm_castsi128_ps(*xmm_crc3); + psa0_0 = _mm_castsi128_ps(xmm_a0_0); + psa0_1 = _mm_castsi128_ps(xmm_a0_1); + + ps_res = _mm_xor_ps(ps_crc3, psa0_0); + ps_res = _mm_xor_ps(ps_res, psa0_1); + + *xmm_crc3 = _mm_castps_si128(ps_res); +} + +static const unsigned ALIGNED_(16) crc_k[] = { + 0xccaa009e, 0x00000000, /* rk1 */ + 0x751997d0, 0x00000001, /* rk2 */ + 0xccaa009e, 0x00000000, /* rk5 */ + 0x63cd6124, 0x00000001, /* rk6 */ + 0xf7011640, 0x00000001, /* rk7 */ + 0xdb710640, 0x00000001 /* rk8 */ +}; + +static const unsigned ALIGNED_(16) crc_mask[4] = { + 0xFFFFFFFF, 0xFFFFFFFF, 0x00000000, 0x00000000 +}; + +static const unsigned ALIGNED_(16) crc_mask2[4] = { + 0x00000000, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF +}; + +#define ONCE(op) if(first) { \ + first = 0; \ + (op); \ +} + +/* + * somewhat surprisingly the "naive" way of doing this, ie. with a flag and a cond. branch, + * is consistently ~5 % faster on average than the implied-recommended branchless way (always xor, + * always zero xmm_initial). Guess speculative execution and branch prediction got the better of + * yet another "optimization tip". + */ +#define XOR_INITIAL(where) ONCE(where = _mm_xor_si128(where, xmm_initial)) + + +CLMUL +static uint32_t +crc32_clmul(const uint8_t *src, long len, uint32_t initial_crc) +{ + unsigned long algn_diff; + __m128i xmm_t0, xmm_t1, xmm_t2, xmm_t3; + __m128i xmm_initial = _mm_cvtsi32_si128(initial_crc); + __m128i xmm_crc0 = _mm_cvtsi32_si128(0x9db42487); + __m128i xmm_crc1 = _mm_setzero_si128(); + __m128i xmm_crc2 = _mm_setzero_si128(); + __m128i xmm_crc3 = _mm_setzero_si128(); + __m128i xmm_crc_part; + + int first = 1; + + if (len < 16) { + if (len == 0) + return initial_crc; + if (len < 4) { + /* + * no idea how to do this for <4 bytes, delegate to classic impl. + */ + uint32_t crc = ~initial_crc; + switch (len) { + case 3: crc = (crc >> 8) ^ Crc32Lookup[0][(crc & 0xFF) ^ *src++]; + case 2: crc = (crc >> 8) ^ Crc32Lookup[0][(crc & 0xFF) ^ *src++]; + case 1: crc = (crc >> 8) ^ Crc32Lookup[0][(crc & 0xFF) ^ *src++]; + } + return ~crc; + } + xmm_crc_part = _mm_loadu_si128((__m128i *)src); + XOR_INITIAL(xmm_crc_part); + goto partial; + } + + /* this alignment computation would be wrong for len<16 handled above */ + algn_diff = (0 - (uintptr_t)src) & 0xF; + if (algn_diff) { + xmm_crc_part = _mm_loadu_si128((__m128i *)src); + XOR_INITIAL(xmm_crc_part); + + src += algn_diff; + len -= algn_diff; + + partial_fold(algn_diff, &xmm_crc0, &xmm_crc1, &xmm_crc2, &xmm_crc3, &xmm_crc_part); + } + + while ((len -= 64) >= 0) { + xmm_t0 = _mm_load_si128((__m128i *)src); + xmm_t1 = _mm_load_si128((__m128i *)src + 1); + xmm_t2 = _mm_load_si128((__m128i *)src + 2); + xmm_t3 = _mm_load_si128((__m128i *)src + 3); + + XOR_INITIAL(xmm_t0); + + fold_4(&xmm_crc0, &xmm_crc1, &xmm_crc2, &xmm_crc3); + + xmm_crc0 = _mm_xor_si128(xmm_crc0, xmm_t0); + xmm_crc1 = _mm_xor_si128(xmm_crc1, xmm_t1); + xmm_crc2 = _mm_xor_si128(xmm_crc2, xmm_t2); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_t3); + + src += 64; + } + + /* + * len = num bytes left - 64 + */ + if (len + 16 >= 0) { + len += 16; + + xmm_t0 = _mm_load_si128((__m128i *)src); + xmm_t1 = _mm_load_si128((__m128i *)src + 1); + xmm_t2 = _mm_load_si128((__m128i *)src + 2); + + XOR_INITIAL(xmm_t0); + + fold_3(&xmm_crc0, &xmm_crc1, &xmm_crc2, &xmm_crc3); + + xmm_crc1 = _mm_xor_si128(xmm_crc1, xmm_t0); + xmm_crc2 = _mm_xor_si128(xmm_crc2, xmm_t1); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_t2); + + if (len == 0) + goto done; + + xmm_crc_part = _mm_load_si128((__m128i *)src + 3); + } else if (len + 32 >= 0) { + len += 32; + + xmm_t0 = _mm_load_si128((__m128i *)src); + xmm_t1 = _mm_load_si128((__m128i *)src + 1); + + XOR_INITIAL(xmm_t0); + + fold_2(&xmm_crc0, &xmm_crc1, &xmm_crc2, &xmm_crc3); + + xmm_crc2 = _mm_xor_si128(xmm_crc2, xmm_t0); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_t1); + + if (len == 0) + goto done; + + xmm_crc_part = _mm_load_si128((__m128i *)src + 2); + } else if (len + 48 >= 0) { + len += 48; + + xmm_t0 = _mm_load_si128((__m128i *)src); + + XOR_INITIAL(xmm_t0); + + fold_1(&xmm_crc0, &xmm_crc1, &xmm_crc2, &xmm_crc3); + + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_t0); + + if (len == 0) + goto done; + + xmm_crc_part = _mm_load_si128((__m128i *)src + 1); + } else { + len += 64; + if (len == 0) + goto done; + xmm_crc_part = _mm_load_si128((__m128i *)src); + XOR_INITIAL(xmm_crc_part); + } + +partial: + partial_fold(len, &xmm_crc0, &xmm_crc1, &xmm_crc2, &xmm_crc3, &xmm_crc_part); + +done: + (void)0; + + /* fold 512 to 32 */ + const __m128i xmm_mask = _mm_load_si128((__m128i *)crc_mask); + const __m128i xmm_mask2 = _mm_load_si128((__m128i *)crc_mask2); + + uint32_t crc; + __m128i x_tmp0, x_tmp1, x_tmp2, crc_fold; + + /* + * k1 + */ + crc_fold = _mm_load_si128((__m128i *)crc_k); + + x_tmp0 = _mm_clmulepi64_si128(xmm_crc0, crc_fold, 0x10); + xmm_crc0 = _mm_clmulepi64_si128(xmm_crc0, crc_fold, 0x01); + xmm_crc1 = _mm_xor_si128(xmm_crc1, x_tmp0); + xmm_crc1 = _mm_xor_si128(xmm_crc1, xmm_crc0); + + x_tmp1 = _mm_clmulepi64_si128(xmm_crc1, crc_fold, 0x10); + xmm_crc1 = _mm_clmulepi64_si128(xmm_crc1, crc_fold, 0x01); + xmm_crc2 = _mm_xor_si128(xmm_crc2, x_tmp1); + xmm_crc2 = _mm_xor_si128(xmm_crc2, xmm_crc1); + + x_tmp2 = _mm_clmulepi64_si128(xmm_crc2, crc_fold, 0x10); + xmm_crc2 = _mm_clmulepi64_si128(xmm_crc2, crc_fold, 0x01); + xmm_crc3 = _mm_xor_si128(xmm_crc3, x_tmp2); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_crc2); + + /* + * k5 + */ + crc_fold = _mm_load_si128((__m128i *)crc_k + 1); + + xmm_crc0 = xmm_crc3; + xmm_crc3 = _mm_clmulepi64_si128(xmm_crc3, crc_fold, 0); + xmm_crc0 = _mm_srli_si128(xmm_crc0, 8); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_crc0); + + xmm_crc0 = xmm_crc3; + xmm_crc3 = _mm_slli_si128(xmm_crc3, 4); + xmm_crc3 = _mm_clmulepi64_si128(xmm_crc3, crc_fold, 0x10); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_crc0); + xmm_crc3 = _mm_and_si128(xmm_crc3, xmm_mask2); + + /* + * k7 + */ + xmm_crc1 = xmm_crc3; + xmm_crc2 = xmm_crc3; + crc_fold = _mm_load_si128((__m128i *)crc_k + 2); + + xmm_crc3 = _mm_clmulepi64_si128(xmm_crc3, crc_fold, 0); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_crc2); + xmm_crc3 = _mm_and_si128(xmm_crc3, xmm_mask); + + xmm_crc2 = xmm_crc3; + xmm_crc3 = _mm_clmulepi64_si128(xmm_crc3, crc_fold, 0x10); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_crc2); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_crc1); + + /* + * could just as well write xmm_crc3[2], doing a movaps and truncating, but + * no real advantage - it's a tiny bit slower per call, while no additional CPUs + * would be supported by only requiring SSSE3 and CLMUL instead of SSE4.1 + CLMUL + */ + crc = _mm_extract_epi32(xmm_crc3, 2); + return ~crc; +} diff --git a/src/borg/_crc32/crc32.c b/src/borg/_crc32/crc32.c new file mode 100644 index 00000000..212f40f9 --- /dev/null +++ b/src/borg/_crc32/crc32.c @@ -0,0 +1,103 @@ + +/* always compile slice by 8 as a runtime fallback */ +#include "slice_by_8.c" + +#ifdef __GNUC__ +#if __x86_64__ +/* + * Because we don't want a configure script we need compiler-dependent pre-defined macros for detecting this, + * also some compiler-dependent stuff to invoke SSE modes and align things. + */ + +#define FOLDING_CRC + +/* + * SSE2 misses _mm_shuffle_epi32, and _mm_extract_epi32 + * SSSE3 added _mm_shuffle_epi32 + * SSE4.1 added _mm_extract_epi32 + * Also requires CLMUL of course (all AES-NI CPUs have it) + * Note that there are no CPUs with AES-NI/CLMUL but without SSE4.1 + */ +#define CLMUL __attribute__ ((target ("pclmul,sse4.1"))) + +#define ALIGNED_(n) __attribute__ ((aligned(n))) + +/* + * Work around https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56298 + * These are taken from GCC 6.x, so apparently the above bug has been resolved in that version, + * but it still affects widely used GCC 4.x. + * Part 2 of 2 follows below. + */ + +/* clang also defines __GNUC__, but doesn't need this, it emits warnings instead */ +#ifndef __clang__ + +#ifndef __PCLMUL__ +#pragma GCC push_options +#pragma GCC target("pclmul") +#define __BORG_DISABLE_PCLMUL__ +#endif + +#ifndef __SSE3__ +#pragma GCC push_options +#pragma GCC target("sse3") +#define __BORG_DISABLE_SSE3__ +#endif + +#ifndef __SSSE3__ +#pragma GCC push_options +#pragma GCC target("ssse3") +#define __BORG_DISABLE_SSSE3__ +#endif + +#ifndef __SSE4_1__ +#pragma GCC push_options +#pragma GCC target("sse4.1") +#define __BORG_DISABLE_SSE4_1__ +#endif + +#endif /* ifdef __clang__ */ + +#endif /* if __x86_64__ */ +#endif /* ifdef __GNUC__ */ + +#ifdef FOLDING_CRC +#include "clmul.c" +#else + +static uint32_t +crc32_clmul(const uint8_t *src, long len, uint32_t initial_crc) +{ + assert(0); + return 0; +} + +static int +have_clmul(void) +{ + return 0; +} +#endif + +/* + * Part 2 of 2 of the GCC workaround. + */ +#ifdef __BORG_DISABLE_PCLMUL__ +#undef __BORG_DISABLE_PCLMUL__ +#pragma GCC pop_options +#endif + +#ifdef __BORG_DISABLE_SSE3__ +#undef __BORG_DISABLE_SSE3__ +#pragma GCC pop_options +#endif + +#ifdef __BORG_DISABLE_SSSE3__ +#undef __BORG_DISABLE_SSSE3__ +#pragma GCC pop_options +#endif + +#ifdef __BORG_DISABLE_SSE4_1__ +#undef __BORG_DISABLE_SSE4_1__ +#pragma GCC pop_options +#endif diff --git a/src/borg/_crc32/slice_by_8.c b/src/borg/_crc32/slice_by_8.c new file mode 100644 index 00000000..30bd07c1 --- /dev/null +++ b/src/borg/_crc32/slice_by_8.c @@ -0,0 +1,427 @@ +// ////////////////////////////////////////////////////////// +// Crc32.h +// Copyright (c) 2011-2016 Stephan Brumme. All rights reserved. +// see http://create.stephan-brumme.com/disclaimer.html +// + +// uint8_t, uint32_t, int32_t +#include +// size_t +#include + +/// compute CRC32 (Slicing-by-8 algorithm), unroll inner loop 4 times +uint32_t crc32_4x8bytes(const void* data, size_t length, uint32_t previousCrc32); + + + +// ////////////////////////////////////////////////////////// +// Crc32.cpp +// Copyright (c) 2011-2016 Stephan Brumme. All rights reserved. +// Slicing-by-16 contributed by Bulat Ziganshin +// Tableless bytewise CRC contributed by Hagai Gold +// see http://create.stephan-brumme.com/disclaimer.html +// + +// if running on an embedded system, you might consider shrinking the +// big Crc32Lookup table: +// - crc32_bitwise doesn't need it at all +// - crc32_halfbyte has its own small lookup table +// - crc32_1byte needs only Crc32Lookup[0] +// - crc32_4bytes needs only Crc32Lookup[0..3] +// - crc32_8bytes needs only Crc32Lookup[0..7] +// - crc32_4x8bytes needs only Crc32Lookup[0..7] +// - crc32_16bytes needs all of Crc32Lookup + +// define endianess and some integer data types +#if defined(_MSC_VER) || defined(__MINGW32__) + #define __LITTLE_ENDIAN 1234 + #define __BIG_ENDIAN 4321 + #define __BYTE_ORDER __LITTLE_ENDIAN + + #include + #ifdef __MINGW32__ + #define PREFETCH(location) __builtin_prefetch(location) + #else + #define PREFETCH(location) _mm_prefetch(location, _MM_HINT_T0) + #endif +#else + // defines __BYTE_ORDER as __LITTLE_ENDIAN or __BIG_ENDIAN + #include + + #ifdef __GNUC__ + #define PREFETCH(location) __builtin_prefetch(location) + #else + #define PREFETCH(location) ; + #endif +#endif + + +/// zlib's CRC32 polynomial +const uint32_t Polynomial = 0xEDB88320; + +/// swap endianess +static inline uint32_t swap(uint32_t x) +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_bswap32(x); +#else + return (x >> 24) | + ((x >> 8) & 0x0000FF00) | + ((x << 8) & 0x00FF0000) | + (x << 24); +#endif +} + +// ////////////////////////////////////////////////////////// +// constants + +/// look-up table, already declared above +const uint32_t Crc32Lookup[8][256] = +{ + //// same algorithm as crc32_bitwise + //for (int i = 0; i <= 0xFF; i++) + //{ + // uint32_t crc = i; + // for (int j = 0; j < 8; j++) + // crc = (crc >> 1) ^ ((crc & 1) * Polynomial); + // Crc32Lookup[0][i] = crc; + //} + //// ... and the following slicing-by-8 algorithm (from Intel): + //// http://www.intel.com/technology/comms/perfnet/download/CRC_generators.pdf + //// http://sourceforge.net/projects/slicing-by-8/ + //for (int slice = 1; slice < MaxSlice; slice++) + // Crc32Lookup[slice][i] = (Crc32Lookup[slice - 1][i] >> 8) ^ Crc32Lookup[0][Crc32Lookup[slice - 1][i] & 0xFF]; + { + // note: the first number of every second row corresponds to the half-byte look-up table ! + 0x00000000,0x77073096,0xEE0E612C,0x990951BA,0x076DC419,0x706AF48F,0xE963A535,0x9E6495A3, + 0x0EDB8832,0x79DCB8A4,0xE0D5E91E,0x97D2D988,0x09B64C2B,0x7EB17CBD,0xE7B82D07,0x90BF1D91, + 0x1DB71064,0x6AB020F2,0xF3B97148,0x84BE41DE,0x1ADAD47D,0x6DDDE4EB,0xF4D4B551,0x83D385C7, + 0x136C9856,0x646BA8C0,0xFD62F97A,0x8A65C9EC,0x14015C4F,0x63066CD9,0xFA0F3D63,0x8D080DF5, + 0x3B6E20C8,0x4C69105E,0xD56041E4,0xA2677172,0x3C03E4D1,0x4B04D447,0xD20D85FD,0xA50AB56B, + 0x35B5A8FA,0x42B2986C,0xDBBBC9D6,0xACBCF940,0x32D86CE3,0x45DF5C75,0xDCD60DCF,0xABD13D59, + 0x26D930AC,0x51DE003A,0xC8D75180,0xBFD06116,0x21B4F4B5,0x56B3C423,0xCFBA9599,0xB8BDA50F, + 0x2802B89E,0x5F058808,0xC60CD9B2,0xB10BE924,0x2F6F7C87,0x58684C11,0xC1611DAB,0xB6662D3D, + 0x76DC4190,0x01DB7106,0x98D220BC,0xEFD5102A,0x71B18589,0x06B6B51F,0x9FBFE4A5,0xE8B8D433, + 0x7807C9A2,0x0F00F934,0x9609A88E,0xE10E9818,0x7F6A0DBB,0x086D3D2D,0x91646C97,0xE6635C01, + 0x6B6B51F4,0x1C6C6162,0x856530D8,0xF262004E,0x6C0695ED,0x1B01A57B,0x8208F4C1,0xF50FC457, + 0x65B0D9C6,0x12B7E950,0x8BBEB8EA,0xFCB9887C,0x62DD1DDF,0x15DA2D49,0x8CD37CF3,0xFBD44C65, + 0x4DB26158,0x3AB551CE,0xA3BC0074,0xD4BB30E2,0x4ADFA541,0x3DD895D7,0xA4D1C46D,0xD3D6F4FB, + 0x4369E96A,0x346ED9FC,0xAD678846,0xDA60B8D0,0x44042D73,0x33031DE5,0xAA0A4C5F,0xDD0D7CC9, + 0x5005713C,0x270241AA,0xBE0B1010,0xC90C2086,0x5768B525,0x206F85B3,0xB966D409,0xCE61E49F, + 0x5EDEF90E,0x29D9C998,0xB0D09822,0xC7D7A8B4,0x59B33D17,0x2EB40D81,0xB7BD5C3B,0xC0BA6CAD, + 0xEDB88320,0x9ABFB3B6,0x03B6E20C,0x74B1D29A,0xEAD54739,0x9DD277AF,0x04DB2615,0x73DC1683, + 0xE3630B12,0x94643B84,0x0D6D6A3E,0x7A6A5AA8,0xE40ECF0B,0x9309FF9D,0x0A00AE27,0x7D079EB1, + 0xF00F9344,0x8708A3D2,0x1E01F268,0x6906C2FE,0xF762575D,0x806567CB,0x196C3671,0x6E6B06E7, + 0xFED41B76,0x89D32BE0,0x10DA7A5A,0x67DD4ACC,0xF9B9DF6F,0x8EBEEFF9,0x17B7BE43,0x60B08ED5, + 0xD6D6A3E8,0xA1D1937E,0x38D8C2C4,0x4FDFF252,0xD1BB67F1,0xA6BC5767,0x3FB506DD,0x48B2364B, + 0xD80D2BDA,0xAF0A1B4C,0x36034AF6,0x41047A60,0xDF60EFC3,0xA867DF55,0x316E8EEF,0x4669BE79, + 0xCB61B38C,0xBC66831A,0x256FD2A0,0x5268E236,0xCC0C7795,0xBB0B4703,0x220216B9,0x5505262F, + 0xC5BA3BBE,0xB2BD0B28,0x2BB45A92,0x5CB36A04,0xC2D7FFA7,0xB5D0CF31,0x2CD99E8B,0x5BDEAE1D, + 0x9B64C2B0,0xEC63F226,0x756AA39C,0x026D930A,0x9C0906A9,0xEB0E363F,0x72076785,0x05005713, + 0x95BF4A82,0xE2B87A14,0x7BB12BAE,0x0CB61B38,0x92D28E9B,0xE5D5BE0D,0x7CDCEFB7,0x0BDBDF21, + 0x86D3D2D4,0xF1D4E242,0x68DDB3F8,0x1FDA836E,0x81BE16CD,0xF6B9265B,0x6FB077E1,0x18B74777, + 0x88085AE6,0xFF0F6A70,0x66063BCA,0x11010B5C,0x8F659EFF,0xF862AE69,0x616BFFD3,0x166CCF45, + 0xA00AE278,0xD70DD2EE,0x4E048354,0x3903B3C2,0xA7672661,0xD06016F7,0x4969474D,0x3E6E77DB, + 0xAED16A4A,0xD9D65ADC,0x40DF0B66,0x37D83BF0,0xA9BCAE53,0xDEBB9EC5,0x47B2CF7F,0x30B5FFE9, + 0xBDBDF21C,0xCABAC28A,0x53B39330,0x24B4A3A6,0xBAD03605,0xCDD70693,0x54DE5729,0x23D967BF, + 0xB3667A2E,0xC4614AB8,0x5D681B02,0x2A6F2B94,0xB40BBE37,0xC30C8EA1,0x5A05DF1B,0x2D02EF8D, + } + + ,{ + 0x00000000,0x191B3141,0x32366282,0x2B2D53C3,0x646CC504,0x7D77F445,0x565AA786,0x4F4196C7, + 0xC8D98A08,0xD1C2BB49,0xFAEFE88A,0xE3F4D9CB,0xACB54F0C,0xB5AE7E4D,0x9E832D8E,0x87981CCF, + 0x4AC21251,0x53D92310,0x78F470D3,0x61EF4192,0x2EAED755,0x37B5E614,0x1C98B5D7,0x05838496, + 0x821B9859,0x9B00A918,0xB02DFADB,0xA936CB9A,0xE6775D5D,0xFF6C6C1C,0xD4413FDF,0xCD5A0E9E, + 0x958424A2,0x8C9F15E3,0xA7B24620,0xBEA97761,0xF1E8E1A6,0xE8F3D0E7,0xC3DE8324,0xDAC5B265, + 0x5D5DAEAA,0x44469FEB,0x6F6BCC28,0x7670FD69,0x39316BAE,0x202A5AEF,0x0B07092C,0x121C386D, + 0xDF4636F3,0xC65D07B2,0xED705471,0xF46B6530,0xBB2AF3F7,0xA231C2B6,0x891C9175,0x9007A034, + 0x179FBCFB,0x0E848DBA,0x25A9DE79,0x3CB2EF38,0x73F379FF,0x6AE848BE,0x41C51B7D,0x58DE2A3C, + 0xF0794F05,0xE9627E44,0xC24F2D87,0xDB541CC6,0x94158A01,0x8D0EBB40,0xA623E883,0xBF38D9C2, + 0x38A0C50D,0x21BBF44C,0x0A96A78F,0x138D96CE,0x5CCC0009,0x45D73148,0x6EFA628B,0x77E153CA, + 0xBABB5D54,0xA3A06C15,0x888D3FD6,0x91960E97,0xDED79850,0xC7CCA911,0xECE1FAD2,0xF5FACB93, + 0x7262D75C,0x6B79E61D,0x4054B5DE,0x594F849F,0x160E1258,0x0F152319,0x243870DA,0x3D23419B, + 0x65FD6BA7,0x7CE65AE6,0x57CB0925,0x4ED03864,0x0191AEA3,0x188A9FE2,0x33A7CC21,0x2ABCFD60, + 0xAD24E1AF,0xB43FD0EE,0x9F12832D,0x8609B26C,0xC94824AB,0xD05315EA,0xFB7E4629,0xE2657768, + 0x2F3F79F6,0x362448B7,0x1D091B74,0x04122A35,0x4B53BCF2,0x52488DB3,0x7965DE70,0x607EEF31, + 0xE7E6F3FE,0xFEFDC2BF,0xD5D0917C,0xCCCBA03D,0x838A36FA,0x9A9107BB,0xB1BC5478,0xA8A76539, + 0x3B83984B,0x2298A90A,0x09B5FAC9,0x10AECB88,0x5FEF5D4F,0x46F46C0E,0x6DD93FCD,0x74C20E8C, + 0xF35A1243,0xEA412302,0xC16C70C1,0xD8774180,0x9736D747,0x8E2DE606,0xA500B5C5,0xBC1B8484, + 0x71418A1A,0x685ABB5B,0x4377E898,0x5A6CD9D9,0x152D4F1E,0x0C367E5F,0x271B2D9C,0x3E001CDD, + 0xB9980012,0xA0833153,0x8BAE6290,0x92B553D1,0xDDF4C516,0xC4EFF457,0xEFC2A794,0xF6D996D5, + 0xAE07BCE9,0xB71C8DA8,0x9C31DE6B,0x852AEF2A,0xCA6B79ED,0xD37048AC,0xF85D1B6F,0xE1462A2E, + 0x66DE36E1,0x7FC507A0,0x54E85463,0x4DF36522,0x02B2F3E5,0x1BA9C2A4,0x30849167,0x299FA026, + 0xE4C5AEB8,0xFDDE9FF9,0xD6F3CC3A,0xCFE8FD7B,0x80A96BBC,0x99B25AFD,0xB29F093E,0xAB84387F, + 0x2C1C24B0,0x350715F1,0x1E2A4632,0x07317773,0x4870E1B4,0x516BD0F5,0x7A468336,0x635DB277, + 0xCBFAD74E,0xD2E1E60F,0xF9CCB5CC,0xE0D7848D,0xAF96124A,0xB68D230B,0x9DA070C8,0x84BB4189, + 0x03235D46,0x1A386C07,0x31153FC4,0x280E0E85,0x674F9842,0x7E54A903,0x5579FAC0,0x4C62CB81, + 0x8138C51F,0x9823F45E,0xB30EA79D,0xAA1596DC,0xE554001B,0xFC4F315A,0xD7626299,0xCE7953D8, + 0x49E14F17,0x50FA7E56,0x7BD72D95,0x62CC1CD4,0x2D8D8A13,0x3496BB52,0x1FBBE891,0x06A0D9D0, + 0x5E7EF3EC,0x4765C2AD,0x6C48916E,0x7553A02F,0x3A1236E8,0x230907A9,0x0824546A,0x113F652B, + 0x96A779E4,0x8FBC48A5,0xA4911B66,0xBD8A2A27,0xF2CBBCE0,0xEBD08DA1,0xC0FDDE62,0xD9E6EF23, + 0x14BCE1BD,0x0DA7D0FC,0x268A833F,0x3F91B27E,0x70D024B9,0x69CB15F8,0x42E6463B,0x5BFD777A, + 0xDC656BB5,0xC57E5AF4,0xEE530937,0xF7483876,0xB809AEB1,0xA1129FF0,0x8A3FCC33,0x9324FD72, + }, + + { + 0x00000000,0x01C26A37,0x0384D46E,0x0246BE59,0x0709A8DC,0x06CBC2EB,0x048D7CB2,0x054F1685, + 0x0E1351B8,0x0FD13B8F,0x0D9785D6,0x0C55EFE1,0x091AF964,0x08D89353,0x0A9E2D0A,0x0B5C473D, + 0x1C26A370,0x1DE4C947,0x1FA2771E,0x1E601D29,0x1B2F0BAC,0x1AED619B,0x18ABDFC2,0x1969B5F5, + 0x1235F2C8,0x13F798FF,0x11B126A6,0x10734C91,0x153C5A14,0x14FE3023,0x16B88E7A,0x177AE44D, + 0x384D46E0,0x398F2CD7,0x3BC9928E,0x3A0BF8B9,0x3F44EE3C,0x3E86840B,0x3CC03A52,0x3D025065, + 0x365E1758,0x379C7D6F,0x35DAC336,0x3418A901,0x3157BF84,0x3095D5B3,0x32D36BEA,0x331101DD, + 0x246BE590,0x25A98FA7,0x27EF31FE,0x262D5BC9,0x23624D4C,0x22A0277B,0x20E69922,0x2124F315, + 0x2A78B428,0x2BBADE1F,0x29FC6046,0x283E0A71,0x2D711CF4,0x2CB376C3,0x2EF5C89A,0x2F37A2AD, + 0x709A8DC0,0x7158E7F7,0x731E59AE,0x72DC3399,0x7793251C,0x76514F2B,0x7417F172,0x75D59B45, + 0x7E89DC78,0x7F4BB64F,0x7D0D0816,0x7CCF6221,0x798074A4,0x78421E93,0x7A04A0CA,0x7BC6CAFD, + 0x6CBC2EB0,0x6D7E4487,0x6F38FADE,0x6EFA90E9,0x6BB5866C,0x6A77EC5B,0x68315202,0x69F33835, + 0x62AF7F08,0x636D153F,0x612BAB66,0x60E9C151,0x65A6D7D4,0x6464BDE3,0x662203BA,0x67E0698D, + 0x48D7CB20,0x4915A117,0x4B531F4E,0x4A917579,0x4FDE63FC,0x4E1C09CB,0x4C5AB792,0x4D98DDA5, + 0x46C49A98,0x4706F0AF,0x45404EF6,0x448224C1,0x41CD3244,0x400F5873,0x4249E62A,0x438B8C1D, + 0x54F16850,0x55330267,0x5775BC3E,0x56B7D609,0x53F8C08C,0x523AAABB,0x507C14E2,0x51BE7ED5, + 0x5AE239E8,0x5B2053DF,0x5966ED86,0x58A487B1,0x5DEB9134,0x5C29FB03,0x5E6F455A,0x5FAD2F6D, + 0xE1351B80,0xE0F771B7,0xE2B1CFEE,0xE373A5D9,0xE63CB35C,0xE7FED96B,0xE5B86732,0xE47A0D05, + 0xEF264A38,0xEEE4200F,0xECA29E56,0xED60F461,0xE82FE2E4,0xE9ED88D3,0xEBAB368A,0xEA695CBD, + 0xFD13B8F0,0xFCD1D2C7,0xFE976C9E,0xFF5506A9,0xFA1A102C,0xFBD87A1B,0xF99EC442,0xF85CAE75, + 0xF300E948,0xF2C2837F,0xF0843D26,0xF1465711,0xF4094194,0xF5CB2BA3,0xF78D95FA,0xF64FFFCD, + 0xD9785D60,0xD8BA3757,0xDAFC890E,0xDB3EE339,0xDE71F5BC,0xDFB39F8B,0xDDF521D2,0xDC374BE5, + 0xD76B0CD8,0xD6A966EF,0xD4EFD8B6,0xD52DB281,0xD062A404,0xD1A0CE33,0xD3E6706A,0xD2241A5D, + 0xC55EFE10,0xC49C9427,0xC6DA2A7E,0xC7184049,0xC25756CC,0xC3953CFB,0xC1D382A2,0xC011E895, + 0xCB4DAFA8,0xCA8FC59F,0xC8C97BC6,0xC90B11F1,0xCC440774,0xCD866D43,0xCFC0D31A,0xCE02B92D, + 0x91AF9640,0x906DFC77,0x922B422E,0x93E92819,0x96A63E9C,0x976454AB,0x9522EAF2,0x94E080C5, + 0x9FBCC7F8,0x9E7EADCF,0x9C381396,0x9DFA79A1,0x98B56F24,0x99770513,0x9B31BB4A,0x9AF3D17D, + 0x8D893530,0x8C4B5F07,0x8E0DE15E,0x8FCF8B69,0x8A809DEC,0x8B42F7DB,0x89044982,0x88C623B5, + 0x839A6488,0x82580EBF,0x801EB0E6,0x81DCDAD1,0x8493CC54,0x8551A663,0x8717183A,0x86D5720D, + 0xA9E2D0A0,0xA820BA97,0xAA6604CE,0xABA46EF9,0xAEEB787C,0xAF29124B,0xAD6FAC12,0xACADC625, + 0xA7F18118,0xA633EB2F,0xA4755576,0xA5B73F41,0xA0F829C4,0xA13A43F3,0xA37CFDAA,0xA2BE979D, + 0xB5C473D0,0xB40619E7,0xB640A7BE,0xB782CD89,0xB2CDDB0C,0xB30FB13B,0xB1490F62,0xB08B6555, + 0xBBD72268,0xBA15485F,0xB853F606,0xB9919C31,0xBCDE8AB4,0xBD1CE083,0xBF5A5EDA,0xBE9834ED, + }, + + { + 0x00000000,0xB8BC6765,0xAA09C88B,0x12B5AFEE,0x8F629757,0x37DEF032,0x256B5FDC,0x9DD738B9, + 0xC5B428EF,0x7D084F8A,0x6FBDE064,0xD7018701,0x4AD6BFB8,0xF26AD8DD,0xE0DF7733,0x58631056, + 0x5019579F,0xE8A530FA,0xFA109F14,0x42ACF871,0xDF7BC0C8,0x67C7A7AD,0x75720843,0xCDCE6F26, + 0x95AD7F70,0x2D111815,0x3FA4B7FB,0x8718D09E,0x1ACFE827,0xA2738F42,0xB0C620AC,0x087A47C9, + 0xA032AF3E,0x188EC85B,0x0A3B67B5,0xB28700D0,0x2F503869,0x97EC5F0C,0x8559F0E2,0x3DE59787, + 0x658687D1,0xDD3AE0B4,0xCF8F4F5A,0x7733283F,0xEAE41086,0x525877E3,0x40EDD80D,0xF851BF68, + 0xF02BF8A1,0x48979FC4,0x5A22302A,0xE29E574F,0x7F496FF6,0xC7F50893,0xD540A77D,0x6DFCC018, + 0x359FD04E,0x8D23B72B,0x9F9618C5,0x272A7FA0,0xBAFD4719,0x0241207C,0x10F48F92,0xA848E8F7, + 0x9B14583D,0x23A83F58,0x311D90B6,0x89A1F7D3,0x1476CF6A,0xACCAA80F,0xBE7F07E1,0x06C36084, + 0x5EA070D2,0xE61C17B7,0xF4A9B859,0x4C15DF3C,0xD1C2E785,0x697E80E0,0x7BCB2F0E,0xC377486B, + 0xCB0D0FA2,0x73B168C7,0x6104C729,0xD9B8A04C,0x446F98F5,0xFCD3FF90,0xEE66507E,0x56DA371B, + 0x0EB9274D,0xB6054028,0xA4B0EFC6,0x1C0C88A3,0x81DBB01A,0x3967D77F,0x2BD27891,0x936E1FF4, + 0x3B26F703,0x839A9066,0x912F3F88,0x299358ED,0xB4446054,0x0CF80731,0x1E4DA8DF,0xA6F1CFBA, + 0xFE92DFEC,0x462EB889,0x549B1767,0xEC277002,0x71F048BB,0xC94C2FDE,0xDBF98030,0x6345E755, + 0x6B3FA09C,0xD383C7F9,0xC1366817,0x798A0F72,0xE45D37CB,0x5CE150AE,0x4E54FF40,0xF6E89825, + 0xAE8B8873,0x1637EF16,0x048240F8,0xBC3E279D,0x21E91F24,0x99557841,0x8BE0D7AF,0x335CB0CA, + 0xED59B63B,0x55E5D15E,0x47507EB0,0xFFEC19D5,0x623B216C,0xDA874609,0xC832E9E7,0x708E8E82, + 0x28ED9ED4,0x9051F9B1,0x82E4565F,0x3A58313A,0xA78F0983,0x1F336EE6,0x0D86C108,0xB53AA66D, + 0xBD40E1A4,0x05FC86C1,0x1749292F,0xAFF54E4A,0x322276F3,0x8A9E1196,0x982BBE78,0x2097D91D, + 0x78F4C94B,0xC048AE2E,0xD2FD01C0,0x6A4166A5,0xF7965E1C,0x4F2A3979,0x5D9F9697,0xE523F1F2, + 0x4D6B1905,0xF5D77E60,0xE762D18E,0x5FDEB6EB,0xC2098E52,0x7AB5E937,0x680046D9,0xD0BC21BC, + 0x88DF31EA,0x3063568F,0x22D6F961,0x9A6A9E04,0x07BDA6BD,0xBF01C1D8,0xADB46E36,0x15080953, + 0x1D724E9A,0xA5CE29FF,0xB77B8611,0x0FC7E174,0x9210D9CD,0x2AACBEA8,0x38191146,0x80A57623, + 0xD8C66675,0x607A0110,0x72CFAEFE,0xCA73C99B,0x57A4F122,0xEF189647,0xFDAD39A9,0x45115ECC, + 0x764DEE06,0xCEF18963,0xDC44268D,0x64F841E8,0xF92F7951,0x41931E34,0x5326B1DA,0xEB9AD6BF, + 0xB3F9C6E9,0x0B45A18C,0x19F00E62,0xA14C6907,0x3C9B51BE,0x842736DB,0x96929935,0x2E2EFE50, + 0x2654B999,0x9EE8DEFC,0x8C5D7112,0x34E11677,0xA9362ECE,0x118A49AB,0x033FE645,0xBB838120, + 0xE3E09176,0x5B5CF613,0x49E959FD,0xF1553E98,0x6C820621,0xD43E6144,0xC68BCEAA,0x7E37A9CF, + 0xD67F4138,0x6EC3265D,0x7C7689B3,0xC4CAEED6,0x591DD66F,0xE1A1B10A,0xF3141EE4,0x4BA87981, + 0x13CB69D7,0xAB770EB2,0xB9C2A15C,0x017EC639,0x9CA9FE80,0x241599E5,0x36A0360B,0x8E1C516E, + 0x866616A7,0x3EDA71C2,0x2C6FDE2C,0x94D3B949,0x090481F0,0xB1B8E695,0xA30D497B,0x1BB12E1E, + 0x43D23E48,0xFB6E592D,0xE9DBF6C3,0x516791A6,0xCCB0A91F,0x740CCE7A,0x66B96194,0xDE0506F1, + } + + ,{ + 0x00000000,0x3D6029B0,0x7AC05360,0x47A07AD0,0xF580A6C0,0xC8E08F70,0x8F40F5A0,0xB220DC10, + 0x30704BC1,0x0D106271,0x4AB018A1,0x77D03111,0xC5F0ED01,0xF890C4B1,0xBF30BE61,0x825097D1, + 0x60E09782,0x5D80BE32,0x1A20C4E2,0x2740ED52,0x95603142,0xA80018F2,0xEFA06222,0xD2C04B92, + 0x5090DC43,0x6DF0F5F3,0x2A508F23,0x1730A693,0xA5107A83,0x98705333,0xDFD029E3,0xE2B00053, + 0xC1C12F04,0xFCA106B4,0xBB017C64,0x866155D4,0x344189C4,0x0921A074,0x4E81DAA4,0x73E1F314, + 0xF1B164C5,0xCCD14D75,0x8B7137A5,0xB6111E15,0x0431C205,0x3951EBB5,0x7EF19165,0x4391B8D5, + 0xA121B886,0x9C419136,0xDBE1EBE6,0xE681C256,0x54A11E46,0x69C137F6,0x2E614D26,0x13016496, + 0x9151F347,0xAC31DAF7,0xEB91A027,0xD6F18997,0x64D15587,0x59B17C37,0x1E1106E7,0x23712F57, + 0x58F35849,0x659371F9,0x22330B29,0x1F532299,0xAD73FE89,0x9013D739,0xD7B3ADE9,0xEAD38459, + 0x68831388,0x55E33A38,0x124340E8,0x2F236958,0x9D03B548,0xA0639CF8,0xE7C3E628,0xDAA3CF98, + 0x3813CFCB,0x0573E67B,0x42D39CAB,0x7FB3B51B,0xCD93690B,0xF0F340BB,0xB7533A6B,0x8A3313DB, + 0x0863840A,0x3503ADBA,0x72A3D76A,0x4FC3FEDA,0xFDE322CA,0xC0830B7A,0x872371AA,0xBA43581A, + 0x9932774D,0xA4525EFD,0xE3F2242D,0xDE920D9D,0x6CB2D18D,0x51D2F83D,0x167282ED,0x2B12AB5D, + 0xA9423C8C,0x9422153C,0xD3826FEC,0xEEE2465C,0x5CC29A4C,0x61A2B3FC,0x2602C92C,0x1B62E09C, + 0xF9D2E0CF,0xC4B2C97F,0x8312B3AF,0xBE729A1F,0x0C52460F,0x31326FBF,0x7692156F,0x4BF23CDF, + 0xC9A2AB0E,0xF4C282BE,0xB362F86E,0x8E02D1DE,0x3C220DCE,0x0142247E,0x46E25EAE,0x7B82771E, + 0xB1E6B092,0x8C869922,0xCB26E3F2,0xF646CA42,0x44661652,0x79063FE2,0x3EA64532,0x03C66C82, + 0x8196FB53,0xBCF6D2E3,0xFB56A833,0xC6368183,0x74165D93,0x49767423,0x0ED60EF3,0x33B62743, + 0xD1062710,0xEC660EA0,0xABC67470,0x96A65DC0,0x248681D0,0x19E6A860,0x5E46D2B0,0x6326FB00, + 0xE1766CD1,0xDC164561,0x9BB63FB1,0xA6D61601,0x14F6CA11,0x2996E3A1,0x6E369971,0x5356B0C1, + 0x70279F96,0x4D47B626,0x0AE7CCF6,0x3787E546,0x85A73956,0xB8C710E6,0xFF676A36,0xC2074386, + 0x4057D457,0x7D37FDE7,0x3A978737,0x07F7AE87,0xB5D77297,0x88B75B27,0xCF1721F7,0xF2770847, + 0x10C70814,0x2DA721A4,0x6A075B74,0x576772C4,0xE547AED4,0xD8278764,0x9F87FDB4,0xA2E7D404, + 0x20B743D5,0x1DD76A65,0x5A7710B5,0x67173905,0xD537E515,0xE857CCA5,0xAFF7B675,0x92979FC5, + 0xE915E8DB,0xD475C16B,0x93D5BBBB,0xAEB5920B,0x1C954E1B,0x21F567AB,0x66551D7B,0x5B3534CB, + 0xD965A31A,0xE4058AAA,0xA3A5F07A,0x9EC5D9CA,0x2CE505DA,0x11852C6A,0x562556BA,0x6B457F0A, + 0x89F57F59,0xB49556E9,0xF3352C39,0xCE550589,0x7C75D999,0x4115F029,0x06B58AF9,0x3BD5A349, + 0xB9853498,0x84E51D28,0xC34567F8,0xFE254E48,0x4C059258,0x7165BBE8,0x36C5C138,0x0BA5E888, + 0x28D4C7DF,0x15B4EE6F,0x521494BF,0x6F74BD0F,0xDD54611F,0xE03448AF,0xA794327F,0x9AF41BCF, + 0x18A48C1E,0x25C4A5AE,0x6264DF7E,0x5F04F6CE,0xED242ADE,0xD044036E,0x97E479BE,0xAA84500E, + 0x4834505D,0x755479ED,0x32F4033D,0x0F942A8D,0xBDB4F69D,0x80D4DF2D,0xC774A5FD,0xFA148C4D, + 0x78441B9C,0x4524322C,0x028448FC,0x3FE4614C,0x8DC4BD5C,0xB0A494EC,0xF704EE3C,0xCA64C78C, + }, + + { + 0x00000000,0xCB5CD3A5,0x4DC8A10B,0x869472AE,0x9B914216,0x50CD91B3,0xD659E31D,0x1D0530B8, + 0xEC53826D,0x270F51C8,0xA19B2366,0x6AC7F0C3,0x77C2C07B,0xBC9E13DE,0x3A0A6170,0xF156B2D5, + 0x03D6029B,0xC88AD13E,0x4E1EA390,0x85427035,0x9847408D,0x531B9328,0xD58FE186,0x1ED33223, + 0xEF8580F6,0x24D95353,0xA24D21FD,0x6911F258,0x7414C2E0,0xBF481145,0x39DC63EB,0xF280B04E, + 0x07AC0536,0xCCF0D693,0x4A64A43D,0x81387798,0x9C3D4720,0x57619485,0xD1F5E62B,0x1AA9358E, + 0xEBFF875B,0x20A354FE,0xA6372650,0x6D6BF5F5,0x706EC54D,0xBB3216E8,0x3DA66446,0xF6FAB7E3, + 0x047A07AD,0xCF26D408,0x49B2A6A6,0x82EE7503,0x9FEB45BB,0x54B7961E,0xD223E4B0,0x197F3715, + 0xE82985C0,0x23755665,0xA5E124CB,0x6EBDF76E,0x73B8C7D6,0xB8E41473,0x3E7066DD,0xF52CB578, + 0x0F580A6C,0xC404D9C9,0x4290AB67,0x89CC78C2,0x94C9487A,0x5F959BDF,0xD901E971,0x125D3AD4, + 0xE30B8801,0x28575BA4,0xAEC3290A,0x659FFAAF,0x789ACA17,0xB3C619B2,0x35526B1C,0xFE0EB8B9, + 0x0C8E08F7,0xC7D2DB52,0x4146A9FC,0x8A1A7A59,0x971F4AE1,0x5C439944,0xDAD7EBEA,0x118B384F, + 0xE0DD8A9A,0x2B81593F,0xAD152B91,0x6649F834,0x7B4CC88C,0xB0101B29,0x36846987,0xFDD8BA22, + 0x08F40F5A,0xC3A8DCFF,0x453CAE51,0x8E607DF4,0x93654D4C,0x58399EE9,0xDEADEC47,0x15F13FE2, + 0xE4A78D37,0x2FFB5E92,0xA96F2C3C,0x6233FF99,0x7F36CF21,0xB46A1C84,0x32FE6E2A,0xF9A2BD8F, + 0x0B220DC1,0xC07EDE64,0x46EAACCA,0x8DB67F6F,0x90B34FD7,0x5BEF9C72,0xDD7BEEDC,0x16273D79, + 0xE7718FAC,0x2C2D5C09,0xAAB92EA7,0x61E5FD02,0x7CE0CDBA,0xB7BC1E1F,0x31286CB1,0xFA74BF14, + 0x1EB014D8,0xD5ECC77D,0x5378B5D3,0x98246676,0x852156CE,0x4E7D856B,0xC8E9F7C5,0x03B52460, + 0xF2E396B5,0x39BF4510,0xBF2B37BE,0x7477E41B,0x6972D4A3,0xA22E0706,0x24BA75A8,0xEFE6A60D, + 0x1D661643,0xD63AC5E6,0x50AEB748,0x9BF264ED,0x86F75455,0x4DAB87F0,0xCB3FF55E,0x006326FB, + 0xF135942E,0x3A69478B,0xBCFD3525,0x77A1E680,0x6AA4D638,0xA1F8059D,0x276C7733,0xEC30A496, + 0x191C11EE,0xD240C24B,0x54D4B0E5,0x9F886340,0x828D53F8,0x49D1805D,0xCF45F2F3,0x04192156, + 0xF54F9383,0x3E134026,0xB8873288,0x73DBE12D,0x6EDED195,0xA5820230,0x2316709E,0xE84AA33B, + 0x1ACA1375,0xD196C0D0,0x5702B27E,0x9C5E61DB,0x815B5163,0x4A0782C6,0xCC93F068,0x07CF23CD, + 0xF6999118,0x3DC542BD,0xBB513013,0x700DE3B6,0x6D08D30E,0xA65400AB,0x20C07205,0xEB9CA1A0, + 0x11E81EB4,0xDAB4CD11,0x5C20BFBF,0x977C6C1A,0x8A795CA2,0x41258F07,0xC7B1FDA9,0x0CED2E0C, + 0xFDBB9CD9,0x36E74F7C,0xB0733DD2,0x7B2FEE77,0x662ADECF,0xAD760D6A,0x2BE27FC4,0xE0BEAC61, + 0x123E1C2F,0xD962CF8A,0x5FF6BD24,0x94AA6E81,0x89AF5E39,0x42F38D9C,0xC467FF32,0x0F3B2C97, + 0xFE6D9E42,0x35314DE7,0xB3A53F49,0x78F9ECEC,0x65FCDC54,0xAEA00FF1,0x28347D5F,0xE368AEFA, + 0x16441B82,0xDD18C827,0x5B8CBA89,0x90D0692C,0x8DD55994,0x46898A31,0xC01DF89F,0x0B412B3A, + 0xFA1799EF,0x314B4A4A,0xB7DF38E4,0x7C83EB41,0x6186DBF9,0xAADA085C,0x2C4E7AF2,0xE712A957, + 0x15921919,0xDECECABC,0x585AB812,0x93066BB7,0x8E035B0F,0x455F88AA,0xC3CBFA04,0x089729A1, + 0xF9C19B74,0x329D48D1,0xB4093A7F,0x7F55E9DA,0x6250D962,0xA90C0AC7,0x2F987869,0xE4C4ABCC, + }, + + { + 0x00000000,0xA6770BB4,0x979F1129,0x31E81A9D,0xF44F2413,0x52382FA7,0x63D0353A,0xC5A73E8E, + 0x33EF4E67,0x959845D3,0xA4705F4E,0x020754FA,0xC7A06A74,0x61D761C0,0x503F7B5D,0xF64870E9, + 0x67DE9CCE,0xC1A9977A,0xF0418DE7,0x56368653,0x9391B8DD,0x35E6B369,0x040EA9F4,0xA279A240, + 0x5431D2A9,0xF246D91D,0xC3AEC380,0x65D9C834,0xA07EF6BA,0x0609FD0E,0x37E1E793,0x9196EC27, + 0xCFBD399C,0x69CA3228,0x582228B5,0xFE552301,0x3BF21D8F,0x9D85163B,0xAC6D0CA6,0x0A1A0712, + 0xFC5277FB,0x5A257C4F,0x6BCD66D2,0xCDBA6D66,0x081D53E8,0xAE6A585C,0x9F8242C1,0x39F54975, + 0xA863A552,0x0E14AEE6,0x3FFCB47B,0x998BBFCF,0x5C2C8141,0xFA5B8AF5,0xCBB39068,0x6DC49BDC, + 0x9B8CEB35,0x3DFBE081,0x0C13FA1C,0xAA64F1A8,0x6FC3CF26,0xC9B4C492,0xF85CDE0F,0x5E2BD5BB, + 0x440B7579,0xE27C7ECD,0xD3946450,0x75E36FE4,0xB044516A,0x16335ADE,0x27DB4043,0x81AC4BF7, + 0x77E43B1E,0xD19330AA,0xE07B2A37,0x460C2183,0x83AB1F0D,0x25DC14B9,0x14340E24,0xB2430590, + 0x23D5E9B7,0x85A2E203,0xB44AF89E,0x123DF32A,0xD79ACDA4,0x71EDC610,0x4005DC8D,0xE672D739, + 0x103AA7D0,0xB64DAC64,0x87A5B6F9,0x21D2BD4D,0xE47583C3,0x42028877,0x73EA92EA,0xD59D995E, + 0x8BB64CE5,0x2DC14751,0x1C295DCC,0xBA5E5678,0x7FF968F6,0xD98E6342,0xE86679DF,0x4E11726B, + 0xB8590282,0x1E2E0936,0x2FC613AB,0x89B1181F,0x4C162691,0xEA612D25,0xDB8937B8,0x7DFE3C0C, + 0xEC68D02B,0x4A1FDB9F,0x7BF7C102,0xDD80CAB6,0x1827F438,0xBE50FF8C,0x8FB8E511,0x29CFEEA5, + 0xDF879E4C,0x79F095F8,0x48188F65,0xEE6F84D1,0x2BC8BA5F,0x8DBFB1EB,0xBC57AB76,0x1A20A0C2, + 0x8816EAF2,0x2E61E146,0x1F89FBDB,0xB9FEF06F,0x7C59CEE1,0xDA2EC555,0xEBC6DFC8,0x4DB1D47C, + 0xBBF9A495,0x1D8EAF21,0x2C66B5BC,0x8A11BE08,0x4FB68086,0xE9C18B32,0xD82991AF,0x7E5E9A1B, + 0xEFC8763C,0x49BF7D88,0x78576715,0xDE206CA1,0x1B87522F,0xBDF0599B,0x8C184306,0x2A6F48B2, + 0xDC27385B,0x7A5033EF,0x4BB82972,0xEDCF22C6,0x28681C48,0x8E1F17FC,0xBFF70D61,0x198006D5, + 0x47ABD36E,0xE1DCD8DA,0xD034C247,0x7643C9F3,0xB3E4F77D,0x1593FCC9,0x247BE654,0x820CEDE0, + 0x74449D09,0xD23396BD,0xE3DB8C20,0x45AC8794,0x800BB91A,0x267CB2AE,0x1794A833,0xB1E3A387, + 0x20754FA0,0x86024414,0xB7EA5E89,0x119D553D,0xD43A6BB3,0x724D6007,0x43A57A9A,0xE5D2712E, + 0x139A01C7,0xB5ED0A73,0x840510EE,0x22721B5A,0xE7D525D4,0x41A22E60,0x704A34FD,0xD63D3F49, + 0xCC1D9F8B,0x6A6A943F,0x5B828EA2,0xFDF58516,0x3852BB98,0x9E25B02C,0xAFCDAAB1,0x09BAA105, + 0xFFF2D1EC,0x5985DA58,0x686DC0C5,0xCE1ACB71,0x0BBDF5FF,0xADCAFE4B,0x9C22E4D6,0x3A55EF62, + 0xABC30345,0x0DB408F1,0x3C5C126C,0x9A2B19D8,0x5F8C2756,0xF9FB2CE2,0xC813367F,0x6E643DCB, + 0x982C4D22,0x3E5B4696,0x0FB35C0B,0xA9C457BF,0x6C636931,0xCA146285,0xFBFC7818,0x5D8B73AC, + 0x03A0A617,0xA5D7ADA3,0x943FB73E,0x3248BC8A,0xF7EF8204,0x519889B0,0x6070932D,0xC6079899, + 0x304FE870,0x9638E3C4,0xA7D0F959,0x01A7F2ED,0xC400CC63,0x6277C7D7,0x539FDD4A,0xF5E8D6FE, + 0x647E3AD9,0xC209316D,0xF3E12BF0,0x55962044,0x90311ECA,0x3646157E,0x07AE0FE3,0xA1D90457, + 0x579174BE,0xF1E67F0A,0xC00E6597,0x66796E23,0xA3DE50AD,0x05A95B19,0x34414184,0x92364A30, + }, + + { + 0x00000000,0xCCAA009E,0x4225077D,0x8E8F07E3,0x844A0EFA,0x48E00E64,0xC66F0987,0x0AC50919, + 0xD3E51BB5,0x1F4F1B2B,0x91C01CC8,0x5D6A1C56,0x57AF154F,0x9B0515D1,0x158A1232,0xD92012AC, + 0x7CBB312B,0xB01131B5,0x3E9E3656,0xF23436C8,0xF8F13FD1,0x345B3F4F,0xBAD438AC,0x767E3832, + 0xAF5E2A9E,0x63F42A00,0xED7B2DE3,0x21D12D7D,0x2B142464,0xE7BE24FA,0x69312319,0xA59B2387, + 0xF9766256,0x35DC62C8,0xBB53652B,0x77F965B5,0x7D3C6CAC,0xB1966C32,0x3F196BD1,0xF3B36B4F, + 0x2A9379E3,0xE639797D,0x68B67E9E,0xA41C7E00,0xAED97719,0x62737787,0xECFC7064,0x205670FA, + 0x85CD537D,0x496753E3,0xC7E85400,0x0B42549E,0x01875D87,0xCD2D5D19,0x43A25AFA,0x8F085A64, + 0x562848C8,0x9A824856,0x140D4FB5,0xD8A74F2B,0xD2624632,0x1EC846AC,0x9047414F,0x5CED41D1, + 0x299DC2ED,0xE537C273,0x6BB8C590,0xA712C50E,0xADD7CC17,0x617DCC89,0xEFF2CB6A,0x2358CBF4, + 0xFA78D958,0x36D2D9C6,0xB85DDE25,0x74F7DEBB,0x7E32D7A2,0xB298D73C,0x3C17D0DF,0xF0BDD041, + 0x5526F3C6,0x998CF358,0x1703F4BB,0xDBA9F425,0xD16CFD3C,0x1DC6FDA2,0x9349FA41,0x5FE3FADF, + 0x86C3E873,0x4A69E8ED,0xC4E6EF0E,0x084CEF90,0x0289E689,0xCE23E617,0x40ACE1F4,0x8C06E16A, + 0xD0EBA0BB,0x1C41A025,0x92CEA7C6,0x5E64A758,0x54A1AE41,0x980BAEDF,0x1684A93C,0xDA2EA9A2, + 0x030EBB0E,0xCFA4BB90,0x412BBC73,0x8D81BCED,0x8744B5F4,0x4BEEB56A,0xC561B289,0x09CBB217, + 0xAC509190,0x60FA910E,0xEE7596ED,0x22DF9673,0x281A9F6A,0xE4B09FF4,0x6A3F9817,0xA6959889, + 0x7FB58A25,0xB31F8ABB,0x3D908D58,0xF13A8DC6,0xFBFF84DF,0x37558441,0xB9DA83A2,0x7570833C, + 0x533B85DA,0x9F918544,0x111E82A7,0xDDB48239,0xD7718B20,0x1BDB8BBE,0x95548C5D,0x59FE8CC3, + 0x80DE9E6F,0x4C749EF1,0xC2FB9912,0x0E51998C,0x04949095,0xC83E900B,0x46B197E8,0x8A1B9776, + 0x2F80B4F1,0xE32AB46F,0x6DA5B38C,0xA10FB312,0xABCABA0B,0x6760BA95,0xE9EFBD76,0x2545BDE8, + 0xFC65AF44,0x30CFAFDA,0xBE40A839,0x72EAA8A7,0x782FA1BE,0xB485A120,0x3A0AA6C3,0xF6A0A65D, + 0xAA4DE78C,0x66E7E712,0xE868E0F1,0x24C2E06F,0x2E07E976,0xE2ADE9E8,0x6C22EE0B,0xA088EE95, + 0x79A8FC39,0xB502FCA7,0x3B8DFB44,0xF727FBDA,0xFDE2F2C3,0x3148F25D,0xBFC7F5BE,0x736DF520, + 0xD6F6D6A7,0x1A5CD639,0x94D3D1DA,0x5879D144,0x52BCD85D,0x9E16D8C3,0x1099DF20,0xDC33DFBE, + 0x0513CD12,0xC9B9CD8C,0x4736CA6F,0x8B9CCAF1,0x8159C3E8,0x4DF3C376,0xC37CC495,0x0FD6C40B, + 0x7AA64737,0xB60C47A9,0x3883404A,0xF42940D4,0xFEEC49CD,0x32464953,0xBCC94EB0,0x70634E2E, + 0xA9435C82,0x65E95C1C,0xEB665BFF,0x27CC5B61,0x2D095278,0xE1A352E6,0x6F2C5505,0xA386559B, + 0x061D761C,0xCAB77682,0x44387161,0x889271FF,0x825778E6,0x4EFD7878,0xC0727F9B,0x0CD87F05, + 0xD5F86DA9,0x19526D37,0x97DD6AD4,0x5B776A4A,0x51B26353,0x9D1863CD,0x1397642E,0xDF3D64B0, + 0x83D02561,0x4F7A25FF,0xC1F5221C,0x0D5F2282,0x079A2B9B,0xCB302B05,0x45BF2CE6,0x89152C78, + 0x50353ED4,0x9C9F3E4A,0x121039A9,0xDEBA3937,0xD47F302E,0x18D530B0,0x965A3753,0x5AF037CD, + 0xFF6B144A,0x33C114D4,0xBD4E1337,0x71E413A9,0x7B211AB0,0xB78B1A2E,0x39041DCD,0xF5AE1D53, + 0x2C8E0FFF,0xE0240F61,0x6EAB0882,0xA201081C,0xA8C40105,0x646E019B,0xEAE10678,0x264B06E6, + } +}; + +/// compute CRC32 (Slicing-by-8 algorithm), unroll inner loop 4 times +uint32_t crc32_slice_by_8(const void* data, size_t length, uint32_t previousCrc32) +{ + uint32_t crc = ~previousCrc32; // same as previousCrc32 ^ 0xFFFFFFFF + const uint32_t* current = (const uint32_t*) data; + + // enabling optimization (at least -O2) automatically unrolls the inner for-loop + const size_t Unroll = 4; + const size_t BytesAtOnce = 8 * Unroll; + + // process 4x eight bytes at once (Slicing-by-8) + while (length >= BytesAtOnce) + { + size_t unrolling; + for (unrolling = 0; unrolling < Unroll; unrolling++) + { +#if __BYTE_ORDER == __BIG_ENDIAN + uint32_t one = *current++ ^ swap(crc); + uint32_t two = *current++; + crc = Crc32Lookup[0][ two & 0xFF] ^ + Crc32Lookup[1][(two>> 8) & 0xFF] ^ + Crc32Lookup[2][(two>>16) & 0xFF] ^ + Crc32Lookup[3][(two>>24) & 0xFF] ^ + Crc32Lookup[4][ one & 0xFF] ^ + Crc32Lookup[5][(one>> 8) & 0xFF] ^ + Crc32Lookup[6][(one>>16) & 0xFF] ^ + Crc32Lookup[7][(one>>24) & 0xFF]; +#else + uint32_t one = *current++ ^ crc; + uint32_t two = *current++; + crc = Crc32Lookup[0][(two>>24) & 0xFF] ^ + Crc32Lookup[1][(two>>16) & 0xFF] ^ + Crc32Lookup[2][(two>> 8) & 0xFF] ^ + Crc32Lookup[3][ two & 0xFF] ^ + Crc32Lookup[4][(one>>24) & 0xFF] ^ + Crc32Lookup[5][(one>>16) & 0xFF] ^ + Crc32Lookup[6][(one>> 8) & 0xFF] ^ + Crc32Lookup[7][ one & 0xFF]; +#endif + + } + + length -= BytesAtOnce; + } + + const uint8_t* currentChar = (const uint8_t*) current; + // remaining 1 to 31 bytes (standard algorithm) + while (length-- != 0) + crc = (crc >> 8) ^ Crc32Lookup[0][(crc & 0xFF) ^ *currentChar++]; + + return ~crc; // same as crc ^ 0xFFFFFFFF +} diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e5bc3a8f..8d680dc5 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -27,6 +27,7 @@ from .archive import Archive, ArchiveChecker, ArchiveRecreater, Statistics, is_s from .archive import BackupOSError from .cache import Cache from .constants import * # NOQA +from .crc32 import crc32 from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from .helpers import Error, NoManifestError from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec @@ -1190,6 +1191,9 @@ class Archiver: def do_debug_info(self, args): """display system information for debugging / bug reports""" print(sysinfo()) + + # Additional debug information + print('CRC implementation:', crc32.__name__) return EXIT_SUCCESS @with_repository() diff --git a/src/borg/crc32.pyx b/src/borg/crc32.pyx new file mode 100644 index 00000000..4854e38f --- /dev/null +++ b/src/borg/crc32.pyx @@ -0,0 +1,41 @@ + +from libc.stdint cimport uint32_t +from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release + + +cdef extern from "_crc32/crc32.c": + uint32_t _crc32_slice_by_8 "crc32_slice_by_8"(const void* data, size_t length, uint32_t initial_crc) + uint32_t _crc32_clmul "crc32_clmul"(const void* data, size_t length, uint32_t initial_crc) + + int _have_clmul "have_clmul"() + + +cdef Py_buffer ro_buffer(object data) except *: + cdef Py_buffer view + PyObject_GetBuffer(data, &view, PyBUF_SIMPLE) + return view + + +def crc32_slice_by_8(data, value=0): + cdef Py_buffer data_buf = ro_buffer(data) + cdef uint32_t val = value + try: + return _crc32_slice_by_8(data_buf.buf, data_buf.len, val) + finally: + PyBuffer_Release(&data_buf) + + +def crc32_clmul(data, value=0): + cdef Py_buffer data_buf = ro_buffer(data) + cdef uint32_t val = value + try: + return _crc32_clmul(data_buf.buf, data_buf.len, val) + finally: + PyBuffer_Release(&data_buf) + + +have_clmul = _have_clmul() +if have_clmul: + crc32 = crc32_clmul +else: + crc32 = crc32_slice_by_8 diff --git a/src/borg/repository.py b/src/borg/repository.py index fda085a6..20ae274e 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -8,7 +8,6 @@ from configparser import ConfigParser from datetime import datetime from functools import partial from itertools import islice -from zlib import crc32 import msgpack @@ -26,6 +25,7 @@ from .locking import Lock, LockError, LockErrorT from .logger import create_logger from .lrucache import LRUCache from .platform import SaveFile, SyncFile, sync_dir +from .crc32 import crc32 MAX_OBJECT_SIZE = 20 * 1024 * 1024 MAGIC = b'BORG_SEG' diff --git a/src/borg/testsuite/crc32.py b/src/borg/testsuite/crc32.py new file mode 100644 index 00000000..4faed809 --- /dev/null +++ b/src/borg/testsuite/crc32.py @@ -0,0 +1,21 @@ +import os +import zlib + +import pytest + +from .. import crc32 + +crc32_implementations = [crc32.crc32_slice_by_8] +if crc32.have_clmul: + crc32_implementations.append(crc32.crc32_clmul) + + +@pytest.mark.parametrize('implementation', crc32_implementations) +def test_crc32(implementation): + # This includes many critical values, like zero length, 3/4/5, 6/7/8 and so on which are near and on + # alignment boundaries. This is of course just a sanity check ie. "did it compile all right?". + data = os.urandom(256) + initial_crc = 0x12345678 + for i in range(0, 256): + d = data[:i] + assert zlib.crc32(d, initial_crc) == implementation(d, initial_crc) From 56818d1db8e23e4f08232a3966253bdd572a3e65 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Dec 2016 18:04:38 +0100 Subject: [PATCH 0516/1387] Credit the excellent work of these people in AUTHORS --- AUTHORS | 18 ++++++++++++++++++ src/borg/_crc32/clmul.c | 18 ++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index bc7a8c7c..46cbc447 100644 --- a/AUTHORS +++ b/AUTHORS @@ -31,3 +31,21 @@ Attic Patches and Suggestions - Johann Klähn - Petros Moisiadis - Thomas Waldmann + +BLAKE2 +------ + +Borg includes BLAKE2: Copyright 2012, Samuel Neves , licensed under the terms +of the CC0, the OpenSSL Licence, or the Apache Public License 2.0. + +Slicing CRC32 +------------- + +Borg includes a fast slice-by-8 implementation of CRC32, Copyright 2011-2015 Stephan Brumme, +licensed under the terms of a zlib license. See http://create.stephan-brumme.com/crc32/ + +Folding CRC32 +------------- + +Borg includes an extremely fast folding implementation of CRC32, Copyright 2013 Intel Corporation, +licensed under the terms of the zlib license. diff --git a/src/borg/_crc32/clmul.c b/src/borg/_crc32/clmul.c index 64fa613a..4b84b035 100644 --- a/src/borg/_crc32/clmul.c +++ b/src/borg/_crc32/clmul.c @@ -13,9 +13,23 @@ * Erdinc Ozturk * Jim Kukunas * - * For conditions of distribution and use, see copyright notice in zlib.h - * * Copyright (c) 2016 Marian Beermann (add support for initial value, restructuring) + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. */ #include From 85e79f96a134c93e14873de8248021eeb5551ce4 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 3 Jan 2017 12:47:42 +0100 Subject: [PATCH 0517/1387] xattr: ignore empty names returned by llistxattr(2) et al --- borg/xattr.py | 6 +++--- docs/changes.rst | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/borg/xattr.py b/borg/xattr.py index 50d1b0b8..c408268a 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -205,7 +205,7 @@ if sys.platform.startswith('linux'): # pragma: linux only n, buf = _listxattr_inner(func, path) return [os.fsdecode(name) for name in split_string0(buf[:n]) - if not name.startswith(b'system.posix_acl_')] + if name and not name.startswith(b'system.posix_acl_')] def getxattr(path, name, *, follow_symlinks=True): def func(path, name, buf, size): @@ -261,7 +261,7 @@ elif sys.platform == 'darwin': # pragma: darwin only return libc.listxattr(path, buf, size, XATTR_NOFOLLOW) n, buf = _listxattr_inner(func, path) - return [os.fsdecode(name) for name in split_string0(buf[:n])] + return [os.fsdecode(name) for name in split_string0(buf[:n]) if name] def getxattr(path, name, *, follow_symlinks=True): def func(path, name, buf, size): @@ -320,7 +320,7 @@ elif sys.platform.startswith('freebsd'): # pragma: freebsd only return libc.extattr_list_link(path, ns, buf, size) n, buf = _listxattr_inner(func, path) - return [os.fsdecode(name) for name in split_lstring(buf[:n])] + return [os.fsdecode(name) for name in split_lstring(buf[:n]) if name] def getxattr(path, name, *, follow_symlinks=True): def func(path, name, buf, size): diff --git a/docs/changes.rst b/docs/changes.rst index 209893bb..2a860f07 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -126,6 +126,13 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= +Version 1.0.10rc1 (not released yet) +------------------------------------ + +Bug fixes: + +- Avoid triggering an ObjectiveFS bug in xattr retrieval, #1992 + Version 1.0.9 (2016-12-20) -------------------------- From 3e04fa972ad499f621474c4a980f9ccf1b66fe3c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 3 Jan 2017 14:25:55 +0100 Subject: [PATCH 0518/1387] xattr: only skip file on BufferTooSmallError redefine __str__ to get a proper error message, not '' --- borg/xattr.py | 9 ++++++--- docs/changes.rst | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/borg/xattr.py b/borg/xattr.py index c408268a..0c1e9f0e 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -10,7 +10,7 @@ from ctypes import CDLL, create_string_buffer, c_ssize_t, c_size_t, c_char_p, c_ from ctypes.util import find_library from distutils.version import LooseVersion -from .helpers import Buffer +from .helpers import Buffer, Error try: @@ -113,8 +113,11 @@ def split_lstring(buf): return result -class BufferTooSmallError(Exception): - """the buffer given to an xattr function was too small for the result""" +class BufferTooSmallError(OSError): + """insufficient buffer memory for completing a xattr operation.""" + + def __str__(self): + return self.__doc__ def _check(rv, path=None, detect_buffer_too_small=False): diff --git a/docs/changes.rst b/docs/changes.rst index 2a860f07..7e3c876f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -132,6 +132,7 @@ Version 1.0.10rc1 (not released yet) Bug fixes: - Avoid triggering an ObjectiveFS bug in xattr retrieval, #1992 +- When running out of buffer memory when reading xattrs, only skip the current file, #1993 Version 1.0.9 (2016-12-20) -------------------------- From 6a5b3018c1c1d3c8db13dd93bb22c7297ce78afd Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 3 Jan 2017 17:15:32 +0100 Subject: [PATCH 0519/1387] fix upgrade --tam crashing if repository is not encrypted --- borg/archiver.py | 4 ++++ docs/changes.rst | 2 ++ 2 files changed, 6 insertions(+) diff --git a/borg/archiver.py b/borg/archiver.py index 7ad2195d..973ea6f7 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -730,6 +730,10 @@ class Archiver: if args.tam: manifest, key = Manifest.load(repository, force_tam_not_required=args.force) + if not hasattr(key, 'change_passphrase'): + print('This repository is not encrypted, cannot enable TAM.') + return EXIT_ERROR + if not manifest.tam_verified or not manifest.config.get(b'tam_required', False): # The standard archive listing doesn't include the archive ID like in borg 1.1.x print('Manifest contents:') diff --git a/docs/changes.rst b/docs/changes.rst index 7e3c876f..ae471af8 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -133,6 +133,8 @@ Bug fixes: - Avoid triggering an ObjectiveFS bug in xattr retrieval, #1992 - When running out of buffer memory when reading xattrs, only skip the current file, #1993 +- Fixed "borg upgrade --tam" crashing with unencrypted repositories. Since :ref:`the issue ` is + not relevant for unencrypted repositories, it now does nothing and prints an error, #1981. Version 1.0.9 (2016-12-20) -------------------------- From 7519bf8100a3aa9d664d39f2d4f0dd7a1ff7b10e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 3 Jan 2017 17:15:59 +0100 Subject: [PATCH 0520/1387] fix change-passphrase crashing if repository is not encrypted --- borg/archiver.py | 3 +++ docs/changes.rst | 1 + 2 files changed, 4 insertions(+) diff --git a/borg/archiver.py b/borg/archiver.py index 973ea6f7..1adbba89 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -177,6 +177,9 @@ class Archiver: @with_repository() def do_change_passphrase(self, args, repository, manifest, key): """Change repository key file passphrase""" + if not hasattr(key, 'change_passphrase'): + print('This repository is not encrypted, cannot change the passphrase.') + return EXIT_ERROR key.change_passphrase() logger.info('Key updated') return EXIT_SUCCESS diff --git a/docs/changes.rst b/docs/changes.rst index ae471af8..30124600 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -135,6 +135,7 @@ Bug fixes: - When running out of buffer memory when reading xattrs, only skip the current file, #1993 - Fixed "borg upgrade --tam" crashing with unencrypted repositories. Since :ref:`the issue ` is not relevant for unencrypted repositories, it now does nothing and prints an error, #1981. +- Fixed change-passphrase crashing with unencrypted repositories, #1978 Version 1.0.9 (2016-12-20) -------------------------- From 4b9a9f9b5ec8fd54be78569b2e5e9201a2a9dada Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 3 Jan 2017 13:00:37 +0100 Subject: [PATCH 0521/1387] change-passphrase: print key location --- borg/archiver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/borg/archiver.py b/borg/archiver.py index 1adbba89..c8c80f37 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -182,6 +182,9 @@ class Archiver: return EXIT_ERROR key.change_passphrase() logger.info('Key updated') + if hasattr(key, 'find_key'): + # print key location to make backing it up easier + logger.info('Key location: %s', key.find_key()) return EXIT_SUCCESS @with_repository(lock=False, exclusive=False, manifest=False, cache=False) From 95334930475b70f7e5cbf21314093bdc088d9d75 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 4 Jan 2017 00:57:35 +0100 Subject: [PATCH 0522/1387] tox / travis: also test on Python 3.6 --- .travis.yml | 8 ++++++++ tox.ini | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 36c38d63..e1433a17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,10 @@ matrix: os: linux dist: trusty env: TOXENV=py35 + - python: 3.6 + os: linux + dist: trusty + env: TOXENV=py36 - python: 3.5 os: linux dist: trusty @@ -28,6 +32,10 @@ matrix: os: osx osx_image: xcode6.4 env: TOXENV=py35 + - language: generic + os: osx + osx_image: xcode6.4 + env: TOXENV=py36 allow_failures: - os: osx diff --git a/tox.ini b/tox.ini index 699ef251..d44009e4 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ # fakeroot -u tox --recreate [tox] -envlist = py{34,35},flake8 +envlist = py{34,35,36},flake8 [testenv] # Change dir to avoid import problem for cython code. The directory does From 7d4d7e79012ea4798f7281a34ad9579ebf4de16d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 4 Jan 2017 01:02:25 +0100 Subject: [PATCH 0523/1387] setup.py: add Python 3.6 qualifier --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 00e48f3f..4f2ca868 100644 --- a/setup.py +++ b/setup.py @@ -292,6 +292,7 @@ setup( 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Security :: Cryptography', 'Topic :: System :: Archiving :: Backup', ], From c412b86455ced36a3b9e77c2b91143381f8096b6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 4 Jan 2017 01:06:57 +0100 Subject: [PATCH 0524/1387] vagrant: add Python 3.6.0 --- Vagrantfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index df636ce3..26dcfc04 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -223,6 +223,7 @@ def install_pythons(boxname) . ~/.bash_profile pyenv install 3.4.0 # tests pyenv install 3.5.0 # tests + pyenv install 3.6.0 # tests pyenv install 3.5.2 # binary build, use latest 3.5.x release pyenv rehash EOF @@ -315,7 +316,7 @@ def run_tests(boxname) . ../borg-env/bin/activate if which pyenv 2> /dev/null; then # for testing, use the earliest point releases of the supported python versions: - pyenv global 3.4.0 3.5.0 + pyenv global 3.4.0 3.5.0 3.6.0 fi # otherwise: just use the system python if which fakeroot 2> /dev/null; then From be8e0c89b3d4a59ecf01be09876a58066c8d4c4a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 4 Jan 2017 19:25:03 +0100 Subject: [PATCH 0525/1387] check: fail if single archive does not exist --- borg/archive.py | 4 ++++ docs/changes.rst | 1 + 2 files changed, 5 insertions(+) diff --git a/borg/archive.py b/borg/archive.py index baf21233..afc734ce 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -1107,6 +1107,10 @@ class ArchiveChecker: archive_items = [item for item in self.manifest.archives.items() if item[0] == archive] num_archives = 1 end = 1 + if not archive_items: + logger.error('Archive %s does not exist', archive) + self.error_found = True + return with cache_if_remote(self.repository) as repository: for i, (name, info) in enumerate(archive_items[:end]): diff --git a/docs/changes.rst b/docs/changes.rst index 30124600..a26e0040 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -136,6 +136,7 @@ Bug fixes: - Fixed "borg upgrade --tam" crashing with unencrypted repositories. Since :ref:`the issue ` is not relevant for unencrypted repositories, it now does nothing and prints an error, #1981. - Fixed change-passphrase crashing with unencrypted repositories, #1978 +- Fixed "borg check repo::archive" indicating success if "archive" does not exist, #1997 Version 1.0.9 (2016-12-20) -------------------------- From 320a56144fa5228562c2211b29754ce18690c4e2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 3 Jan 2017 17:38:18 +0100 Subject: [PATCH 0526/1387] helpers.Buffer: raise OSError subclass if too much memory is allocd --- borg/helpers.py | 14 ++++++++++++-- borg/testsuite/helpers.py | 4 ++-- borg/xattr.py | 9 +++------ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/borg/helpers.py b/borg/helpers.py index b38ec945..7c4a0fa9 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -55,9 +55,15 @@ class Error(Exception): # show a traceback? traceback = False + def __init__(self, *args): + super().__init__(*args) + self.args = args + def get_message(self): return type(self).__doc__.format(*self.args) + __str__ = get_message + class ErrorWithTraceback(Error): """like Error, but show a traceback also""" @@ -699,6 +705,10 @@ class Buffer: """ provide a thread-local buffer """ + + class MemoryLimitExceeded(Error, OSError): + """Requested buffer size {} is above the limit of {}.""" + def __init__(self, allocator, size=4096, limit=None): """ Initialize the buffer: use allocator(size) call to allocate a buffer. @@ -718,11 +728,11 @@ class Buffer: """ resize the buffer - to avoid frequent reallocation, we usually always grow (if needed). giving init=True it is possible to first-time initialize or shrink the buffer. - if a buffer size beyond the limit is requested, raise ValueError. + if a buffer size beyond the limit is requested, raise Buffer.MemoryLimitExceeded (OSError). """ size = int(size) if self.limit is not None and size > self.limit: - raise ValueError('Requested buffer size %d is above the limit of %d.' % (size, self.limit)) + raise Buffer.MemoryLimitExceeded(size, self.limit) if init or len(self) < size: self._thread_local.buffer = self.allocator(size) diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py index 47d49f99..3f9c7096 100644 --- a/borg/testsuite/helpers.py +++ b/borg/testsuite/helpers.py @@ -793,7 +793,7 @@ class TestBuffer: buffer = Buffer(bytearray, size=100, limit=200) buffer.resize(200) assert len(buffer) == 200 - with pytest.raises(ValueError): + with pytest.raises(Buffer.MemoryLimitExceeded): buffer.resize(201) assert len(buffer) == 200 @@ -807,7 +807,7 @@ class TestBuffer: b3 = buffer.get(200) assert len(b3) == 200 assert b3 is not b2 # new, resized buffer - with pytest.raises(ValueError): + with pytest.raises(Buffer.MemoryLimitExceeded): buffer.get(201) # beyond limit assert len(buffer) == 200 diff --git a/borg/xattr.py b/borg/xattr.py index 0c1e9f0e..75cb9189 100644 --- a/borg/xattr.py +++ b/borg/xattr.py @@ -10,7 +10,7 @@ from ctypes import CDLL, create_string_buffer, c_ssize_t, c_size_t, c_char_p, c_ from ctypes.util import find_library from distutils.version import LooseVersion -from .helpers import Buffer, Error +from .helpers import Buffer try: @@ -113,11 +113,8 @@ def split_lstring(buf): return result -class BufferTooSmallError(OSError): - """insufficient buffer memory for completing a xattr operation.""" - - def __str__(self): - return self.__doc__ +class BufferTooSmallError(Exception): + """the buffer given to an xattr function was too small for the result.""" def _check(rv, path=None, detect_buffer_too_small=False): From 853cfb703b98400177ebeb01b0df2b29710766ac Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 3 Jan 2017 04:26:04 +0100 Subject: [PATCH 0527/1387] parallelizing tests via pytest-xdist --- requirements.d/development.txt | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.d/development.txt b/requirements.d/development.txt index a0cb3c2a..a07f0b02 100644 --- a/requirements.d/development.txt +++ b/requirements.d/development.txt @@ -1,6 +1,7 @@ virtualenv<14.0 tox pytest +pytest-xdist pytest-cov pytest-benchmark Cython diff --git a/tox.ini b/tox.ini index d44009e4..ce0aaacb 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ deps = -rrequirements.d/development.txt -rrequirements.d/attic.txt -rrequirements.d/fuse.txt -commands = py.test -rs --cov=borg --cov-config=../.coveragerc --benchmark-skip --pyargs {posargs:borg.testsuite} +commands = py.test -n 8 -rs --cov=borg --cov-config=../.coveragerc --benchmark-skip --pyargs {posargs:borg.testsuite} # fakeroot -u needs some env vars: passenv = * From 5ed6d213028082cb094bb0e8661ac52f677fb571 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 3 Jan 2017 04:27:51 +0100 Subject: [PATCH 0528/1387] parallel testing: fix issue related to non-reproducible set / dict order --- borg/testsuite/archive.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/borg/testsuite/archive.py b/borg/testsuite/archive.py index 2dbdd7bc..65b3f78b 100644 --- a/borg/testsuite/archive.py +++ b/borg/testsuite/archive.py @@ -1,3 +1,4 @@ +from collections import OrderedDict from datetime import datetime, timezone from unittest.mock import Mock @@ -131,11 +132,15 @@ def test_invalid_msgpacked_item(packed, item_keys_serialized): assert not valid_msgpacked_dict(packed, item_keys_serialized) +# pytest-xdist requires always same order for the keys and dicts: +IK = sorted(list(ITEM_KEYS)) + + @pytest.mark.parametrize('packed', [msgpack.packb(o) for o in [ {b'path': b'/a/b/c'}, # small (different msgpack mapping type!) - dict((k, b'') for k in ITEM_KEYS), # as big (key count) as it gets - dict((k, b'x' * 1000) for k in ITEM_KEYS), # as big (key count and volume) as it gets + OrderedDict((k, b'') for k in IK), # as big (key count) as it gets + OrderedDict((k, b'x' * 1000) for k in IK), # as big (key count and volume) as it gets ]]) def test_valid_msgpacked_items(packed, item_keys_serialized): assert valid_msgpacked_dict(packed, item_keys_serialized) From a1d223cec0839be47f338ce4d13a317016ffbcb8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 5 Jan 2017 03:47:13 +0100 Subject: [PATCH 0529/1387] always setup module level "logger" in the same way this is a cleanup change, found this while trying to find out why borg_cmd spuriously does not have INFO loglevel when testing with pytest-xdist. the cleanup did NOT help with this, but is at least a cleanup. --- borg/repository.py | 7 ++++--- borg/upgrader.py | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/borg/repository.py b/borg/repository.py index 690e7770..dd49b368 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -3,15 +3,16 @@ from binascii import unhexlify from datetime import datetime from itertools import islice import errno -import logging -logger = logging.getLogger(__name__) - import os import shutil import struct from zlib import crc32 import msgpack + +from .logger import create_logger +logger = create_logger() + from .helpers import Error, ErrorWithTraceback, IntegrityError, Location, ProgressIndicatorPercent, bin_to_hex from .hashindex import NSIndex from .locking import Lock, LockError, LockErrorT diff --git a/borg/upgrader.py b/borg/upgrader.py index c6700262..208e7f7b 100644 --- a/borg/upgrader.py +++ b/borg/upgrader.py @@ -1,10 +1,11 @@ import datetime -import logging -logger = logging.getLogger(__name__) import os import shutil import time +from .logger import create_logger +logger = create_logger() + from .helpers import get_keys_dir, get_cache_dir, ProgressIndicatorPercent, bin_to_hex from .locking import Lock from .repository import Repository, MAGIC From 2938a5f6fbb437adced4b88357d30e7ee9f5d29b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 5 Jan 2017 03:59:33 +0100 Subject: [PATCH 0530/1387] work around spurious log level related test fail when using pytest-xdist --- borg/testsuite/repository.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/borg/testsuite/repository.py b/borg/testsuite/repository.py index e217137a..713b03cf 100644 --- a/borg/testsuite/repository.py +++ b/borg/testsuite/repository.py @@ -1,3 +1,4 @@ +import logging import os import shutil import sys @@ -440,6 +441,8 @@ class RemoteRepositoryTestCase(RepositoryTestCase): assert self.repository.borg_cmd(None, testing=True) == [sys.executable, '-m', 'borg.archiver', 'serve'] args = MockArgs() + # XXX without next line we get spurious test fails when using pytest-xdist, root cause unknown: + logging.getLogger().setLevel(logging.INFO) # note: test logger is on info log level, so --info gets added automagically assert self.repository.borg_cmd(args, testing=False) == ['borg', 'serve', '--umask=077', '--info'] args.remote_path = 'borg-0.28.2' From 370cb1f19a9ba8b5191000e96350482e593749e7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 6 Jan 2017 06:19:26 +0100 Subject: [PATCH 0531/1387] travis: fix osxfuse install --- .travis/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis/install.sh b/.travis/install.sh index 92f14e21..71026d5d 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -17,7 +17,7 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then brew install lz4 brew outdated pyenv || brew upgrade pyenv brew install pkg-config - brew install Caskroom/versions/osxfuse + brew install Caskroom/cask/osxfuse case "${TOXENV}" in py34) From e119042f4c7693592619a9b26e6424391b1ad25b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 6 Jan 2017 06:49:40 +0100 Subject: [PATCH 0532/1387] travis: install py36 on OS X --- .travis/install.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis/install.sh b/.travis/install.sh index 71026d5d..d9025437 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -28,6 +28,10 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then pyenv install 3.5.1 pyenv global 3.5.1 ;; + py36) + pyenv install 3.6.0 + pyenv global 3.6.0 + ;; esac pyenv rehash python -m pip install --user 'virtualenv<14.0' From 69b816fe763e3303dfca378eb4b9da0abf484db6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 7 Jan 2017 23:12:27 +0100 Subject: [PATCH 0533/1387] update CHANGES (1.0-maint) --- docs/changes.rst | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 30124600..23f9174e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -132,11 +132,27 @@ Version 1.0.10rc1 (not released yet) Bug fixes: - Avoid triggering an ObjectiveFS bug in xattr retrieval, #1992 -- When running out of buffer memory when reading xattrs, only skip the current file, #1993 -- Fixed "borg upgrade --tam" crashing with unencrypted repositories. Since :ref:`the issue ` is - not relevant for unencrypted repositories, it now does nothing and prints an error, #1981. +- When running out of buffer memory when reading xattrs, only skip the + current file, #1993 +- Fixed "borg upgrade --tam" crashing with unencrypted repositories. Since + :ref:`the issue ` is not relevant for unencrypted repositories, + it now does nothing and prints an error, #1981. - Fixed change-passphrase crashing with unencrypted repositories, #1978 +Other changes: + +- xattr: ignore empty names returned by llistxattr(2) et al +- Enable the fault handler: install handlers for the SIGSEGV, SIGFPE, SIGABRT, + SIGBUS and SIGILL signals to dump the Python traceback. +- Also print a traceback on SIGUSR2. +- borg change-passphrase: print key location (simplify making a backup of it) +- officially support Python 3.6 (setup.py: add Python 3.6 qualifier) +- tests: + + - vagrant / travis / tox: add Python 3.6 based testing + - travis: fix osxfuse install (fixes OS X testing on Travis CI) + + Version 1.0.9 (2016-12-20) -------------------------- From 33be583920b8880da829b512066f64e1131ad769 Mon Sep 17 00:00:00 2001 From: sherbang Date: Sat, 7 Jan 2017 18:34:11 -0500 Subject: [PATCH 0534/1387] Update faq.rst --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index ed4b6f76..f0ae2fe7 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -13,7 +13,7 @@ Yes, the `deduplication`_ technique used by Also, we have optional simple sparse file support for extract. If you use non-snapshotting backup tools like Borg to back up virtual machines, -then these should be turned off for doing so. Backing up live VMs this way can (and will) +then the VMs should be turned off for the duration of the backup. Backing up live VMs can (and will) result in corrupted or inconsistent backup contents: a VM image is just a regular file to Borg with the same issues as regular files when it comes to concurrent reading and writing from the same file. From 0f7493299c2a81297049571f3091f22fe64f2ca6 Mon Sep 17 00:00:00 2001 From: Hartmut Goebel Date: Sun, 8 Jan 2017 22:16:54 +0100 Subject: [PATCH 0535/1387] Vagrantfile: Split cygwin packaged to be installed into base and project. The packages which are required for ssh log-in and rsync synchronized folders to work are separated from those specific for the borg project. This makes it easier to reuse in other projects. --- Vagrantfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 81e9ac9c..8aa3aef0 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -174,8 +174,9 @@ def packages_cygwin(version) set OURPATH=%cd% set CYGBUILD="C:\\cygwin\\CygWin" set CYGMIRROR=ftp://mirrors.kernel.org/sourceware/cygwin/ - set BUILDPKGS=python3,python3-setuptools,binutils,gcc-g++,libopenssl,openssl-devel,git,make,openssh,liblz4-devel,liblz4_1,rsync,curl,python-devel - %CYGSETUP% -q -B -o -n -R %CYGBUILD% -L -D -s %CYGMIRROR% -P %BUILDPKGS% + set BASEPKGS=openssh,rsync + set BUILDPKGS=python3,python3-setuptools,python-devel,binutils,gcc-g++,libopenssl,openssl-devel,git,make,liblz4-devel,liblz4_1,curl + %CYGSETUP% -q -B -o -n -R %CYGBUILD% -L -D -s %CYGMIRROR% -P %BASEPKGS%,%BUILDPKGS% cd /d C:\\cygwin\\CygWin\\bin regtool set /HKLM/SYSTEM/CurrentControlSet/Services/OpenSSHd/ImagePath "C:\\cygwin\\CygWin\\bin\\cygrunsrv.exe" bash -c "ssh-host-config --no" From b9770c348f2c30b0a9cc86fff9914d48b5cfbfef Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 12 Jan 2017 00:54:17 +0100 Subject: [PATCH 0536/1387] posix: use fully-qualified hostname + node ID The node ID is usually the 48 bit MAC of the primary network interface. --- src/borg/archiver.py | 3 ++- src/borg/platform/posix.pyx | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e1c05aa1..777130b4 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -48,7 +48,7 @@ from .helpers import ProgressIndicatorPercent from .item import Item from .key import key_creator, tam_required_file, tam_required, RepoKey, PassphraseKey from .keymanager import KeyManager -from .platform import get_flags, umount +from .platform import get_flags, umount, get_process_id from .remote import RepositoryServer, RemoteRepository, cache_if_remote from .repository import Repository from .selftest import selftest @@ -1194,6 +1194,7 @@ class Archiver: # Additional debug information print('CRC implementation:', crc32.__name__) + print('Process ID:', get_process_id()) return EXIT_SUCCESS @with_repository() diff --git a/src/borg/platform/posix.pyx b/src/borg/platform/posix.pyx index db71a17f..8b2b959b 100644 --- a/src/borg/platform/posix.pyx +++ b/src/borg/platform/posix.pyx @@ -1,5 +1,6 @@ import errno import os +import uuid import socket import subprocess @@ -7,6 +8,7 @@ import subprocess cdef extern from "wchar.h": cdef int wcswidth(const Py_UNICODE *str, size_t n) + def swidth(s): str_len = len(s) terminal_width = wcswidth(s, str_len) @@ -21,7 +23,7 @@ def swidth(s): # the lock made by the parent, so it needs to use the same PID for that. _pid = os.getpid() # XXX this sometimes requires live internet access for issuing a DNS query in the background. -_hostname = socket.gethostname() +_hostname = '%s@%s' % (socket.getfqdn(), uuid.getnode()) def get_process_id(): @@ -75,5 +77,3 @@ def local_pid_alive(pid): # most POSIX platforms (but not Linux) def umount(mountpoint): return subprocess.call(['umount', mountpoint]) - - From 1667926c9645a7030835ead982629a6191366da5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 12 Jan 2017 01:01:24 +0100 Subject: [PATCH 0537/1387] fix bad parsing of wrong syntax this was like whack-a-mole: fix one regex -> another issue pops up --- borg/helpers.py | 12 +++++++++--- borg/testsuite/helpers.py | 5 +++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/borg/helpers.py b/borg/helpers.py index 7c4a0fa9..2d1ec510 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -823,11 +823,17 @@ class Location: """ # path must not contain :: (it ends at :: or string end), but may contain single colons. - # to avoid ambiguities with other regexes, it must also not start with ":". + # to avoid ambiguities with other regexes, it must also not start with ":" nor with "//" nor with "ssh://". path_re = r""" - (?!:) # not starting with ":" + (?!(:|//|ssh://)) # not starting with ":" or // or ssh:// (?P([^:]|(:(?!:)))+) # any chars, but no "::" """ + # abs_path must not contain :: (it ends at :: or string end), but may contain single colons. + # it must start with a / and that slash is part of the path. + abs_path_re = r""" + (?P(/([^:]|(:(?!:)))+)) # start with /, then any chars, but no "::" + """ + # optional ::archive_name at the end, archive name must not contain "/". # borg mount's FUSE filesystem creates one level of directories from # the archive names and of course "/" is not valid in a directory name. @@ -842,7 +848,7 @@ class Location: (?Pssh):// # ssh:// """ + optional_user_re + r""" # user@ (optional) (?P[^:/]+)(?::(?P\d+))? # host or host:port - """ + path_re + optional_archive_re, re.VERBOSE) # path or path::archive + """ + abs_path_re + optional_archive_re, re.VERBOSE) # path or path::archive file_re = re.compile(r""" (?Pfile):// # file:// diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py index 3f9c7096..dcf1859e 100644 --- a/borg/testsuite/helpers.py +++ b/borg/testsuite/helpers.py @@ -134,6 +134,11 @@ class TestLocationWithoutEnv: location_time2 = Location('/some/path::archive{now:%s}') assert location_time1.archive != location_time2.archive + def test_bad_syntax(self): + with pytest.raises(ValueError): + # this is invalid due to the 2nd colon, correct: 'ssh://user@host/path' + Location('ssh://user@host:/path') + class TestLocationWithEnv: def test_ssh(self, monkeypatch): From 81bd55eec338e31a24edee2d35f1860ecc191bb3 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 12 Jan 2017 01:05:59 +0100 Subject: [PATCH 0538/1387] key testsuite: update blake2 test data to include padded keys --- src/borg/testsuite/key.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index 99e3a6c8..4aa1d8b8 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -37,21 +37,28 @@ class TestKey: keyfile_blake2_key_file = """ BORG_KEY 0000000000000000000000000000000000000000000000000000000000000000 - hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaANAwo4EbUPF/kLQXhQnT4LxRc1advS8lUiegDa - q2Q6oOkP1Jc7MwBa7ZVMgoBG1sBeKYO6Sn6W6BBrHbMR8Dxv7xquaQIh8jIpnjLWpzyFIk - JlijFiTWI58Sxj+2D19b2ayFolnGkF9PJSARgfaieo0GkryqjcIgcXuKHO/H9NfaUDk5YJ - UqrJ9TUMohXSQzwF1pO4ak2BHPZKnbeJ7XL/8fFN8VFQZl27R0et4WlTFRBI1qQYyQaTiL - +/1ICMUpVsQM0mvyW6dc8/zGMsAlmZVApGhhc2jaACDdRF7uPv90UN3zsZy5Be89728RBl - zKvtzupDyTsfrJMqppdGVyYXRpb25zzgABhqCkc2FsdNoAIGTK3TR09UZqw1bPi17gyHOi - 7YtSp4BVK7XptWeKh6Vip3ZlcnNpb24B""".strip() + hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaAZBu680Do3CmfWzeMCwe48KJi3Vps9mEDy7MKF + TastsEhiAd1RQMuxfZpklkLeddMMWk+aPtFiURRFb02JLXV5cKRC1o2ZDdiNa0nao+o6+i + gUjjsea9TAu25t3vxh8uQWs5BuKRLBRr0nUgrSd0IYMUgn+iVbLJRzCCssvxsklkwQxN3F + Y+MvBnn8kUXSeoSoQ2l0fBHzq94Y7LMOm/owMam5URnE8/UEc6ZXBrbyX4EXxDtUqJcs+D + i451thtlGdigDLpvf9nyK66mjiCpPCTCgtlzq0Pe1jcdhnsUYLg+qWzXZ7e2opEZoC6XxS + 3DIuBOxG3Odqj9IKB+6/kl94vz98awPWFSpYcLZVWu7sIP38ZkUK+ad5MHTo/LvTuZdFnd + iqKzZIDUJl3Zl1WGmP/0xVOmfIlznkCZy4d3SMuujwIcqQ5kDvwDRPpdhBBk+UWQY5vFXk + kR1NBNLSTyhAzu3fiUmFl0qZ+UWPRkGAEBy/NuoEibrWwab8BX97cATyvnmOqYkU9PT0C6 + l2l9E4bPpGhhc2jaACDnIa8KgKv84/b5sjaMgSZeIVkuKSLJy2NN8zoH8lnd36ppdGVyYX + Rpb25zzgABhqCkc2FsdNoAIEJLlLh7q74j3q53856H5GgzA1HH+aW5bA/as544+PGkp3Zl + cnNpb24B""".strip() - keyfile_blake2_cdata = bytes.fromhex('045d225d745e07af9002d739391e4e7509ff82a04f98debd74' - '012f09b82cc1d07e0404040404040408ec852921309243b164') - # Verified against b2sum. Entire string passed to BLAKE2, including the 32 byte key contained in + keyfile_blake2_cdata = bytes.fromhex('04fdf9475cf2323c0ba7a99ddc011064f2e7d039f539f2e448' + '0e6f5fc6ff9993d604040404040404098c8cee1c6db8c28947') + # Verified against b2sum. Entire string passed to BLAKE2, including the padded 64 byte key contained in # keyfile_blake2_key_file above is - # 037fb9b75b20d623f1d5a568050fccde4a1b7c5f5047432925e941a17c7a2d0d7061796c6f6164 - # p a y l o a d - keyfile_blake2_id = bytes.fromhex('a22d4fc81bb61c3846c334a09eaf28d22dd7df08c9a7a41e713ef28d80eebd45') + # 19280471de95185ec27ecb6fc9edbb4f4db26974c315ede1cd505fab4250ce7cd0d081ea66946c + # 95f0db934d5f616921efbd869257e8ded2bd9bd93d7f07b1a30000000000000000000000000000 + # 000000000000000000000000000000000000000000000000000000000000000000000000000000 + # 00000000000000000000007061796c6f6164 + # p a y l o a d + keyfile_blake2_id = bytes.fromhex('d8bc68e961c79f99be39061589e5179b2113cd9226e07b08ddd4a1fef7ce93fb') @pytest.fixture def keys_dir(self, request, monkeypatch, tmpdir): From fe6b03a72d9d7db8d1a0a4eace2c3bbd142002c7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 4 Jan 2017 19:32:39 +0100 Subject: [PATCH 0539/1387] check: print non-exit-code warning if --last or --prefix aren't fulfilled --- borg/archive.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/borg/archive.py b/borg/archive.py index afc734ce..26ad1645 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -1100,8 +1100,12 @@ class ArchiveChecker: key=lambda name_info: name_info[1][b'time']) if prefix is not None: archive_items = [item for item in archive_items if item[0].startswith(prefix)] + if not archive_items: + logger.warning('--prefix %s does not match any archives', prefix) num_archives = len(archive_items) end = None if last is None else min(num_archives, last) + if last is not None and end < last: + logger.warning('--last %d archives: only found %d archives', last, end) else: # we only want one specific archive archive_items = [item for item in self.manifest.archives.items() if item[0] == archive] From 01090d2d40b20f75568740f888227c48d9f115f4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 12 Jan 2017 02:25:41 +0100 Subject: [PATCH 0540/1387] fix typos taken from debian package, thanks to danny edel and lintian for finding these. --- borg/archiver.py | 2 +- docs/usage.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index c8c80f37..2d0e792f 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -541,7 +541,7 @@ class Archiver: @with_repository() def do_mount(self, args, repository, manifest, key): - """Mount archive or an entire repository as a FUSE fileystem""" + """Mount archive or an entire repository as a FUSE filesystem""" try: from .fuse import FuseOperations except ImportError as e: diff --git a/docs/usage.rst b/docs/usage.rst index ec1a790f..18a7cdbd 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -160,7 +160,7 @@ General: BORG_FILES_CACHE_TTL When set to a numeric value, this determines the maximum "time to live" for the files cache entries (default: 20). The files cache is used to quickly determine whether a file is unchanged. - The FAQ explains this more detailled in: :ref:`always_chunking` + The FAQ explains this more detailed in: :ref:`always_chunking` TMPDIR where temporary files are stored (might need a lot of temporary space for some operations) From b6fa8629dbc858d997253e25fa48172e92435463 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 12 Jan 2017 02:39:56 +0100 Subject: [PATCH 0541/1387] remote: log SSH command line at debug level --- borg/remote.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/borg/remote.py b/borg/remote.py index 00c3114a..6a6c51f8 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -13,9 +13,12 @@ from . import __version__ from .helpers import Error, IntegrityError, sysinfo from .helpers import replace_placeholders from .repository import Repository +from .logger import create_logger import msgpack +logger = create_logger(__name__) + RPC_PROTOCOL_VERSION = 2 BUFSIZE = 10 * 1024 * 1024 @@ -185,6 +188,7 @@ class RemoteRepository: env.pop('LD_LIBRARY_PATH', None) env.pop('BORG_PASSPHRASE', None) # security: do not give secrets to subprocess env['BORG_VERSION'] = __version__ + logger.debug('SSH command line: %s', borg_cmd) self.p = Popen(borg_cmd, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) self.stdin_fd = self.p.stdin.fileno() self.stdout_fd = self.p.stdout.fileno() From 10f48dbd0b0b0266f5f191a34223bf3103b0c1dc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 12 Jan 2017 02:30:29 +0100 Subject: [PATCH 0542/1387] Update 1.1.0b3 CHANGES --- docs/changes.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 053147ff..ba951d17 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -129,20 +129,26 @@ Changelog Version 1.1.0b3 (not released yet) ---------------------------------- +Compatibility notes: + +- borg init: removed the default of "--encryption/-e", #1979 + Bug fixes: - borg recreate: don't rechunkify unless explicitly told so - borg info: fixed bug when called without arguments, #1914 - borg init: fix free space check crashing if disk is full, #1821 - borg debug delete/get obj: fix wrong reference to exception +- fix processing of remote ~/ and ~user/ paths (regressed since 1.1.0b1), #1759 New features: +- new CRC32 implementations that are much faster than the zlib one used previously, #1970 - add blake2b key modes (use blake2b as MAC). This links against system libb2, if possible, otherwise uses bundled code - automatically remove stale locks - set BORG_HOSTNAME_IS_UNIQUE env var to enable stale lock killing. If set, stale locks in both cache and - repository are deleted. #562 + repository are deleted. #562 #1253 - borg info : print general repo information, #1680 - borg check --first / --last / --sort / --prefix, #1663 - borg mount --first / --last / --sort / --prefix, #1542 From 2d2bff9bf6fe03716861f5c621610a88315f8e94 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 12 Jan 2017 02:41:29 +0100 Subject: [PATCH 0543/1387] remote: include unknown data in error message this makes it far, far easier to diagnose issues like an account being locked: Got unexpected RPC data format from server: This account is currently not available. --- borg/remote.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/borg/remote.py b/borg/remote.py index 6a6c51f8..0944997c 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -4,14 +4,16 @@ import logging import os import select import shlex -from subprocess import Popen, PIPE import sys import tempfile +import textwrap +from subprocess import Popen, PIPE from . import __version__ from .helpers import Error, IntegrityError, sysinfo from .helpers import replace_placeholders +from .helpers import bin_to_hex from .repository import Repository from .logger import create_logger @@ -47,7 +49,16 @@ class UnexpectedRPCDataFormatFromClient(Error): class UnexpectedRPCDataFormatFromServer(Error): - """Got unexpected RPC data format from server.""" + """Got unexpected RPC data format from server:\n{}""" + + def __init__(self, data): + try: + data = data.decode()[:128] + except UnicodeDecodeError: + data = data[:128] + data = ['%02X' % byte for byte in data] + data = textwrap.fill(' '.join(data), 16 * 3) + super().__init__(data) class RepositoryServer: # pragma: no cover @@ -350,7 +361,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. self.unpacker.feed(data) for unpacked in self.unpacker: if not (isinstance(unpacked, tuple) and len(unpacked) == 4): - raise UnexpectedRPCDataFormatFromServer() + raise UnexpectedRPCDataFormatFromServer(data) type, msgid, error, res = unpacked if msgid in self.ignore_responses: self.ignore_responses.remove(msgid) From 660313334472f22c9accc7c8d012bb8cd1db3685 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 12 Jan 2017 02:47:51 +0100 Subject: [PATCH 0544/1387] update CHANGES (1.0-maint) --- docs/changes.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 1a9b40d5..f9a2f4fa 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -139,6 +139,7 @@ Bug fixes: it now does nothing and prints an error, #1981. - Fixed change-passphrase crashing with unencrypted repositories, #1978 - Fixed "borg check repo::archive" indicating success if "archive" does not exist, #1997 +- borg check: print non-exit-code warning if --last or --prefix aren't fulfilled Other changes: @@ -152,6 +153,13 @@ Other changes: - vagrant / travis / tox: add Python 3.6 based testing - travis: fix osxfuse install (fixes OS X testing on Travis CI) + - use pytest-xdist to parallelize testing +- docs: + + - language clarification - VM backup FAQ +- fix typos (taken from Debian package patch) +- remote: include data hexdump in "unexpected RPC data" error message +- remote: log SSH command line at debug level Version 1.0.9 (2016-12-20) From 3c0a903e8a596681615d81b3b853b1a6acc00c72 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 12 Jan 2017 14:30:09 +0100 Subject: [PATCH 0545/1387] upgrade: fix incorrect title levels --- borg/archiver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 2d0e792f..032d600c 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1680,7 +1680,7 @@ class Archiver: Upgrade an existing Borg repository. Borg 1.x.y upgrades - ------------------- + +++++++++++++++++++ Use ``borg upgrade --tam REPO`` to require manifest authentication introduced with Borg 1.0.9 to address security issues. This means @@ -1702,7 +1702,7 @@ class Archiver: for details. Attic and Borg 0.xx to Borg 1.x - ------------------------------- + +++++++++++++++++++++++++++++++ This currently supports converting an Attic repository to Borg and also helps with converting Borg 0.xx to 1.0. From 1d40675ce49d6f7608ab74931c9ad518e820b6f6 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 12 Jan 2017 15:04:57 +0100 Subject: [PATCH 0546/1387] merge fixup --- src/borg/testsuite/archiver.py | 76 ---------------------------------- 1 file changed, 76 deletions(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 6da3073a..2d97ce06 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2358,82 +2358,6 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): assert not self.cmd('list', self.repository_location) -class ManifestAuthenticationTest(ArchiverTestCaseBase): - def spoof_manifest(self, repository): - with repository: - _, key = Manifest.load(repository) - repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({ - 'version': 1, - 'archives': {}, - 'config': {}, - 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), - }))) - repository.commit() - - def test_fresh_init_tam_required(self): - self.cmd('init', self.repository_location) - repository = Repository(self.repository_path, exclusive=True) - with repository: - manifest, key = Manifest.load(repository) - repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({ - 'version': 1, - 'archives': {}, - 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), - }))) - repository.commit() - - with pytest.raises(TAMRequiredError): - self.cmd('list', self.repository_location) - - def test_not_required(self): - self.cmd('init', self.repository_location) - self.create_src_archive('archive1234') - repository = Repository(self.repository_path, exclusive=True) - with repository: - shutil.rmtree(get_security_dir(bin_to_hex(repository.id))) - _, key = Manifest.load(repository) - key.tam_required = False - key.change_passphrase(key._passphrase) - - manifest = msgpack.unpackb(key.decrypt(None, repository.get(Manifest.MANIFEST_ID))) - del manifest[b'tam'] - repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb(manifest))) - repository.commit() - output = self.cmd('list', '--debug', self.repository_location) - assert 'archive1234' in output - assert 'TAM not found and not required' in output - # Run upgrade - self.cmd('upgrade', '--tam', self.repository_location) - # Manifest must be authenticated now - output = self.cmd('list', '--debug', self.repository_location) - assert 'archive1234' in output - assert 'TAM-verified manifest' in output - # Try to spoof / modify pre-1.0.9 - self.spoof_manifest(repository) - # Fails - with pytest.raises(TAMRequiredError): - self.cmd('list', self.repository_location) - # Force upgrade - self.cmd('upgrade', '--tam', '--force', self.repository_location) - self.cmd('list', self.repository_location) - - def test_disable(self): - self.cmd('init', self.repository_location) - self.create_src_archive('archive1234') - self.cmd('upgrade', '--disable-tam', self.repository_location) - repository = Repository(self.repository_path, exclusive=True) - self.spoof_manifest(repository) - assert not self.cmd('list', self.repository_location) - - def test_disable2(self): - self.cmd('init', self.repository_location) - self.create_src_archive('archive1234') - repository = Repository(self.repository_path, exclusive=True) - self.spoof_manifest(repository) - self.cmd('upgrade', '--disable-tam', self.repository_location) - assert not self.cmd('list', self.repository_location) - - @pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteArchiverTestCase(ArchiverTestCase): prefix = '__testsuite__:' From 7923088ff9d1236f3a22a8e7b5641e89c155085f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 12 Jan 2017 17:03:51 +0100 Subject: [PATCH 0547/1387] check: pick better insufficent archives matched warning from TW's merge --- src/borg/archive.py | 8 ++++++-- src/borg/repository.py | 5 ++--- src/borg/testsuite/archive.py | 4 ++-- src/borg/upgrader.py | 6 +++--- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 25983a3c..195ae8e1 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1360,8 +1360,12 @@ class ArchiveChecker: sort_by = sort_by.split(',') if any((first, last, prefix)): archive_infos = self.manifest.archives.list(sort_by=sort_by, prefix=prefix, first=first, last=last) - if not archive_infos: - logger.warning('--first/--last/--prefix did not match any archives') + if prefix and not archive_infos: + logger.warning('--prefix %s does not match any archives', prefix) + if first and len(archive_infos) < first: + logger.warning('--first %d archives: only found %d archives', first, len(archive_infos)) + if last and len(archive_infos) < last: + logger.warning('--last %d archives: only found %d archives', last, len(archive_infos)) else: archive_infos = self.manifest.archives.list(sort_by=sort_by) else: diff --git a/src/borg/repository.py b/src/borg/repository.py index 20ae274e..824985da 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -11,9 +11,6 @@ from itertools import islice import msgpack -import logging -logger = logging.getLogger(__name__) - from .constants import * # NOQA from .hashindex import NSIndex from .helpers import Error, ErrorWithTraceback, IntegrityError, format_file_size, parse_file_size @@ -27,6 +24,8 @@ from .lrucache import LRUCache from .platform import SaveFile, SyncFile, sync_dir from .crc32 import crc32 +logger = create_logger(__name__) + MAX_OBJECT_SIZE = 20 * 1024 * 1024 MAGIC = b'BORG_SEG' MAGIC_LEN = len(MAGIC) diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index a4f613d1..7bf3f5b6 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -31,8 +31,8 @@ def test_stats_basic(stats): assert stats.usize == 10 -def tests_stats_progress(stats, columns=80): - os.environ['COLUMNS'] = str(columns) +def tests_stats_progress(stats, monkeypatch, columns=80): + monkeypatch.setenv('COLUMNS', str(columns)) out = StringIO() stats.show_progress(stream=out) s = '20 B O 10 B C 10 B D 0 N ' diff --git a/src/borg/upgrader.py b/src/borg/upgrader.py index 78da849a..28473e07 100644 --- a/src/borg/upgrader.py +++ b/src/borg/upgrader.py @@ -3,15 +3,15 @@ import os import shutil import time -import logging -logger = logging.getLogger(__name__) - from .constants import REPOSITORY_README from .helpers import get_home_dir, get_keys_dir, get_cache_dir from .helpers import ProgressIndicatorPercent from .key import KeyfileKey, KeyfileNotFoundError from .locking import Lock from .repository import Repository, MAGIC +from .logger import create_logger + +logger = create_logger(__name__) ATTIC_MAGIC = b'ATTICSEG' From 292fb1e2a9866372d5a9d20c28c76df81c598b91 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 12 Jan 2017 21:28:18 +0100 Subject: [PATCH 0548/1387] crc: disable nice CLMUL version due to clang bugs. --- src/borg/_crc32/crc32.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/borg/_crc32/crc32.c b/src/borg/_crc32/crc32.c index 212f40f9..9d290f3c 100644 --- a/src/borg/_crc32/crc32.c +++ b/src/borg/_crc32/crc32.c @@ -3,6 +3,11 @@ #include "slice_by_8.c" #ifdef __GNUC__ +/* + * clang also has or had GCC bug #56298 explained below, but doesn't support + * target attributes or the options stack. So we disable this faster code path for clang. + */ +#ifndef __clang__ #if __x86_64__ /* * Because we don't want a configure script we need compiler-dependent pre-defined macros for detecting this, @@ -29,9 +34,6 @@ * Part 2 of 2 follows below. */ -/* clang also defines __GNUC__, but doesn't need this, it emits warnings instead */ -#ifndef __clang__ - #ifndef __PCLMUL__ #pragma GCC push_options #pragma GCC target("pclmul") @@ -56,9 +58,8 @@ #define __BORG_DISABLE_SSE4_1__ #endif -#endif /* ifdef __clang__ */ - #endif /* if __x86_64__ */ +#endif /* ifndef __clang__ */ #endif /* ifdef __GNUC__ */ #ifdef FOLDING_CRC From f482c32423370b9d5bdecf837969d9fc3b85f457 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 13 Jan 2017 00:30:17 +0100 Subject: [PATCH 0549/1387] crc32: sb8: remove some unneeded stuff, use hashindex byteorder detection --- src/borg/_crc32/slice_by_8.c | 64 +++++++++--------------------------- 1 file changed, 16 insertions(+), 48 deletions(-) diff --git a/src/borg/_crc32/slice_by_8.c b/src/borg/_crc32/slice_by_8.c index 30bd07c1..d58be194 100644 --- a/src/borg/_crc32/slice_by_8.c +++ b/src/borg/_crc32/slice_by_8.c @@ -12,8 +12,6 @@ /// compute CRC32 (Slicing-by-8 algorithm), unroll inner loop 4 times uint32_t crc32_4x8bytes(const void* data, size_t length, uint32_t previousCrc32); - - // ////////////////////////////////////////////////////////// // Crc32.cpp // Copyright (c) 2011-2016 Stephan Brumme. All rights reserved. @@ -22,55 +20,25 @@ uint32_t crc32_4x8bytes(const void* data, size_t length, uint32_t previousCrc32) // see http://create.stephan-brumme.com/disclaimer.html // -// if running on an embedded system, you might consider shrinking the -// big Crc32Lookup table: -// - crc32_bitwise doesn't need it at all -// - crc32_halfbyte has its own small lookup table -// - crc32_1byte needs only Crc32Lookup[0] -// - crc32_4bytes needs only Crc32Lookup[0..3] -// - crc32_8bytes needs only Crc32Lookup[0..7] -// - crc32_4x8bytes needs only Crc32Lookup[0..7] -// - crc32_16bytes needs all of Crc32Lookup - -// define endianess and some integer data types -#if defined(_MSC_VER) || defined(__MINGW32__) - #define __LITTLE_ENDIAN 1234 - #define __BIG_ENDIAN 4321 - #define __BYTE_ORDER __LITTLE_ENDIAN - - #include - #ifdef __MINGW32__ - #define PREFETCH(location) __builtin_prefetch(location) - #else - #define PREFETCH(location) _mm_prefetch(location, _MM_HINT_T0) - #endif -#else - // defines __BYTE_ORDER as __LITTLE_ENDIAN or __BIG_ENDIAN - #include - - #ifdef __GNUC__ - #define PREFETCH(location) __builtin_prefetch(location) - #else - #define PREFETCH(location) ; - #endif -#endif - - /// zlib's CRC32 polynomial const uint32_t Polynomial = 0xEDB88320; /// swap endianess -static inline uint32_t swap(uint32_t x) -{ -#if defined(__GNUC__) || defined(__clang__) - return __builtin_bswap32(x); -#else - return (x >> 24) | - ((x >> 8) & 0x0000FF00) | - ((x << 8) & 0x00FF0000) | - (x << 24); +#if defined (__SVR4) && defined (__sun) +#include +#endif + +#if (defined(BYTE_ORDER)&&(BYTE_ORDER == BIG_ENDIAN)) || \ + (defined(_BIG_ENDIAN)&&defined(__SVR4)&&defined(__sun)) +#define _le32toh(x) __builtin_bswap32(x) +#define BORG_BIG_ENDIAN +#elif (defined(BYTE_ORDER)&&(BYTE_ORDER == LITTLE_ENDIAN)) || \ + (defined(_LITTLE_ENDIAN)&&defined(__SVR4)&&defined(__sun)) +#define _le32toh(x) (x) +#define BORG_LITTLE_ENDIAN +#else +#error Unknown byte order #endif -} // ////////////////////////////////////////////////////////// // constants @@ -389,8 +357,8 @@ uint32_t crc32_slice_by_8(const void* data, size_t length, uint32_t previousCrc3 size_t unrolling; for (unrolling = 0; unrolling < Unroll; unrolling++) { -#if __BYTE_ORDER == __BIG_ENDIAN - uint32_t one = *current++ ^ swap(crc); +#ifdef BORG_BIG_ENDIAN + uint32_t one = *current++ ^ _le32toh(crc); uint32_t two = *current++; crc = Crc32Lookup[0][ two & 0xFF] ^ Crc32Lookup[1][(two>> 8) & 0xFF] ^ From 5cc292c52cd78249fc0cbe40d6f2972d5bc55b28 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 13 Jan 2017 15:33:38 +0100 Subject: [PATCH 0550/1387] fix performance regression in "borg info ::archive" --- src/borg/archive.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 195ae8e1..a08f5fc3 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -438,10 +438,10 @@ Number of files: {0.stats.nfiles}'''.format( _, data = self.key.decrypt(id, chunk) unpacker.feed(data) for item in unpacker: - item = Item(internal_dict=item) - if 'chunks' in item: + chunks = item.get(b'chunks') + if chunks is not None: stats.nfiles += 1 - add_file_chunks(item.chunks) + add_file_chunks(chunks) cache.rollback() return stats From 941b8d777802bbfecd8c700bd017b2add0fd3d27 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 13 Jan 2017 19:09:57 +0100 Subject: [PATCH 0551/1387] borg serve: fix transmission data loss of pipe writes, fixes #1268 This problem was found on cygwin/windows due to its small pipe buffer size of 64kiB. Due to that, bigger (like >64kiB) writes are always only partially done and os.write() returns the amount of data that was actually sent. the code previously did not use that return value and assumed that always all is sent, which led to a loss of the remainder of transmission data and usually some "unexpected RPC data format" error on the client side. Neither Linux nor *BSD ever do partial writes on blocking pipes, unless interrupted by a signal, in which case serve() would terminate. --- borg/remote.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/borg/remote.py b/borg/remote.py index 0944997c..30291cba 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -7,6 +7,7 @@ import shlex import sys import tempfile import textwrap +import time from subprocess import Popen, PIPE from . import __version__ @@ -28,6 +29,23 @@ BUFSIZE = 10 * 1024 * 1024 MAX_INFLIGHT = 100 +def os_write(fd, data): + """os.write wrapper so we do not lose data for partial writes.""" + # This is happening frequently on cygwin due to its small pipe buffer size of only 64kiB + # and also due to its different blocking pipe behaviour compared to Linux/*BSD. + # Neither Linux nor *BSD ever do partial writes on blocking pipes, unless interrupted by a + # signal, in which case serve() would terminate. + amount = remaining = len(data) + while remaining: + count = os.write(fd, data) + remaining -= count + if not remaining: + break + data = data[count:] + time.sleep(count * 1e-09) + return amount + + class ConnectionClosed(Error): """Connection closed by remote host""" @@ -106,7 +124,7 @@ class RepositoryServer: # pragma: no cover if self.repository is not None: self.repository.close() else: - os.write(stderr_fd, "Borg {}: Got connection close before repository was opened.\n" + os_write(stderr_fd, "Borg {}: Got connection close before repository was opened.\n" .format(__version__).encode()) return unpacker.feed(data) @@ -133,9 +151,9 @@ class RepositoryServer: # pragma: no cover logging.exception('Borg %s: exception in RPC call:', __version__) logging.error(sysinfo()) exc = "Remote Exception (see remote log for the traceback)" - os.write(stdout_fd, msgpack.packb((1, msgid, e.__class__.__name__, exc))) + os_write(stdout_fd, msgpack.packb((1, msgid, e.__class__.__name__, exc))) else: - os.write(stdout_fd, msgpack.packb((1, msgid, None, res))) + os_write(stdout_fd, msgpack.packb((1, msgid, None, res))) if es: self.repository.close() return From 93d7d3c1dbc5e8ab67021b89c9f1b84fa46872b1 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 13 Jan 2017 21:02:58 +0100 Subject: [PATCH 0552/1387] travis: require succeeding OS X tests, fixes #2028 --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e1433a17..e4c5fc7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,8 +36,6 @@ matrix: os: osx osx_image: xcode6.4 env: TOXENV=py36 - allow_failures: - - os: osx install: - ./.travis/install.sh From 1c854b9f600b93834cdde12ef0a6a60ca622bdcb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 13 Jan 2017 21:23:21 +0100 Subject: [PATCH 0553/1387] API_VERSION: use numberspaces, fixes #2023 like '_', e.g. '1.0_01' for version 01 (used in 1.0 maintenance branch). this avoids overlap and accidental collisions between different release branches. --- borg/chunker.pyx | 2 +- borg/crypto.pyx | 2 +- borg/hashindex.pyx | 2 +- borg/helpers.py | 8 ++++---- borg/platform.py | 2 +- borg/platform_darwin.pyx | 2 +- borg/platform_freebsd.pyx | 2 +- borg/platform_linux.pyx | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/borg/chunker.pyx b/borg/chunker.pyx index 0faa06f3..7ac664ed 100644 --- a/borg/chunker.pyx +++ b/borg/chunker.pyx @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -API_VERSION = 2 +API_VERSION = '1.0_01' from libc.stdlib cimport free diff --git a/borg/crypto.pyx b/borg/crypto.pyx index f7beb343..e4b3322f 100644 --- a/borg/crypto.pyx +++ b/borg/crypto.pyx @@ -8,7 +8,7 @@ from math import ceil from libc.stdlib cimport malloc, free -API_VERSION = 3 +API_VERSION = '1.0_01' cdef extern from "openssl/rand.h": int RAND_bytes(unsigned char *buf, int num) diff --git a/borg/hashindex.pyx b/borg/hashindex.pyx index a27d0e8f..1b0d07c2 100644 --- a/borg/hashindex.pyx +++ b/borg/hashindex.pyx @@ -4,7 +4,7 @@ import os cimport cython from libc.stdint cimport uint32_t, UINT32_MAX, uint64_t -API_VERSION = 3 +API_VERSION = '1.0_01' cdef extern from "_hashindex.c": diff --git a/borg/helpers.py b/borg/helpers.py index 7c4a0fa9..4edb0313 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -88,13 +88,13 @@ class PlaceholderError(Error): def check_extension_modules(): from . import platform - if hashindex.API_VERSION != 3: + if hashindex.API_VERSION != '1.0_01': raise ExtensionModuleError - if chunker.API_VERSION != 2: + if chunker.API_VERSION != '1.0_01': raise ExtensionModuleError - if crypto.API_VERSION != 3: + if crypto.API_VERSION != '1.0_01': raise ExtensionModuleError - if platform.API_VERSION != 3: + if platform.API_VERSION != '1.0_01': raise ExtensionModuleError diff --git a/borg/platform.py b/borg/platform.py index be7a2bcd..652ff896 100644 --- a/borg/platform.py +++ b/borg/platform.py @@ -30,7 +30,7 @@ elif sys.platform.startswith('freebsd'): # pragma: freebsd only elif sys.platform == 'darwin': # pragma: darwin only from .platform_darwin import acl_get, acl_set, API_VERSION else: # pragma: unknown platform only - API_VERSION = 3 + API_VERSION = '1.0_01' def acl_get(path, item, st, numeric_owner=False): pass diff --git a/borg/platform_darwin.pyx b/borg/platform_darwin.pyx index 4dc25b83..e421ce28 100644 --- a/borg/platform_darwin.pyx +++ b/borg/platform_darwin.pyx @@ -1,7 +1,7 @@ import os from .helpers import user2uid, group2gid, safe_decode, safe_encode -API_VERSION = 3 +API_VERSION = '1.0_01' cdef extern from "sys/acl.h": ctypedef struct _acl_t: diff --git a/borg/platform_freebsd.pyx b/borg/platform_freebsd.pyx index ae69af68..27796c31 100644 --- a/borg/platform_freebsd.pyx +++ b/borg/platform_freebsd.pyx @@ -1,7 +1,7 @@ import os from .helpers import posix_acl_use_stored_uid_gid, safe_encode, safe_decode -API_VERSION = 3 +API_VERSION = '1.0_01' cdef extern from "errno.h": int errno diff --git a/borg/platform_linux.pyx b/borg/platform_linux.pyx index 0185268c..5f1e8aef 100644 --- a/borg/platform_linux.pyx +++ b/borg/platform_linux.pyx @@ -4,7 +4,7 @@ import subprocess from stat import S_ISLNK from .helpers import posix_acl_use_stored_uid_gid, user2uid, group2gid, safe_decode, safe_encode -API_VERSION = 3 +API_VERSION = '1.0_01' cdef extern from "sys/types.h": int ACL_TYPE_ACCESS From 022c1288e7a5ea983729eab92c84bab593e95edb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 13 Jan 2017 21:49:06 +0100 Subject: [PATCH 0554/1387] borg create: document how to backup stdin, fixes #2013 --- borg/archiver.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 032d600c..667a8e90 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1311,8 +1311,12 @@ class Archiver: create_epilog = textwrap.dedent(""" This command creates a backup archive containing all files found while recursively - traversing all paths specified. The archive will consume almost no disk space for - files or parts of files that have already been stored in other archives. + traversing all paths specified. When giving '-' as path, borg will read data + from standard input and create a file 'stdin' in the created archive from that + data. + + The archive will consume almost no disk space for files or parts of files that + have already been stored in other archives. The archive name needs to be unique. It must not end in '.checkpoint' or '.checkpoint.N' (with N being a number), because these names are used for From ededb6f2c806361369685dfd48e2239a8ade5b41 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 14 Jan 2017 05:06:15 +0100 Subject: [PATCH 0555/1387] fix crc32 compile error, fixes #2039 --- src/borg/_crc32/slice_by_8.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/borg/_crc32/slice_by_8.c b/src/borg/_crc32/slice_by_8.c index d58be194..f1efbb37 100644 --- a/src/borg/_crc32/slice_by_8.c +++ b/src/borg/_crc32/slice_by_8.c @@ -350,6 +350,7 @@ uint32_t crc32_slice_by_8(const void* data, size_t length, uint32_t previousCrc3 // enabling optimization (at least -O2) automatically unrolls the inner for-loop const size_t Unroll = 4; const size_t BytesAtOnce = 8 * Unroll; + const uint8_t* currentChar; // process 4x eight bytes at once (Slicing-by-8) while (length >= BytesAtOnce) @@ -386,7 +387,7 @@ uint32_t crc32_slice_by_8(const void* data, size_t length, uint32_t previousCrc3 length -= BytesAtOnce; } - const uint8_t* currentChar = (const uint8_t*) current; + currentChar = (const uint8_t*) current; // remaining 1 to 31 bytes (standard algorithm) while (length-- != 0) crc = (crc >> 8) ^ Crc32Lookup[0][(crc & 0xFF) ^ *currentChar++]; From 00c7a4f8865ee32404f883110d10d7d202cc1435 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 14 Jan 2017 14:12:39 +0100 Subject: [PATCH 0556/1387] vagrant: fix openbsd repo, fixes #2042 original repo is 404. --- Vagrantfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Vagrantfile b/Vagrantfile index 26dcfc04..3d6ad05d 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -111,6 +111,8 @@ end def packages_openbsd return <<-EOF + echo 'installpath = http://ftp.hostserver.de/pub/OpenBSD/6.0/packages/amd64/' > /etc/pkg.conf + echo 'export PKG_PATH=http://ftp.hostserver.de/pub/OpenBSD/6.0/packages/amd64/' >> ~/.profile . ~/.profile pkg_add bash chsh -s /usr/local/bin/bash vagrant From e4c5db4efca34e73328edbc07bbd1ff7994ac054 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 14 Jan 2017 14:48:49 +0100 Subject: [PATCH 0557/1387] posix platform module: only build / import on non-win32 platforms, fixes #2041 rather use a inverted check like "not windows". also: add a base implementation for this stuff, just raising NotImplementedError --- setup.py | 2 +- src/borg/platform/__init__.py | 7 +++++-- src/borg/platform/base.py | 20 ++++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 902157b3..ffc7eb36 100644 --- a/setup.py +++ b/setup.py @@ -373,7 +373,7 @@ if not on_rtd: Extension('borg.item', [item_source]), Extension('borg.crc32', [crc32_source]), ] - if sys.platform.startswith(('linux', 'freebsd', 'darwin')): + if not sys.platform.startswith(('win32', )): ext_modules.append(Extension('borg.platform.posix', [platform_posix_source])) if sys.platform == 'linux': diff --git a/src/borg/platform/__init__.py b/src/borg/platform/__init__.py index cae738de..79fe949d 100644 --- a/src/borg/platform/__init__.py +++ b/src/borg/platform/__init__.py @@ -10,10 +10,13 @@ from .base import acl_get, acl_set from .base import set_flags, get_flags from .base import SaveFile, SyncFile, sync_dir, fdatasync from .base import swidth, umount, API_VERSION -from .posix import process_alive, get_process_id, local_pid_alive - +from .base import process_alive, get_process_id, local_pid_alive OS_API_VERSION = API_VERSION + +if not sys.platform.startswith(('win32', )): + from .posix import process_alive, get_process_id, local_pid_alive + if sys.platform.startswith('linux'): # pragma: linux only from .linux import API_VERSION as OS_API_VERSION from .linux import acl_get, acl_set diff --git a/src/borg/platform/base.py b/src/borg/platform/base.py index d3aa594b..1449a1f7 100644 --- a/src/borg/platform/base.py +++ b/src/borg/platform/base.py @@ -162,3 +162,23 @@ def swidth(s): def umount(mountpoint): """un-mount the FUSE filesystem mounted at """ return 0 # dummy, see also posix module + + +def get_process_id(): + """ + Return identification tuple (hostname, pid, thread_id) for 'us'. If this is a FUSE process, then the PID will be + that of the parent, not the forked FUSE child. + """ + raise NotImplementedError + + +def process_alive(host, pid, thread): + """ + Check if the (host, pid, thread_id) combination corresponds to a potentially alive process. + """ + raise NotImplementedError + + +def local_pid_alive(pid): + """Return whether *pid* is alive.""" + raise NotImplementedError From ae0f1422bfbe49bd4ac824fe6ae56069a171246f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 14 Jan 2017 15:24:03 +0100 Subject: [PATCH 0558/1387] crc: openbsd has no Intel intrinsics --- src/borg/_crc32/crc32.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/borg/_crc32/crc32.c b/src/borg/_crc32/crc32.c index 9d290f3c..23959259 100644 --- a/src/borg/_crc32/crc32.c +++ b/src/borg/_crc32/crc32.c @@ -8,6 +8,11 @@ * target attributes or the options stack. So we disable this faster code path for clang. */ #ifndef __clang__ +/* + * While OpenBSD uses GCC, they don't have Intel intrinsics, so we can't compile this code + * on OpenBSD. + */ +#ifndef __OpenBSD__ #if __x86_64__ /* * Because we don't want a configure script we need compiler-dependent pre-defined macros for detecting this, @@ -59,6 +64,7 @@ #endif #endif /* if __x86_64__ */ +#endif /* ifndef __OpenBSD__ */ #endif /* ifndef __clang__ */ #endif /* ifdef __GNUC__ */ From 85b3625bca51af1f4087ffce8705ada53d8bd4bf Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 14 Jan 2017 04:24:10 +0100 Subject: [PATCH 0559/1387] update CHANGES (1.0-maint) --- docs/changes.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index f9a2f4fa..96f28b2d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -131,6 +131,8 @@ Version 1.0.10rc1 (not released yet) Bug fixes: +- borg serve: fix transmission data loss of pipe writes, #1268 + This affects only the cygwin platform (not Linux, *BSD, OS X). - Avoid triggering an ObjectiveFS bug in xattr retrieval, #1992 - When running out of buffer memory when reading xattrs, only skip the current file, #1993 @@ -152,14 +154,19 @@ Other changes: - tests: - vagrant / travis / tox: add Python 3.6 based testing + - vagrant: fix openbsd repo, fixes #2042 - travis: fix osxfuse install (fixes OS X testing on Travis CI) + - travis: require succeeding OS X tests, #2028 - use pytest-xdist to parallelize testing - docs: - language clarification - VM backup FAQ + - borg create: document how to backup stdin, #2013 + - borg upgrade: fix incorrect title levels - fix typos (taken from Debian package patch) - remote: include data hexdump in "unexpected RPC data" error message - remote: log SSH command line at debug level +- API_VERSION: use numberspaces, #2023 Version 1.0.9 (2016-12-20) From eace1476113479a1d73b6ab93aa2fcfeaec4aabf Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 14 Jan 2017 22:27:39 +0100 Subject: [PATCH 0560/1387] setup.py: add crc32.c to sdist --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ffc7eb36..446af345 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ cython_sources = [ chunker_source, hashindex_source, item_source, + crc32_source, platform_posix_source, platform_linux_source, From b4bb21bf6eb9f2716c7426a0e74bc59db2c5189c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 14 Jan 2017 23:00:46 +0100 Subject: [PATCH 0561/1387] remove .github from pypi package, fixes #2051 --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 309c1f8d..8a4123fe 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,5 +5,6 @@ recursive-exclude docs *.pyc recursive-exclude docs *.pyo prune docs/_build prune .travis +prune .github exclude .coveragerc .gitattributes .gitignore .travis.yml Vagrantfile include borg/_version.py From dedc4c0695963ee5344ad69a347198583639b368 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 14 Jan 2017 23:06:16 +0100 Subject: [PATCH 0562/1387] setup.cfg: fix pytest deprecation warning, fixes #2050 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 6f408a95..93f009af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ -[pytest] +[tool:pytest] python_files = testsuite/*.py [flake8] From 0a15530f9afbf7127a227dbd6e0992c287e38f5b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 14 Jan 2017 23:10:12 +0100 Subject: [PATCH 0563/1387] pytest-xdist: adjust parallelism, fixes #2053 it's either auto or env var XDISTN value. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ce0aaacb..c0de8a52 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ deps = -rrequirements.d/development.txt -rrequirements.d/attic.txt -rrequirements.d/fuse.txt -commands = py.test -n 8 -rs --cov=borg --cov-config=../.coveragerc --benchmark-skip --pyargs {posargs:borg.testsuite} +commands = py.test -n {env:XDISTN:auto} -rs --cov=borg --cov-config=../.coveragerc --benchmark-skip --pyargs {posargs:borg.testsuite} # fakeroot -u needs some env vars: passenv = * From c0fb8da595b43ef7e17709199c5c97cc1cf35a1a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 14 Jan 2017 23:19:40 +0100 Subject: [PATCH 0564/1387] fix xattr test race condition, fixes #2047 --- borg/testsuite/xattr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/borg/testsuite/xattr.py b/borg/testsuite/xattr.py index 5693c753..db01a29a 100644 --- a/borg/testsuite/xattr.py +++ b/borg/testsuite/xattr.py @@ -11,7 +11,7 @@ class XattrTestCase(BaseTestCase): def setUp(self): self.tmpfile = tempfile.NamedTemporaryFile() - self.symlink = os.path.join(os.path.dirname(self.tmpfile.name), 'symlink') + self.symlink = self.tmpfile.name + '.symlink' os.symlink(self.tmpfile.name, self.symlink) def tearDown(self): From 555c6a95e4ef6e06f0a548e698817d3ca81984e2 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 14 Jan 2017 23:23:36 +0100 Subject: [PATCH 0565/1387] add pip and setuptools to requirements file, fixes #2030 sometimes the system pip/setuptools is rather old and causes warnings or malfunctions in the primary virtual env. --- requirements.d/development.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.d/development.txt b/requirements.d/development.txt index a07f0b02..f62a5071 100644 --- a/requirements.d/development.txt +++ b/requirements.d/development.txt @@ -1,3 +1,5 @@ +setuptools +pip virtualenv<14.0 tox pytest From a9cd6a09cbbc9b2e380e3554c21fecad32287219 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 14 Jan 2017 23:42:38 +0100 Subject: [PATCH 0566/1387] fix the freebsd64 vagrant machine, fixes #2037 The previous 10.2 got unusable due to missing backwards compatibility of 10.3 binaries it installed. The 10.3 box from freebsd project has some issues: - it needs "vagrant up" twice to start (first start with MAC warning) - it needs shell set to sh --- Vagrantfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index 3d6ad05d..0f1bcee2 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -467,11 +467,13 @@ Vagrant.configure(2) do |config| end # BSD + # note: the FreeBSD-10.3-STABLE box needs "vagrant up" twice to start. config.vm.define "freebsd64" do |b| - b.vm.box = "geoffgarside/freebsd-10.2" + b.vm.box = "freebsd/FreeBSD-10.3-STABLE" b.vm.provider :virtualbox do |v| v.memory = 768 end + b.ssh.shell = "sh" b.vm.provision "install system packages", :type => :shell, :inline => packages_freebsd b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("freebsd") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("freebsd") From 0e1f050440ea9b61274ade5ad2f46a10edcfd686 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 15 Jan 2017 00:18:00 +0100 Subject: [PATCH 0567/1387] pytest -n 4 as default (4 parallel workers) auto does not produce enough load, e.g. on freebsd64 vagrant VM, cpu is 80-90% idle (1 core == 1 parallel tox worker). --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index c0de8a52..8a73c41c 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ deps = -rrequirements.d/development.txt -rrequirements.d/attic.txt -rrequirements.d/fuse.txt -commands = py.test -n {env:XDISTN:auto} -rs --cov=borg --cov-config=../.coveragerc --benchmark-skip --pyargs {posargs:borg.testsuite} +commands = py.test -n {env:XDISTN:4} -rs --cov=borg --cov-config=../.coveragerc --benchmark-skip --pyargs {posargs:borg.testsuite} # fakeroot -u needs some env vars: passenv = * From 94e35fc52b208f82f741ff5e17a7b8eb876e9b44 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 15 Jan 2017 00:23:09 +0100 Subject: [PATCH 0568/1387] travis: use latest pythons for OS X based testing we test on old pythons (3.x.0) on Linux, so we can test on 3.x.latest on OS X. --- .travis/install.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis/install.sh b/.travis/install.sh index d9025437..688bd10a 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -21,12 +21,12 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then case "${TOXENV}" in py34) - pyenv install 3.4.3 - pyenv global 3.4.3 + pyenv install 3.4.5 + pyenv global 3.4.5 ;; py35) - pyenv install 3.5.1 - pyenv global 3.5.1 + pyenv install 3.5.2 + pyenv global 3.5.2 ;; py36) pyenv install 3.6.0 From 9e8af73d7f22657ecebc457a9aad3da70f07097a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 15 Jan 2017 01:05:40 +0100 Subject: [PATCH 0569/1387] update CHANGES (1.0-maint) --- docs/changes.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 96f28b2d..24e36060 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -155,9 +155,13 @@ Other changes: - vagrant / travis / tox: add Python 3.6 based testing - vagrant: fix openbsd repo, fixes #2042 + - vagrant: fix the freebsd64 machine, #2037 - travis: fix osxfuse install (fixes OS X testing on Travis CI) - travis: require succeeding OS X tests, #2028 + - travis: use latest pythons for OS X based testing - use pytest-xdist to parallelize testing + - fix xattr test race condition, #2047 + - setup.cfg: fix pytest deprecation warning, #2050 - docs: - language clarification - VM backup FAQ @@ -167,6 +171,8 @@ Other changes: - remote: include data hexdump in "unexpected RPC data" error message - remote: log SSH command line at debug level - API_VERSION: use numberspaces, #2023 +- remove .github from pypi package, #2051 +- add pip and setuptools to requirements file, #2030 Version 1.0.9 (2016-12-20) From 1845074b2e84152164e7422b3a06b25102fc7645 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 14 Jan 2017 17:16:19 +0100 Subject: [PATCH 0570/1387] update CHANGES (master) --- docs/changes.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index f95d5590..9a21a299 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -126,12 +126,13 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= -Version 1.1.0b3 (not released yet) ----------------------------------- +Version 1.1.0b3 (2017-01-15) +---------------------------- Compatibility notes: - borg init: removed the default of "--encryption/-e", #1979 + This was done so users do a informed decision about -e mode. Bug fixes: @@ -140,6 +141,7 @@ Bug fixes: - borg init: fix free space check crashing if disk is full, #1821 - borg debug delete/get obj: fix wrong reference to exception - fix processing of remote ~/ and ~user/ paths (regressed since 1.1.0b1), #1759 +- posix platform module: only build / import on non-win32 platforms, #2041 New features: From e32503b84c3045506a8cec33fa1748f8d9232a8e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 14 Jan 2017 17:19:19 +0100 Subject: [PATCH 0571/1387] ran setup.py build_usage --- docs/usage/create.rst.inc | 8 +++++-- docs/usage/init.rst.inc | 46 +++++++++++++++++++++++++------------- docs/usage/prune.rst.inc | 2 ++ docs/usage/upgrade.rst.inc | 4 ++-- 4 files changed, 41 insertions(+), 19 deletions(-) diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 1fd794b7..83c8810e 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -73,8 +73,12 @@ Description ~~~~~~~~~~~ This command creates a backup archive containing all files found while recursively -traversing all paths specified. The archive will consume almost no disk space for -files or parts of files that have already been stored in other archives. +traversing all paths specified. When giving '-' as path, borg will read data +from standard input and create a file 'stdin' in the created archive from that +data. + +The archive will consume almost no disk space for files or parts of files that +have already been stored in other archives. The archive name needs to be unique. It must not end in '.checkpoint' or '.checkpoint.N' (with N being a number), because these names are used for diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index a754c5da..c8633b23 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -14,7 +14,7 @@ positional arguments optional arguments ``-e``, ``--encryption`` - | select encryption key mode (default: "repokey") + | select encryption key mode (default: "None") ``-a``, ``--append-only`` | create an append-only mode repository @@ -27,21 +27,22 @@ Description This command initializes an empty repository. A repository is a filesystem directory containing the deduplicated data from zero or more archives. -Encryption can be enabled at repository init time (the default). +Encryption can be enabled at repository init time. -It is not recommended to disable encryption. Repository encryption protects you -e.g. against the case that an attacker has access to your backup repository. +It is not recommended to work without encryption. Repository encryption protects +you e.g. against the case that an attacker has access to your backup repository. But be careful with the key / the passphrase: -If you want "passphrase-only" security, use the repokey mode. The key will -be stored inside the repository (in its "config" file). In above mentioned -attack scenario, the attacker will have the key (but not the passphrase). +If you want "passphrase-only" security, use one of the repokey modes. The +key will be stored inside the repository (in its "config" file). In above +mentioned attack scenario, the attacker will have the key (but not the +passphrase). -If you want "passphrase and having-the-key" security, use the keyfile mode. -The key will be stored in your home directory (in .config/borg/keys). In -the attack scenario, the attacker who has just access to your repo won't have -the key (and also not the passphrase). +If you want "passphrase and having-the-key" security, use one of the keyfile +modes. The key will be stored in your home directory (in .config/borg/keys). +In the attack scenario, the attacker who has just access to your repo won't +have the key (and also not the passphrase). Make a backup copy of the key file (keyfile mode) or repo config file (repokey mode) and keep it at a safe place, so you still have the key in @@ -72,12 +73,27 @@ Encryption modes repokey and keyfile use AES-CTR-256 for encryption and HMAC-SHA256 for authentication in an encrypt-then-MAC (EtM) construction. The chunk ID hash is HMAC-SHA256 as well (with a separate key). +These modes are compatible with borg 1.0.x. -repokey-blake2 and keyfile-blake2 use the same authenticated encryption, but -use a keyed BLAKE2b-256 hash for the chunk ID hash. +repokey-blake2 and keyfile-blake2 are also authenticated encryption modes, +but use BLAKE2b-256 instead of HMAC-SHA256 for authentication. The chunk ID +hash is a keyed BLAKE2b-256 hash. +These modes are new and not compatible with borg 1.0.x. "authenticated" mode uses no encryption, but authenticates repository contents -through the same keyed BLAKE2b-256 hash as the other blake2 modes. -The key is stored like repokey. +through the same keyed BLAKE2b-256 hash as the other blake2 modes (it uses it +as chunk ID hash). The key is stored like repokey. +This mode is new and not compatible with borg 1.0.x. + +"none" mode uses no encryption and no authentication. It uses sha256 as chunk +ID hash. Not recommended, rather consider using an authenticated or +authenticated/encrypted mode. +This mode is compatible with borg 1.0.x. Hardware acceleration will be used automatically. + +On modern Intel/AMD CPUs (except very cheap ones), AES is usually hw +accelerated. BLAKE2b is faster than sha256 on Intel/AMD 64bit CPUs. + +On modern ARM CPUs, NEON provides hw acceleration for sha256 making it faster +than BLAKE2b-256 there. diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index e0c6e16a..01c58d1b 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -17,6 +17,8 @@ optional arguments | do not change repository ``--force`` | force pruning of corrupted archives + ``-p``, ``--progress`` + | show progress display while deleting archives ``-s``, ``--stats`` | print statistics for the deleted archive ``--list`` diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index 6b06847a..8fd443c8 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -36,7 +36,7 @@ Description Upgrade an existing Borg repository. Borg 1.x.y upgrades -------------------- ++++++++++++++++++++ Use ``borg upgrade --tam REPO`` to require manifest authentication introduced with Borg 1.0.9 to address security issues. This means @@ -58,7 +58,7 @@ https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoo for details. Attic and Borg 0.xx to Borg 1.x -------------------------------- ++++++++++++++++++++++++++++++++ This currently supports converting an Attic repository to Borg and also helps with converting Borg 0.xx to 1.0. From e0dfb656ee42b57e8b02fe304df10618e82bc7de Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 15 Jan 2017 04:52:02 +0100 Subject: [PATCH 0572/1387] fix crc32 compilation issues on wheezy moving the declaration / assignment from the middle of the function to the beginning. --- src/borg/_crc32/clmul.c | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/borg/_crc32/clmul.c b/src/borg/_crc32/clmul.c index 4b84b035..8a065390 100644 --- a/src/borg/_crc32/clmul.c +++ b/src/borg/_crc32/clmul.c @@ -74,10 +74,12 @@ static int have_clmul(void) { unsigned eax, ebx, ecx, edx; + int has_pclmulqdq; + int has_sse41; cpuid(1 /* feature bits */, &eax, &ebx, &ecx, &edx); - int has_pclmulqdq = ecx & 0x2; /* bit 1 */ - int has_sse41 = ecx & 0x80000; /* bit 19 */ + has_pclmulqdq = ecx & 0x2; /* bit 1 */ + has_sse41 = ecx & 0x80000; /* bit 19 */ return has_pclmulqdq && has_sse41; } @@ -345,6 +347,13 @@ crc32_clmul(const uint8_t *src, long len, uint32_t initial_crc) int first = 1; + /* fold 512 to 32 step variable declarations for ISO-C90 compat. */ + const __m128i xmm_mask = _mm_load_si128((__m128i *)crc_mask); + const __m128i xmm_mask2 = _mm_load_si128((__m128i *)crc_mask2); + + uint32_t crc; + __m128i x_tmp0, x_tmp1, x_tmp2, crc_fold; + if (len < 16) { if (len == 0) return initial_crc; @@ -464,11 +473,6 @@ done: (void)0; /* fold 512 to 32 */ - const __m128i xmm_mask = _mm_load_si128((__m128i *)crc_mask); - const __m128i xmm_mask2 = _mm_load_si128((__m128i *)crc_mask2); - - uint32_t crc; - __m128i x_tmp0, x_tmp1, x_tmp2, crc_fold; /* * k1 From d67cc229f660731b2e90046b75e4e2ed34cd60c8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 15 Jan 2017 19:23:07 +0100 Subject: [PATCH 0573/1387] MANIFEST: exclude *.so --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index eb7eb0cb..f8e9eda6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ include README.rst AUTHORS LICENSE CHANGES.rst MANIFEST.in graft src recursive-exclude src *.pyc recursive-exclude src *.pyo +recursive-exclude src *.so recursive-include docs * recursive-exclude docs *.pyc recursive-exclude docs *.pyo From 1c3ec747d04070d508fe92fa7398ed80ae9364b8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 16 Jan 2017 06:31:12 +0100 Subject: [PATCH 0574/1387] upgrade pyinstaller from 3.1.1+ to 3.2.1 --- Vagrantfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 724ad79e..aea1c94f 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -286,8 +286,7 @@ def install_pyinstaller(bootloader) . borg-env/bin/activate git clone https://github.com/thomaswaldmann/pyinstaller.git cd pyinstaller - # develop branch, with fixed / freshly rebuilt bootloaders - git checkout fresh-bootloader + git checkout v3.2.1 EOF if bootloader script += <<-EOF From 007d4797d80a437c47a517ba2dc11b2aec710c75 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 16 Jan 2017 08:10:08 +0100 Subject: [PATCH 0575/1387] pyinstaller: automatically builds bootloader if missing --- Vagrantfile | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index aea1c94f..78131247 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -279,27 +279,16 @@ def install_borg(fuse) return script end -def install_pyinstaller(bootloader) - script = <<-EOF +def install_pyinstaller() + return <<-EOF . ~/.bash_profile cd /vagrant/borg . borg-env/bin/activate git clone https://github.com/thomaswaldmann/pyinstaller.git cd pyinstaller git checkout v3.2.1 + python setup.py install EOF - if bootloader - script += <<-EOF - # build bootloader, if it is not included - cd bootloader - python ./waf all - cd .. - EOF - end - script += <<-EOF - pip install -e . - EOF - return script end def build_binary_with_pyinstaller(boxname) @@ -436,7 +425,7 @@ Vagrant.configure(2) do |config| b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("wheezy32") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("wheezy32") b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) - b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller(false) + b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller() b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("wheezy32") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("wheezy32") end @@ -449,7 +438,7 @@ Vagrant.configure(2) do |config| b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("wheezy64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("wheezy64") b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) - b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller(false) + b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller() b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("wheezy64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("wheezy64") end @@ -463,7 +452,7 @@ Vagrant.configure(2) do |config| b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("darwin64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("darwin64") b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) - b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller(false) + b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller() b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("darwin64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("darwin64") end @@ -481,7 +470,7 @@ Vagrant.configure(2) do |config| b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("freebsd") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("freebsd") b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) - b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller(true) + b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller() b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("freebsd") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("freebsd") end From cdffd93139cef65e0c8f0ba441fbf4317d40904c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 17 Jan 2017 02:09:28 +0100 Subject: [PATCH 0576/1387] pyinstaller: use fixed AND freshly compiled bootloader, fixes #2002 --- Vagrantfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 0f1bcee2..f0caae04 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -283,9 +283,10 @@ def install_pyinstaller(bootloader) . ~/.bash_profile cd /vagrant/borg . borg-env/bin/activate - git clone https://github.com/pyinstaller/pyinstaller.git + git clone https://github.com/thomaswaldmann/pyinstaller.git cd pyinstaller - git checkout v3.1.1 + # develop branch, with fixed / freshly rebuilt bootloaders + git checkout fresh-bootloader EOF if bootloader script += <<-EOF From 2b6e8a19e37636c093948cd16517378dc34a68ae Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 19 Jan 2017 18:58:14 +0100 Subject: [PATCH 0577/1387] use python 3.5.3 to build binaries, fixes #2078 --- Vagrantfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index f0caae04..1b978683 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -226,7 +226,7 @@ def install_pythons(boxname) pyenv install 3.4.0 # tests pyenv install 3.5.0 # tests pyenv install 3.6.0 # tests - pyenv install 3.5.2 # binary build, use latest 3.5.x release + pyenv install 3.5.3 # binary build, use latest 3.5.x release pyenv rehash EOF end @@ -244,8 +244,8 @@ def build_pyenv_venv(boxname) . ~/.bash_profile cd /vagrant/borg # use the latest 3.5 release - pyenv global 3.5.2 - pyenv virtualenv 3.5.2 borg-env + pyenv global 3.5.3 + pyenv virtualenv 3.5.3 borg-env ln -s ~/.pyenv/versions/borg-env . EOF end From 7b9ff759606d9c5284a751cf668c0a18d147a6ee Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 19 Jan 2017 19:02:13 +0100 Subject: [PATCH 0578/1387] use osxfuse 3.5.4 for tests / to build binaries --- Vagrantfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 1b978683..63a31764 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -65,9 +65,9 @@ def packages_darwin # install all the (security and other) updates sudo softwareupdate --install --all # get osxfuse 3.x release code from github: - curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.5.3/osxfuse-3.5.3.dmg >osxfuse.dmg + curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.5.4/osxfuse-3.5.4.dmg >osxfuse.dmg MOUNTDIR=$(echo `hdiutil mount osxfuse.dmg | tail -1 | awk '{$1="" ; print $0}'` | xargs -0 echo) \ - && sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for macOS 3.5.3.pkg" -target / + && sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for macOS 3.5.4.pkg" -target / sudo chown -R vagrant /usr/local # brew must be able to create stuff here ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew update From 74c33463dcd01502cb3886ba59d4c7f4b337fe87 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 20 Jan 2017 02:59:36 +0100 Subject: [PATCH 0579/1387] vagrant freebsd: some fixes, fixes #2067 - use -RELEASE, it can be updated via binaries - more RAM, otherwise the 4 workers run out of memory. - do not install / use fakeroot, it seems broken. - set a hostname, this VM has none --- Vagrantfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 63a31764..d8721baf 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -83,11 +83,13 @@ end def packages_freebsd return <<-EOF + # VM has no hostname set + hostname freebsd # install all the (security and other) updates, base system freebsd-update --not-running-from-cron fetch install # for building borgbackup and dependencies: pkg install -y openssl liblz4 fusefs-libs pkgconf - pkg install -y fakeroot git bash + pkg install -y git bash # for building python: pkg install -y sqlite3 # make bash default / work: @@ -468,11 +470,11 @@ Vagrant.configure(2) do |config| end # BSD - # note: the FreeBSD-10.3-STABLE box needs "vagrant up" twice to start. + # note: the FreeBSD-10.3-RELEASE box needs "vagrant up" twice to start. config.vm.define "freebsd64" do |b| - b.vm.box = "freebsd/FreeBSD-10.3-STABLE" + b.vm.box = "freebsd/FreeBSD-10.3-RELEASE" b.vm.provider :virtualbox do |v| - v.memory = 768 + v.memory = 1536 end b.ssh.shell = "sh" b.vm.provision "install system packages", :type => :shell, :inline => packages_freebsd From ddd9d77e5d2a2a7475ca51bcfbcc39129f5bf3b9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 21 Jan 2017 05:36:56 +0100 Subject: [PATCH 0580/1387] Manifest.in: simplify, exclude *.{so,dll,orig}, fixes #2066 --- MANIFEST.in | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 8a4123fe..307ccc70 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,8 @@ include README.rst AUTHORS LICENSE CHANGES.rst MANIFEST.in -recursive-include borg *.pyx -recursive-include docs * -recursive-exclude docs *.pyc -recursive-exclude docs *.pyo -prune docs/_build +exclude .coveragerc .gitattributes .gitignore .travis.yml Vagrantfile prune .travis prune .github -exclude .coveragerc .gitattributes .gitignore .travis.yml Vagrantfile -include borg/_version.py +graft borg +graft docs +prune docs/_build +global-exclude *.py[co] *.orig *.so *.dll From 90ae9076a4856e13110da56afb63228b7f8d3969 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 21 Jan 2017 15:04:01 +0100 Subject: [PATCH 0581/1387] hashindex: detect mingw byte order --- borg/_hashindex.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/borg/_hashindex.c b/borg/_hashindex.c index bfa3ef09..a86f1276 100644 --- a/borg/_hashindex.c +++ b/borg/_hashindex.c @@ -13,10 +13,12 @@ #endif #if (defined(BYTE_ORDER)&&(BYTE_ORDER == BIG_ENDIAN)) || \ + (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) || \ (defined(_BIG_ENDIAN)&&defined(__SVR4)&&defined(__sun)) #define _le32toh(x) __builtin_bswap32(x) #define _htole32(x) __builtin_bswap32(x) #elif (defined(BYTE_ORDER)&&(BYTE_ORDER == LITTLE_ENDIAN)) || \ + (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) || \ (defined(_LITTLE_ENDIAN)&&defined(__SVR4)&&defined(__sun)) #define _le32toh(x) (x) #define _htole32(x) (x) From fafd5e03997713ae9bec00368522f6698034e4b1 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 21 Jan 2017 15:04:42 +0100 Subject: [PATCH 0582/1387] hashindex: separate endian-dependent defs from endian detection also make macro style consistent with other macros in the codebase. --- borg/_hashindex.c | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/borg/_hashindex.c b/borg/_hashindex.c index a86f1276..f0e8ba93 100644 --- a/borg/_hashindex.c +++ b/borg/_hashindex.c @@ -12,20 +12,26 @@ #include #endif -#if (defined(BYTE_ORDER)&&(BYTE_ORDER == BIG_ENDIAN)) || \ +#if (defined(BYTE_ORDER) && (BYTE_ORDER == BIG_ENDIAN)) || \ (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) || \ - (defined(_BIG_ENDIAN)&&defined(__SVR4)&&defined(__sun)) -#define _le32toh(x) __builtin_bswap32(x) -#define _htole32(x) __builtin_bswap32(x) -#elif (defined(BYTE_ORDER)&&(BYTE_ORDER == LITTLE_ENDIAN)) || \ + (defined(_BIG_ENDIAN) && defined(__SVR4)&&defined(__sun)) +#define BORG_BIG_ENDIAN 1 +#elif (defined(BYTE_ORDER) && (BYTE_ORDER == LITTLE_ENDIAN)) || \ (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) || \ - (defined(_LITTLE_ENDIAN)&&defined(__SVR4)&&defined(__sun)) -#define _le32toh(x) (x) -#define _htole32(x) (x) + (defined(_LITTLE_ENDIAN) && defined(__SVR4)&&defined(__sun)) +#define BORG_BIG_ENDIAN 0 #else #error Unknown byte order #endif +#if BORG_BIG_ENDIAN +#define _le32toh(x) __builtin_bswap32(x) +#define _htole32(x) __builtin_bswap32(x) +#else +#define _le32toh(x) (x) +#define _htole32(x) (x) +#endif + #define MAGIC "BORG_IDX" #define MAGIC_LEN 8 From d350e3a2e1447c0d3a4d4a1adee87b2825a5a345 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 22 Jan 2017 02:21:26 +0100 Subject: [PATCH 0583/1387] create: don't create hard link refs to failed files --- borg/archive.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/borg/archive.py b/borg/archive.py index 26ad1645..4d436c86 100644 --- a/borg/archive.py +++ b/borg/archive.py @@ -660,8 +660,6 @@ Number of files: {0.stats.nfiles}'''.format( self.add_item(item) status = 'h' # regular file, hardlink (to already seen inodes) return status - else: - self.hard_links[st.st_ino, st.st_dev] = safe_path is_special_file = is_special(st.st_mode) if not is_special_file: path_hash = self.key.id_hash(os.path.join(self.cwd, path).encode('utf-8', 'surrogateescape')) @@ -709,6 +707,9 @@ Number of files: {0.stats.nfiles}'''.format( item[b'mode'] = stat.S_IFREG | stat.S_IMODE(item[b'mode']) self.stats.nfiles += 1 self.add_item(item) + if st.st_nlink > 1 and source is None: + # Add the hard link reference *after* the file has been added to the archive. + self.hard_links[st.st_ino, st.st_dev] = safe_path return status @staticmethod From fc8be58b636b1fbb8e21720990811e269ada8b8b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 22 Jan 2017 16:54:06 +0100 Subject: [PATCH 0584/1387] SyncFile: fix use of fd object after close --- borg/repository.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/borg/repository.py b/borg/repository.py index dd49b368..2cf00010 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -829,8 +829,9 @@ class LoggedIO: # tell the OS that it does not need to cache what we just wrote, # avoids spoiling the cache for the OS and other processes. os.posix_fadvise(self._write_fd.fileno(), 0, 0, os.POSIX_FADV_DONTNEED) + dirname = os.path.dirname(self._write_fd.name) self._write_fd.close() - sync_dir(os.path.dirname(self._write_fd.name)) + sync_dir(dirname) self._write_fd = None From 8fe047ec8d8281b1738d87bf548af194337074c3 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 22 Jan 2017 02:29:54 +0100 Subject: [PATCH 0585/1387] mount: handle invalid hard link refs --- borg/fuse.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/borg/fuse.py b/borg/fuse.py index f92f7690..25c93522 100644 --- a/borg/fuse.py +++ b/borg/fuse.py @@ -128,15 +128,21 @@ class FuseOperations(llfuse.Operations): else: self.items[inode] = item continue - segments = prefix + os.fsencode(os.path.normpath(item[b'path'])).split(b'/') - del item[b'path'] + path = item.pop(b'path') + segments = prefix + os.fsencode(os.path.normpath(path)).split(b'/') num_segments = len(segments) parent = 1 for i, segment in enumerate(segments, 1): # Leaf segment? if i == num_segments: if b'source' in item and stat.S_ISREG(item[b'mode']): - inode = self._find_inode(item[b'source'], prefix) + try: + inode = self._find_inode(item[b'source'], prefix) + except KeyError: + file = path.decode(errors='surrogateescape') + source = item[b'source'].decode(errors='surrogateescape') + logger.warning('Skipping broken hard link: %s -> %s', file, source) + continue item = self.cache.get(inode) item[b'nlink'] = item.get(b'nlink', 1) + 1 self.items[inode] = item From 6996fa6dc0ba1feb762e6ec3a012cad2c219cc21 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 23 Jan 2017 20:47:33 +0100 Subject: [PATCH 0586/1387] creating a new segment: use "xb" mode, fixes #2099 "ab" seems to make no sense here (if there is already a (crap, but non-empty) segment file, we would write a MAGIC right into the middle of the resulting file) and cause #2099. --- borg/repository.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/borg/repository.py b/borg/repository.py index dd49b368..058e894e 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -670,7 +670,8 @@ class LoggedIO: if not os.path.exists(dirname): os.mkdir(dirname) sync_dir(os.path.join(self.path, 'data')) - self._write_fd = open(self.segment_filename(self.segment), 'ab') + # play safe: fail if file exists (do not overwrite existing contents, do not append) + self._write_fd = open(self.segment_filename(self.segment), 'xb') self._write_fd.write(MAGIC) self.offset = MAGIC_LEN return self._write_fd From fbaefc98c9842291578f54b55a4399b44dd0a109 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 27 Jan 2017 11:54:20 +0100 Subject: [PATCH 0587/1387] docs: add CVE numbers for issues fixed in 1.0.9 https://www.cvedetails.com/product/35461/Borg-Borg.html?vendor_id=16008 --- docs/changes.rst | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 24e36060..e0efe3f5 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,8 +5,8 @@ This section is used for infos about security and corruption issues. .. _tam_vuln: -Pre-1.0.9 manifest spoofing vulnerability ------------------------------------------ +Pre-1.0.9 manifest spoofing vulnerability (CVE-2016-10099) +---------------------------------------------------------- A flaw in the cryptographic authentication scheme in Borg allowed an attacker to spoof the manifest. The attack requires an attacker to be able to @@ -54,7 +54,9 @@ Vulnerability time line: * 2016-11-14: Vulnerability and fix discovered during review of cryptography by Marian Beermann (@enkore) * 2016-11-20: First patch -* 2016-12-18: Released fixed versions: 1.0.9, 1.1.0b3 +* 2016-12-20: Released fixed version 1.0.9 +* 2017-01-02: CVE was assigned +* 2017-01-15: Released fixed version 1.1.0b3 (fix was previously only available from source) .. _attic013_check_corruption: @@ -183,10 +185,14 @@ Security fixes: - A flaw in the cryptographic authentication scheme in Borg allowed an attacker to spoof the manifest. See :ref:`tam_vuln` above for the steps you should take. + + CVE-2016-10099 was assigned to this vulnerability. - borg check: When rebuilding the manifest (which should only be needed very rarely) duplicate archive names would be handled on a "first come first serve" basis, allowing an attacker to apparently replace archives. + CVE-2016-10100 was assigned to this vulnerability. + Bug fixes: - borg check: From 2cfaf03f840d67a0daa3b94ffb9572ff39b2a668 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 24 Jan 2017 14:18:25 +0100 Subject: [PATCH 0588/1387] mount: umount on SIGINT/^C when in foreground --- borg/archiver.py | 7 +++++++ borg/fuse.py | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/borg/archiver.py b/borg/archiver.py index 667a8e90..c2626beb 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1546,6 +1546,13 @@ class Archiver: - allow_damaged_files: by default damaged files (where missing chunks were replaced with runs of zeros by borg check --repair) are not readable and return EIO (I/O error). Set this option to read such files. + + When the daemonized process receives a signal or crashes, it does not unmount. + Unmounting in these cases could cause an active rsync or similar process + to unintentionally delete data. + + When running in the foreground ^C/SIGINT unmounts cleanly, but other + signals or crashes do not. """) subparser = subparsers.add_parser('mount', parents=[common_parser], description=self.do_mount.__doc__, diff --git a/borg/fuse.py b/borg/fuse.py index f92f7690..71061100 100644 --- a/borg/fuse.py +++ b/borg/fuse.py @@ -6,6 +6,7 @@ import os import stat import tempfile import time +from signal import SIGINT from distutils.version import LooseVersion import msgpack @@ -98,7 +99,8 @@ class FuseOperations(llfuse.Operations): umount = False try: signal = fuse_main() - umount = (signal is None) # no crash and no signal -> umount request + # no crash and no signal (or it's ^C and we're in the foreground) -> umount request + umount = (signal is None or (signal == SIGINT and foreground)) finally: llfuse.close(umount) From f74b533d6dce141ebab200aafa864399b1f085fe Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 28 Jan 2017 14:44:42 +0100 Subject: [PATCH 0589/1387] vagrant: improve darwin64 VM settings somehow without these cpuid settings it does not work for everybody. also nice if we can get away without the extensions pack, which is proprietary. do not update iTunes, we just want the OS security / bugfix updates --- Vagrantfile | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Vagrantfile b/Vagrantfile index d8721baf..f26066e7 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -63,6 +63,8 @@ end def packages_darwin return <<-EOF # install all the (security and other) updates + sudo softwareupdate --ignore iTunesX + sudo softwareupdate --ignore iTunes sudo softwareupdate --install --all # get osxfuse 3.x release code from github: curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.5.4/osxfuse-3.5.4.dmg >osxfuse.dmg @@ -458,6 +460,16 @@ Vagrant.configure(2) do |config| # OS X config.vm.define "darwin64" do |b| b.vm.box = "jhcook/yosemite-clitools" + b.vm.provider :virtualbox do |v| + v.customize ['modifyvm', :id, '--ostype', 'MacOS1010_64'] + v.customize ['modifyvm', :id, '--paravirtprovider', 'default'] + # Adjust CPU settings according to + # https://github.com/geerlingguy/macos-virtualbox-vm + v.customize ['modifyvm', :id, '--cpuidset', + '00000001', '000306a9', '00020800', '80000201', '178bfbff'] + # Disable USB variant requiring Virtualbox proprietary extension pack + v.customize ["modifyvm", :id, '--usbehci', 'off', '--usbxhci', 'off'] + end b.vm.provision "packages darwin", :type => :shell, :privileged => false, :inline => packages_darwin b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("darwin64") b.vm.provision "fix pyenv", :type => :shell, :privileged => false, :inline => fix_pyenv_darwin("darwin64") From 5a39d5c4f8a86a9c94449061c293eaaa0ce90bb8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 24 Jan 2017 23:45:54 +0100 Subject: [PATCH 0590/1387] make LoggedIO.close_segment reentrant if anything blows up in the middle of a (first) invocation of close_segment() and an exception gets raised, it could happen that close_segment() gets called again (e.g. in Repository.__del__ or elsewhere). As the self._write_fd was set to None rather late, it would re-enter the if-block then. The new code gets the value of self._write_fd and also sets it to None in one tuple assignment, so re-entrance does not happen. Also, it uses try/finally to make sure the important parts (fd.close()) gets executed, even if there are exceptions in the other parts. --- borg/repository.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/borg/repository.py b/borg/repository.py index ddaf0143..4c7df217 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -821,19 +821,25 @@ class LoggedIO: self.close_segment() # after-commit fsync() def close_segment(self): - if self._write_fd: - self.segment += 1 - self.offset = 0 - self._write_fd.flush() - os.fsync(self._write_fd.fileno()) - if hasattr(os, 'posix_fadvise'): # only on UNIX - # tell the OS that it does not need to cache what we just wrote, - # avoids spoiling the cache for the OS and other processes. - os.posix_fadvise(self._write_fd.fileno(), 0, 0, os.POSIX_FADV_DONTNEED) - dirname = os.path.dirname(self._write_fd.name) - self._write_fd.close() - sync_dir(dirname) - self._write_fd = None + # set self._write_fd to None early to guard against reentry from error handling code pathes: + fd, self._write_fd = self._write_fd, None + if fd is not None: + dirname = None + try: + self.segment += 1 + self.offset = 0 + dirname = os.path.dirname(fd.name) + fd.flush() + fileno = fd.fileno() + os.fsync(fileno) + if hasattr(os, 'posix_fadvise'): # only on UNIX + # tell the OS that it does not need to cache what we just wrote, + # avoids spoiling the cache for the OS and other processes. + os.posix_fadvise(fileno, 0, 0, os.POSIX_FADV_DONTNEED) + finally: + fd.close() + if dirname: + sync_dir(dirname) MAX_DATA_SIZE = MAX_OBJECT_SIZE - LoggedIO.put_header_fmt.size From add38e8cdeb4242a64e5939c764ff6a314d85b5b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 27 Jan 2017 21:09:55 +0100 Subject: [PATCH 0591/1387] ignore posix_fadvise errors in repository.py, work around #2095 note: we also ignore the call's return value in _chunker.c. both is harmless as the call not working does not cause incorrect function, just worse performance due to constant flooding of the cache (as if we would not issue the call). --- borg/repository.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/borg/repository.py b/borg/repository.py index 4c7df217..e85c33d6 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -833,9 +833,19 @@ class LoggedIO: fileno = fd.fileno() os.fsync(fileno) if hasattr(os, 'posix_fadvise'): # only on UNIX - # tell the OS that it does not need to cache what we just wrote, - # avoids spoiling the cache for the OS and other processes. - os.posix_fadvise(fileno, 0, 0, os.POSIX_FADV_DONTNEED) + try: + # tell the OS that it does not need to cache what we just wrote, + # avoids spoiling the cache for the OS and other processes. + os.posix_fadvise(fileno, 0, 0, os.POSIX_FADV_DONTNEED) + except OSError: + # usually, posix_fadvise can't fail for us, but there seem to + # be failures when running borg under docker on ARM, likely due + # to a bug outside of borg. + # also, there is a python wrapper bug, always giving errno = 0. + # https://github.com/borgbackup/borg/issues/2095 + # as this call is not critical for correct function (just to + # optimize cache usage), we ignore these errors. + pass finally: fd.close() if dirname: From dc3492642dfd987b37ef6e80aca7f657c2aee070 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 21 Jan 2017 06:09:25 +0100 Subject: [PATCH 0592/1387] update CHANGES (1.0-maint) --- docs/changes.rst | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index e0efe3f5..129d3e2d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -128,8 +128,8 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= -Version 1.0.10rc1 (not released yet) ------------------------------------- +Version 1.0.10rc1 (2017-01-29) +------------------------------ Bug fixes: @@ -144,9 +144,16 @@ Bug fixes: - Fixed change-passphrase crashing with unencrypted repositories, #1978 - Fixed "borg check repo::archive" indicating success if "archive" does not exist, #1997 - borg check: print non-exit-code warning if --last or --prefix aren't fulfilled +- fix bad parsing of wrong repo location syntax +- create: don't create hard link refs to failed files, + mount: handle invalid hard link refs, #2092 +- detect mingw byte order, #2073 +- creating a new segment: use "xb" mode, #2099 +- mount: umount on SIGINT/^C when in foreground, #2082 Other changes: +- binary: use fixed AND freshly compiled pyinstaller bootloader, #2002 - xattr: ignore empty names returned by llistxattr(2) et al - Enable the fault handler: install handlers for the SIGSEGV, SIGFPE, SIGABRT, SIGBUS and SIGILL signals to dump the Python traceback. @@ -156,8 +163,11 @@ Other changes: - tests: - vagrant / travis / tox: add Python 3.6 based testing - - vagrant: fix openbsd repo, fixes #2042 - - vagrant: fix the freebsd64 machine, #2037 + - vagrant: fix openbsd repo, #2042 + - vagrant: fix the freebsd64 machine, #2037 #2067 + - vagrant: use python 3.5.3 to build binaries, #2078 + - vagrant: use osxfuse 3.5.4 for tests / to build binaries + vagrant: improve darwin64 VM settings - travis: fix osxfuse install (fixes OS X testing on Travis CI) - travis: require succeeding OS X tests, #2028 - travis: use latest pythons for OS X based testing @@ -169,12 +179,18 @@ Other changes: - language clarification - VM backup FAQ - borg create: document how to backup stdin, #2013 - borg upgrade: fix incorrect title levels + - add CVE numbers for issues fixed in 1.0.9, #2106 - fix typos (taken from Debian package patch) - remote: include data hexdump in "unexpected RPC data" error message - remote: log SSH command line at debug level - API_VERSION: use numberspaces, #2023 - remove .github from pypi package, #2051 - add pip and setuptools to requirements file, #2030 +- SyncFile: fix use of fd object after close (cosmetic) +- Manifest.in: simplify, exclude *.{so,dll,orig}, #2066 +- ignore posix_fadvise errors in repository.py, #2095 + (works around issues with docker on ARM) +- make LoggedIO.close_segment reentrant, avoid reentrance Version 1.0.9 (2016-12-20) From e6c1931d476de51010dc5bd482bf1e591eb1ebba Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 28 Jan 2017 23:36:56 +0100 Subject: [PATCH 0593/1387] ran build_usage --- docs/usage/create.rst.inc | 8 ++++++-- docs/usage/mount.rst.inc | 9 ++++++++- docs/usage/upgrade.rst.inc | 4 ++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 56133ef7..a911252c 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -84,8 +84,12 @@ Description ~~~~~~~~~~~ This command creates a backup archive containing all files found while recursively -traversing all paths specified. The archive will consume almost no disk space for -files or parts of files that have already been stored in other archives. +traversing all paths specified. When giving '-' as path, borg will read data +from standard input and create a file 'stdin' in the created archive from that +data. + +The archive will consume almost no disk space for files or parts of files that +have already been stored in other archives. The archive name needs to be unique. It must not end in '.checkpoint' or '.checkpoint.N' (with N being a number), because these names are used for diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index 6deef307..898d9c5b 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -11,7 +11,7 @@ borg mount [--remote-path PATH] [-f] [-o OPTIONS] REPOSITORY_OR_ARCHIVE MOUNTPOINT - Mount archive or an entire repository as a FUSE fileystem + Mount archive or an entire repository as a FUSE filesystem positional arguments: REPOSITORY_OR_ARCHIVE @@ -54,3 +54,10 @@ supported by borg: - allow_damaged_files: by default damaged files (where missing chunks were replaced with runs of zeros by borg check --repair) are not readable and return EIO (I/O error). Set this option to read such files. + +When the daemonized process receives a signal or crashes, it does not unmount. +Unmounting in these cases could cause an active rsync or similar process +to unintentionally delete data. + +When running in the foreground ^C/SIGINT unmounts cleanly, but other +signals or crashes do not. diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index b4793c2c..b566cf11 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -46,7 +46,7 @@ Description Upgrade an existing Borg repository. Borg 1.x.y upgrades -------------------- ++++++++++++++++++++ Use ``borg upgrade --tam REPO`` to require manifest authentication introduced with Borg 1.0.9 to address security issues. This means @@ -68,7 +68,7 @@ https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoo for details. Attic and Borg 0.xx to Borg 1.x -------------------------------- ++++++++++++++++++++++++++++++++ This currently supports converting an Attic repository to Borg and also helps with converting Borg 0.xx to 1.0. From e19537ff6ff1ad21d3fcc3ad7ea47eb36687e593 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 29 Jan 2017 05:54:25 +0100 Subject: [PATCH 0594/1387] ran build_usage --- docs/usage/mount.rst.inc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index ee63cb42..e15f25af 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -61,3 +61,10 @@ The BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is meant for advanced use to tweak the performance. It sets the number of cached data chunks; additional memory usage can be up to ~8 MiB times this number. The default is the number of CPU cores. + +When the daemonized process receives a signal or crashes, it does not unmount. +Unmounting in these cases could cause an active rsync or similar process +to unintentionally delete data. + +When running in the foreground ^C/SIGINT unmounts cleanly, but other +signals or crashes do not. From dd6b90fe6c2d5369081adf97379118fc903cdbee Mon Sep 17 00:00:00 2001 From: Leo Antunes Date: Sun, 29 Jan 2017 18:13:51 +0100 Subject: [PATCH 0595/1387] change dir_is_tagged to use os.path.exists() Add --keep-exclude-tags option as alias to --keep-tag-files and deprecate the later. Also make tagging accept directories as tags, allowing things like `--exclude-if-present .git`. fixes #1999 --- docs/changes.rst | 12 ++++++++++++ src/borg/archive.py | 12 ++++++------ src/borg/archiver.py | 33 +++++++++++++++++++-------------- src/borg/helpers.py | 8 ++++---- src/borg/testsuite/archiver.py | 17 +++++++++++------ 5 files changed, 52 insertions(+), 30 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 9a21a299..70bab92f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -126,6 +126,18 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= +Version 1.1.0b4 (not released yet) +---------------------------------- + +New features: + +- the --exclude-if-present option now supports tagging a folder with any + filesystem object type (file, folder, etc), instead of expecting only files + as tags, #1999 +- the --keep-tag-files option has been deprecated in favor of the new + --keep-exclude-tags, to account for the change mentioned above. + + Version 1.1.0b3 (2017-01-15) ---------------------------- diff --git a/src/borg/archive.py b/src/borg/archive.py index a08f5fc3..16424196 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1439,7 +1439,7 @@ class ArchiveRecreater: return archive_name.endswith('.recreate') def __init__(self, repository, manifest, key, cache, matcher, - exclude_caches=False, exclude_if_present=None, keep_tag_files=False, + exclude_caches=False, exclude_if_present=None, keep_exclude_tags=False, chunker_params=None, compression=None, compression_files=None, always_recompress=False, dry_run=False, stats=False, progress=False, file_status_printer=None, checkpoint_interval=1800): @@ -1451,7 +1451,7 @@ class ArchiveRecreater: self.matcher = matcher self.exclude_caches = exclude_caches self.exclude_if_present = exclude_if_present or [] - self.keep_tag_files = keep_tag_files + self.keep_exclude_tags = keep_exclude_tags self.rechunkify = chunker_params is not None if self.rechunkify: @@ -1591,7 +1591,7 @@ class ArchiveRecreater: def matcher_add_tagged_dirs(self, archive): """Add excludes to the matcher created by exclude_cache and exclude_if_present.""" def exclude(dir, tag_item): - if self.keep_tag_files: + if self.keep_exclude_tags: tag_files.append(PathPrefixPattern(tag_item.path)) tagged_dirs.append(FnmatchPattern(dir + '/')) else: @@ -1607,10 +1607,10 @@ class ArchiveRecreater: filter=lambda item: item.path.endswith(CACHE_TAG_NAME) or matcher.match(item.path)): if item.path.endswith(CACHE_TAG_NAME): cachedir_masters[item.path] = item + dir, tag_file = os.path.split(item.path) + if tag_file in self.exclude_if_present: + exclude(dir, item) if stat.S_ISREG(item.mode): - dir, tag_file = os.path.split(item.path) - if tag_file in self.exclude_if_present: - exclude(dir, item) if self.exclude_caches and tag_file == CACHE_TAG_NAME: if 'chunks' in item: file = open_item(archive, item) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 80ed853c..a23a61fc 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -346,7 +346,7 @@ class Archiver: else: restrict_dev = None self._process(archive, cache, matcher, args.exclude_caches, args.exclude_if_present, - args.keep_tag_files, skip_inodes, path, restrict_dev, + args.keep_exclude_tags, skip_inodes, path, restrict_dev, read_special=args.read_special, dry_run=dry_run, st=st) if not dry_run: archive.save(comment=args.comment, timestamp=args.timestamp) @@ -382,7 +382,7 @@ class Archiver: return self.exit_code def _process(self, archive, cache, matcher, exclude_caches, exclude_if_present, - keep_tag_files, skip_inodes, path, restrict_dev, + keep_exclude_tags, skip_inodes, path, restrict_dev, read_special=False, dry_run=False, st=None): if not matcher.match(path): self.print_file_status('x', path) @@ -419,11 +419,11 @@ class Archiver: if recurse: tag_paths = dir_is_tagged(path, exclude_caches, exclude_if_present) if tag_paths: - if keep_tag_files and not dry_run: + if keep_exclude_tags and not dry_run: archive.process_dir(path, st) for tag_path in tag_paths: self._process(archive, cache, matcher, exclude_caches, exclude_if_present, - keep_tag_files, skip_inodes, tag_path, restrict_dev, + keep_exclude_tags, skip_inodes, tag_path, restrict_dev, read_special=read_special, dry_run=dry_run) return if not dry_run: @@ -438,7 +438,7 @@ class Archiver: for dirent in entries: normpath = os.path.normpath(dirent.path) self._process(archive, cache, matcher, exclude_caches, exclude_if_present, - keep_tag_files, skip_inodes, normpath, restrict_dev, + keep_exclude_tags, skip_inodes, normpath, restrict_dev, read_special=read_special, dry_run=dry_run) elif stat.S_ISLNK(st.st_mode): if not dry_run: @@ -1151,7 +1151,7 @@ class Archiver: recreater = ArchiveRecreater(repository, manifest, key, cache, matcher, exclude_caches=args.exclude_caches, exclude_if_present=args.exclude_if_present, - keep_tag_files=args.keep_tag_files, chunker_params=args.chunker_params, + keep_exclude_tags=args.keep_exclude_tags, chunker_params=args.chunker_params, compression=args.compression, compression_files=args.compression_files, always_recompress=args.always_recompress, progress=args.progress, stats=args.stats, @@ -1571,6 +1571,7 @@ class Archiver: deprecations = [ # ('--old', '--new', 'Warning: "--old" has been deprecated. Use "--new" instead.'), ('--list-format', '--format', 'Warning: "--list-format" has been deprecated. Use "--format" instead.'), + ('--keep-tag-files', '--keep-exclude-tags', 'Warning: "--keep-tag-files" has been deprecated. Use "--keep-exclude-tags" instead.'), ] for i, arg in enumerate(args[:]): for old_name, new_name, warning in deprecations: @@ -1974,11 +1975,13 @@ class Archiver: help='exclude directories that contain a CACHEDIR.TAG file (' 'http://www.brynosaurus.com/cachedir/spec.html)') exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', - metavar='FILENAME', action='append', type=str, - help='exclude directories that contain the specified file') - exclude_group.add_argument('--keep-tag-files', dest='keep_tag_files', + metavar='NAME', action='append', type=str, + help='exclude directories that are tagged by containing a filesystem object with \ + the given NAME') + exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', action='store_true', default=False, - help='keep tag files of excluded caches/directories') + help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise \ + excluded caches/directories') fs_group = subparser.add_argument_group('Filesystem options') fs_group.add_argument('-x', '--one-file-system', dest='one_file_system', @@ -2562,11 +2565,13 @@ class Archiver: help='exclude directories that contain a CACHEDIR.TAG file (' 'http://www.brynosaurus.com/cachedir/spec.html)') exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', - metavar='FILENAME', action='append', type=str, - help='exclude directories that contain the specified file') - exclude_group.add_argument('--keep-tag-files', dest='keep_tag_files', + metavar='NAME', action='append', type=str, + help='exclude directories that are tagged by containing a filesystem object with \ + the given NAME') + exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', action='store_true', default=False, - help='keep tag files of excluded caches/directories') + help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise \ + excluded caches/directories') archive_group = subparser.add_argument_group('Archive options') archive_group.add_argument('--target', dest='target', metavar='TARGET', default=None, diff --git a/src/borg/helpers.py b/src/borg/helpers.py index f4338e75..e61a6d57 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -633,9 +633,9 @@ def dir_is_cachedir(path): def dir_is_tagged(path, exclude_caches, exclude_if_present): """Determines whether the specified path is excluded by being a cache - directory or containing user-specified tag files. Returns a list of the - paths of the tag files (either CACHEDIR.TAG or the matching - user-specified files). + directory or containing user-specified tag files/directories. Returns a + list of the paths of the tag files/directories (either CACHEDIR.TAG or the + matching user-specified files/directories). """ tag_paths = [] if exclude_caches and dir_is_cachedir(path): @@ -643,7 +643,7 @@ def dir_is_tagged(path, exclude_caches, exclude_if_present): if exclude_if_present is not None: for tag in exclude_if_present: tag_path = os.path.join(path, tag) - if os.path.isfile(tag_path): + if os.path.exists(tag_path): tag_paths.append(tag_path) return tag_paths diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 2d97ce06..f5b1fe8c 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -898,12 +898,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('tagged1/.NOBACKUP') self.create_regular_file('tagged2/00-NOBACKUP') - self.create_regular_file('tagged3/.NOBACKUP/file2') + self.create_regular_file('tagged3/.NOBACKUP/file2', size=1024) def _assert_test_tagged(self): with changedir('output'): self.cmd('extract', self.repository_location + '::test') - self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'tagged3']) + self.assert_equal(sorted(os.listdir('output/input')), ['file1']) def test_exclude_tagged(self): self._create_test_tagged() @@ -922,13 +922,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file0', size=1024) self.create_regular_file('tagged1/.NOBACKUP1') self.create_regular_file('tagged1/file1', size=1024) - self.create_regular_file('tagged2/.NOBACKUP2') + self.create_regular_file('tagged2/.NOBACKUP2/subfile1', size=1024) self.create_regular_file('tagged2/file2', size=1024) self.create_regular_file('tagged3/%s' % CACHE_TAG_NAME, contents=CACHE_TAG_CONTENTS + b' extra stuff') self.create_regular_file('tagged3/file3', size=1024) self.create_regular_file('taggedall/.NOBACKUP1') - self.create_regular_file('taggedall/.NOBACKUP2') + self.create_regular_file('taggedall/.NOBACKUP2/subfile1', size=1024) self.create_regular_file('taggedall/%s' % CACHE_TAG_NAME, contents=CACHE_TAG_CONTENTS + b' extra stuff') self.create_regular_file('taggedall/file4', size=1024) @@ -943,17 +943,22 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_equal(sorted(os.listdir('output/input/taggedall')), ['.NOBACKUP1', '.NOBACKUP2', CACHE_TAG_NAME, ]) + def test_exclude_keep_tagged_deprecation(self): + self.cmd('init', '--encryption=repokey', self.repository_location) + output_warn = self.cmd('create', '--exclude-caches', '--keep-tag-files', self.repository_location + '::test', src_dir) + self.assert_in('--keep-tag-files" has been deprecated.', output_warn) + def test_exclude_keep_tagged(self): self._create_test_keep_tagged() self.cmd('create', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2', - '--exclude-caches', '--keep-tag-files', self.repository_location + '::test', 'input') + '--exclude-caches', '--keep-exclude-tags', self.repository_location + '::test', 'input') self._assert_test_keep_tagged() def test_recreate_exclude_keep_tagged(self): self._create_test_keep_tagged() self.cmd('create', self.repository_location + '::test', 'input') self.cmd('recreate', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2', - '--exclude-caches', '--keep-tag-files', self.repository_location + '::test') + '--exclude-caches', '--keep-exclude-tags', self.repository_location + '::test') self._assert_test_keep_tagged() @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='Linux capabilities test, requires fakeroot >= 1.20.2') From 7f2a108c949116c6d3a3edb6ebceee8126b788a5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 30 Jan 2017 03:11:42 +0100 Subject: [PATCH 0596/1387] fixup: do not access os.POSIX_FADV_* early before we know posix_fadvise support exists on the platform. --- src/borg/platform/base.py | 3 ++- src/borg/platform/linux.pyx | 4 ++-- src/borg/repository.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/borg/platform/base.py b/src/borg/platform/base.py index 06d1c272..0d2fb51b 100644 --- a/src/borg/platform/base.py +++ b/src/borg/platform/base.py @@ -65,6 +65,7 @@ def sync_dir(path): def safe_fadvise(fd, offset, len, advice): if hasattr(os, 'posix_fadvise'): + advice = getattr(os, 'POSIX_FADV_' + advice) try: os.posix_fadvise(fd, offset, len, advice) except OSError: @@ -120,7 +121,7 @@ class SyncFile: platform.fdatasync(self.fileno) # tell the OS that it does not need to cache what we just wrote, # avoids spoiling the cache for the OS and other processes. - safe_fadvise(self.fileno, 0, 0, os.POSIX_FADV_DONTNEED) + safe_fadvise(self.fileno, 0, 0, 'DONTNEED') def close(self): """sync() and close.""" diff --git a/src/borg/platform/linux.pyx b/src/borg/platform/linux.pyx index 3658e417..e87983c7 100644 --- a/src/borg/platform/linux.pyx +++ b/src/borg/platform/linux.pyx @@ -217,7 +217,7 @@ cdef _sync_file_range(fd, offset, length, flags): assert length & PAGE_MASK == 0, "length %d not page-aligned" % length if sync_file_range(fd, offset, length, flags) != 0: raise OSError(errno.errno, os.strerror(errno.errno)) - safe_fadvise(fd, offset, length, os.POSIX_FADV_DONTNEED) + safe_fadvise(fd, offset, length, 'DONTNEED') cdef unsigned PAGE_MASK = resource.getpagesize() - 1 @@ -254,7 +254,7 @@ class SyncFile(BaseSyncFile): os.fdatasync(self.fileno) # tell the OS that it does not need to cache what we just wrote, # avoids spoiling the cache for the OS and other processes. - safe_fadvise(self.fileno, 0, 0, os.POSIX_FADV_DONTNEED) + safe_fadvise(self.fileno, 0, 0, 'DONTNEED') def umount(mountpoint): diff --git a/src/borg/repository.py b/src/borg/repository.py index 142e44e9..9d4d604d 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -909,7 +909,7 @@ class LoggedIO: self.fds = None # Just to make sure we're disabled def close_fd(self, fd): - safe_fadvise(fd.fileno(), 0, 0, os.POSIX_FADV_DONTNEED) + safe_fadvise(fd.fileno(), 0, 0, 'DONTNEED') fd.close() def segment_iterator(self, segment=None, reverse=False): From a85cf75465bbd15e016a2c73afbab98ee272e98a Mon Sep 17 00:00:00 2001 From: Radu Ciorba Date: Mon, 30 Jan 2017 23:29:08 +0200 Subject: [PATCH 0597/1387] fix wrong skip_hint on hashindex_set when encountering tombstones hashindex_lookup would always hint at skipping whatever it's probe length had been with no regard for tombstones it had encountered. This meant new keys would not overwrite first tombstones, but would always land on empty buckets. The regression was introduced in #1748 --- src/borg/_hashindex.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/borg/_hashindex.c b/src/borg/_hashindex.c index 7847cc5c..5fea18f7 100644 --- a/src/borg/_hashindex.c +++ b/src/borg/_hashindex.c @@ -109,19 +109,15 @@ hashindex_index(HashIndex *index, const void *key) } static int -hashindex_lookup(HashIndex *index, const void *key, int *skip_hint) +hashindex_lookup(HashIndex *index, const void *key, int *start_idx) { int didx = -1; int start = hashindex_index(index, key); int idx = start; - int offset; - for(offset=0;;offset++) { - if (skip_hint != NULL) { - (*skip_hint) = offset; - } + for(;;) { if(BUCKET_IS_EMPTY(index, idx)) { - return -1; + break; } if(BUCKET_IS_DELETED(index, idx)) { if(didx == -1) { @@ -138,9 +134,13 @@ hashindex_lookup(HashIndex *index, const void *key, int *skip_hint) } idx = (idx + 1) % index->num_buckets; if(idx == start) { - return -1; + break; } } + if (start_idx != NULL) { + (*start_idx) = (didx == -1) ? idx : didx; + } + return -1; } static int @@ -383,8 +383,8 @@ hashindex_get(HashIndex *index, const void *key) static int hashindex_set(HashIndex *index, const void *key, const void *value) { - int offset = 0; - int idx = hashindex_lookup(index, key, &offset); + int start_idx; + int idx = hashindex_lookup(index, key, &start_idx); uint8_t *ptr; if(idx < 0) { @@ -392,9 +392,9 @@ hashindex_set(HashIndex *index, const void *key, const void *value) if(!hashindex_resize(index, grow_size(index->num_buckets))) { return 0; } - offset = 0; + start_idx = hashindex_index(index, key); } - idx = (hashindex_index(index, key) + offset) % index->num_buckets; + idx = start_idx; while(!BUCKET_IS_EMPTY(index, idx) && !BUCKET_IS_DELETED(index, idx)) { idx = (idx + 1) % index->num_buckets; } From 5fe32866e66d2a6c318fa508d75bb0b3ec2b8090 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 4 Feb 2017 15:01:05 +0100 Subject: [PATCH 0598/1387] Move migrate-to-repokey to the "key" command group --- docs/changes.rst | 4 +++ docs/usage.rst | 2 +- docs/usage/migrate-to-repokey.rst.inc | 36 --------------------------- src/borg/archiver.py | 2 +- 4 files changed, 6 insertions(+), 38 deletions(-) delete mode 100644 docs/usage/migrate-to-repokey.rst.inc diff --git a/docs/changes.rst b/docs/changes.rst index 13423103..b5dc76f7 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -131,6 +131,10 @@ Changelog Version 1.1.0b4 (not released yet) ---------------------------------- +Compatibility notes: + +- Moved "borg migrate-to-repokey" to "borg key migrate-to-repokey". + New features: - the --exclude-if-present option now supports tagging a folder with any diff --git a/docs/usage.rst b/docs/usage.rst index 85b5a7cd..1e3ffebf 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -774,7 +774,7 @@ Thus, to upgrade a "passphrase" attic repo to a "repokey" borg repo, 2 steps are needed, in this order: - borg upgrade repo -- borg migrate-to-repokey repo +- borg key migrate-to-repokey repo .. include:: usage/recreate.rst.inc diff --git a/docs/usage/migrate-to-repokey.rst.inc b/docs/usage/migrate-to-repokey.rst.inc deleted file mode 100644 index ec5f1a52..00000000 --- a/docs/usage/migrate-to-repokey.rst.inc +++ /dev/null @@ -1,36 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_migrate-to-repokey: - -borg migrate-to-repokey ------------------------ -:: - - borg migrate-to-repokey REPOSITORY - -positional arguments - REPOSITORY - - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command migrates a repository from passphrase mode (not supported any -more) to repokey mode. - -You will be first asked for the repository passphrase (to open it in passphrase -mode). This is the same passphrase as you used to use for this repo before 1.0. - -It will then derive the different secrets from this passphrase. - -Then you will be asked for a new passphrase (twice, for safety). This -passphrase will be used to protect the repokey (which contains these same -secrets in encrypted form). You may use the same passphrase as you used to -use, but you may also use a different one. - -After migrating to repokey mode, you can change the passphrase at any time. -But please note: the secrets will always stay the same and they could always -be derived from your (old) passphrase-mode passphrase. diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 9301b1a7..0c8cf201 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1904,7 +1904,7 @@ class Archiver: But please note: the secrets will always stay the same and they could always be derived from your (old) passphrase-mode passphrase. """) - subparser = subparsers.add_parser('migrate-to-repokey', parents=[common_parser], add_help=False, + subparser = key_parsers.add_parser('migrate-to-repokey', parents=[common_parser], add_help=False, description=self.do_migrate_to_repokey.__doc__, epilog=migrate_to_repokey_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, From 4e0422cdf0de134b11ebb67b80bb5bb242c4a31e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 4 Feb 2017 15:01:23 +0100 Subject: [PATCH 0599/1387] Move change-passphrase to the "key" group (but leave old name, too) --- docs/changes.rst | 1 + docs/usage.rst | 7 ++++--- src/borg/archiver.py | 45 +++++++++++++++++++++++++++++--------------- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index b5dc76f7..084a42d6 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -134,6 +134,7 @@ Version 1.1.0b4 (not released yet) Compatibility notes: - Moved "borg migrate-to-repokey" to "borg key migrate-to-repokey". +- "borg change-passphrase" is deprecated, use "borg key change-passphrase" instead. New features: diff --git a/docs/usage.rst b/docs/usage.rst index 1e3ffebf..db1acc83 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -687,8 +687,9 @@ borgfs .. include:: usage/key_import.rst.inc +.. _borg-change-passphrase: -.. include:: usage/change-passphrase.rst.inc +.. include:: usage/key_change-passphrase.rst.inc Examples ~~~~~~~~ @@ -707,7 +708,7 @@ Examples Done. # Change key file passphrase - $ borg change-passphrase -v /path/to/repo + $ borg key change-passphrase -v /path/to/repo Enter passphrase for key /root/.config/borg/keys/mnt_backup: Enter new passphrase: Enter same passphrase again: @@ -720,7 +721,7 @@ Fully automated using environment variables: $ BORG_NEW_PASSPHRASE=old borg init repo # now "old" is the current passphrase. - $ BORG_PASSPHRASE=old BORG_NEW_PASSPHRASE=new borg change-passphrase repo + $ BORG_PASSPHRASE=old BORG_NEW_PASSPHRASE=new borg key change-passphrase repo # now "new" is the current passphrase. diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 0c8cf201..680ff1e9 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -251,6 +251,11 @@ class Archiver: logger.info('Key location: %s', key.find_key()) return EXIT_SUCCESS + def do_change_passphrase_deprecated(self, args): + logger.warning('"borg change-passphrase" is deprecated and will be removed in Borg 1.2.\n' + 'Use "borg key change-passphrase" instead.') + return self.do_change_passphrase(args) + @with_repository(lock=False, exclusive=False, manifest=False, cache=False) def do_key_export(self, args, repository): """Export the repository key for backup""" @@ -1809,19 +1814,6 @@ class Archiver: help="""show progress display while checking""") self.add_archives_filters_args(subparser) - change_passphrase_epilog = textwrap.dedent(""" - The key files used for repository encryption are optionally passphrase - protected. This command can be used to change this passphrase. - """) - subparser = subparsers.add_parser('change-passphrase', parents=[common_parser], add_help=False, - description=self.do_change_passphrase.__doc__, - epilog=change_passphrase_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help='change repository passphrase') - subparser.set_defaults(func=self.do_change_passphrase) - subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', - type=location_validator(archive=False)) - subparser = subparsers.add_parser('key', parents=[common_parser], add_help=False, description="Manage a keyfile or repokey of a repository", epilog="", @@ -1886,9 +1878,32 @@ class Archiver: default=False, help='interactively import from a backup done with --paper') + change_passphrase_epilog = textwrap.dedent(""" + The key files used for repository encryption are optionally passphrase + protected. This command can be used to change this passphrase. + """) + subparser = key_parsers.add_parser('change-passphrase', parents=[common_parser], add_help=False, + description=self.do_change_passphrase.__doc__, + epilog=change_passphrase_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='change repository passphrase') + subparser.set_defaults(func=self.do_change_passphrase) + subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', + type=location_validator(archive=False)) + + # Borg 1.0 alias for change passphrase (without the "key" subcommand) + subparser = subparsers.add_parser('change-passphrase', parents=[common_parser], add_help=False, + description=self.do_change_passphrase.__doc__, + epilog=change_passphrase_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='change repository passphrase') + subparser.set_defaults(func=self.do_change_passphrase_deprecated) + subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', + type=location_validator(archive=False)) + migrate_to_repokey_epilog = textwrap.dedent(""" - This command migrates a repository from passphrase mode (not supported any - more) to repokey mode. + This command migrates a repository from passphrase mode (removed in Borg 1.0) + to repokey mode. You will be first asked for the repository passphrase (to open it in passphrase mode). This is the same passphrase as you used to use for this repo before 1.0. From 5bc03cc0425eb36642b1644f5b01e6ea30090a15 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 4 Feb 2017 15:16:18 +0100 Subject: [PATCH 0600/1387] migrate-to-repokey: ask using canonical_path() as we do everywhere else --- src/borg/key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/key.py b/src/borg/key.py index 6044797b..e5d736ea 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -476,7 +476,7 @@ class PassphraseKey(ID_HMAC_SHA_256, AESKeyBase): @classmethod def detect(cls, repository, manifest_data): - prompt = 'Enter passphrase for %s: ' % repository._location.orig + prompt = 'Enter passphrase for %s: ' % repository._location.canonical_path() key = cls(repository) passphrase = Passphrase.env_passphrase() if passphrase is None: From c7106e756e78221834052511a64cd6f279f81262 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 5 Feb 2017 11:32:32 +0100 Subject: [PATCH 0601/1387] create real nice man pages --- setup.py | 189 +++++++++++++++++++++++++++++++++++++++++++ src/borg/archiver.py | 26 +++--- 2 files changed, 203 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index 446af345..979c49ef 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,10 @@ # -*- encoding: utf-8 *-* import os +import io import re import sys +from collections import OrderedDict +from datetime import datetime from glob import glob from distutils.command.build import build @@ -326,6 +329,191 @@ class build_usage(Command): shipout(text) +class build_man(Command): + description = 'build man pages' + + user_options = [] + + see_also = { + 'create': ('delete', 'prune', 'check', 'patterns', 'placeholders', 'compression'), + 'recreate': ('patterns', 'placeholders', 'compression'), + 'list': ('info', 'diff', 'prune', 'patterns'), + 'info': ('list', 'diff'), + 'init': ('create', 'delete', 'check', 'list', 'key-import', 'key-export', 'key-change-passphrase'), + 'key-import': ('key-export', ), + 'key-export': ('key-import', ), + 'mount': ('umount', 'extract'), # Would be cooler if these two were on the same page + 'umount': ('mount', ), + 'extract': ('mount', ), + } + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + print('building man pages (in docs/man)', file=sys.stderr) + os.makedirs('docs/man', exist_ok=True) + # allows us to build docs without the C modules fully loaded during help generation + from borg.archiver import Archiver + parser = Archiver(prog='borg').parser + + self.generate_level('', parser, Archiver) + self.build_topic_pages(Archiver) + + def generate_level(self, prefix, parser, Archiver): + is_subcommand = False + choices = {} + for action in parser._actions: + if action.choices is not None and 'SubParsersAction' in str(action.__class__): + is_subcommand = True + for cmd, parser in action.choices.items(): + choices[prefix + cmd] = parser + if prefix and not choices: + return + + for command, parser in sorted(choices.items()): + if command.startswith('debug') or command == 'help': + continue + + man_title = 'borg-' + command.replace(' ', '-') + print('building man page %-40s' % (man_title + '(1)'), end='\r', file=sys.stderr) + + if self.generate_level(command + ' ', parser, Archiver): + continue + + doc = io.StringIO() + write = self.printer(doc) + + self.write_man_header(write, man_title, parser.description) + + self.write_heading(write, 'SYNOPSIS') + write('borg', command, end='') + self.write_usage(write, parser) + write('\n') + + self.write_heading(write, 'DESCRIPTION') + write(parser.epilog) + + self.write_heading(write, 'OPTIONS') + write('See `borg-common(1)` for common options of Borg commands.') + write() + self.write_options(write, parser) + + self.write_see_also(write, man_title) + + self.gen_man_page(man_title, doc.getvalue()) + + # Generate the borg-common(1) man page with the common options. + if 'create' in choices: + doc = io.StringIO() + write = self.printer(doc) + man_title = 'borg-common' + self.write_man_header(write, man_title, 'Common options of Borg commands') + + common_options = [group for group in choices['create']._action_groups if group.title == 'Common options'][0] + + self.write_heading(write, 'SYNOPSIS') + self.write_options_group(write, common_options) + self.write_see_also(write, man_title) + self.gen_man_page(man_title, doc.getvalue()) + + return is_subcommand + + def build_topic_pages(self, Archiver): + for topic, text in Archiver.helptext.items(): + doc = io.StringIO() + write = self.printer(doc) + man_title = 'borg-' + topic + print('building man page %-40s' % (man_title + '(1)'), end='\r', file=sys.stderr) + + self.write_man_header(write, man_title, 'Details regarding ' + topic) + self.write_heading(write, 'DESCRIPTION') + write(text) + self.gen_man_page(man_title, doc.getvalue()) + + def printer(self, fd): + def write(*args, **kwargs): + print(*args, file=fd, **kwargs) + return write + + def write_heading(self, write, header, char='-', double_sided=False): + write() + if double_sided: + write(char * len(header)) + write(header) + write(char * len(header)) + write() + + def write_man_header(self, write, title, description): + self.write_heading(write, title, '=', double_sided=True) + self.write_heading(write, description, double_sided=True) + # man page metadata + write(':Author: The Borg Collective') + write(':Date:', datetime.utcnow().date().isoformat()) + write(':Manual section: 1') + write(':Manual group: borg backup tool') + write() + + def write_see_also(self, write, man_title): + see_also = self.see_also.get(man_title.replace('borg-', ''), ()) + see_also = ['`borg-%s(1)`' % s for s in see_also] + see_also.insert(0, '`borg(1)`') + self.write_heading(write, 'SEE ALSO') + write(', '.join(see_also)) + + def gen_man_page(self, name, rst): + from docutils.writers import manpage + from docutils.core import publish_string + man_page = publish_string(source=rst, writer=manpage.Writer()) + with open('docs/man/%s.1' % name, 'wb') as fd: + fd.write(man_page) + + def write_usage(self, write, parser): + if any(len(o.option_strings) for o in parser._actions): + write(' ', end='') + for option in parser._actions: + if option.option_strings: + continue + write(option.metavar, end=' ') + + def write_options(self, write, parser): + for group in parser._action_groups: + if group.title == 'Common options' or not group._group_actions: + continue + title = 'arguments' if group.title == 'positional arguments' else group.title + self.write_heading(write, title, '+') + self.write_options_group(write, group) + + def write_options_group(self, write, group): + def is_positional_group(group): + return any(not o.option_strings for o in group._group_actions) + + if is_positional_group(group): + for option in group._group_actions: + write(option.metavar) + write(textwrap.indent(option.help or '', ' ' * 4)) + return + + opts = OrderedDict() + + for option in group._group_actions: + if option.metavar: + option_fmt = '%s ' + option.metavar + else: + option_fmt = '%s' + option_str = ', '.join(option_fmt % s for s in option.option_strings) + option_desc = textwrap.dedent((option.help or '') % option.__dict__) + opts[option_str] = textwrap.indent(option_desc, ' ' * 4) + + padding = len(max(opts)) + 1 + + for option, desc in opts.items(): + write(option.ljust(padding), desc) + + class build_api(Command): description = "generate a basic api.rst file based on the modules available" @@ -361,6 +549,7 @@ cmdclass = { 'build_ext': build_ext, 'build_api': build_api, 'build_usage': build_usage, + 'build_man': build_man, 'sdist': Sdist } diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 680ff1e9..dafb76be 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -179,8 +179,7 @@ class Archiver: return matcher, include_patterns def do_serve(self, args): - """Start in server mode. This command is usually not used manually. - """ + """Start in server mode. This command is usually not used manually.""" return RepositoryServer(restrict_to_paths=args.restrict_to_paths, append_only=args.append_only).serve() @with_repository(create=True, exclusive=True, manifest=False) @@ -2024,16 +2023,17 @@ class Archiver: help='add a comment text to the archive') archive_group.add_argument('--timestamp', dest='timestamp', type=timestamp, default=None, - metavar='yyyy-mm-ddThh:mm:ss', - help='manually specify the archive creation date/time (UTC). ' + metavar='TIMESTAMP', + help='manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). ' 'alternatively, give a reference file/directory.') archive_group.add_argument('-c', '--checkpoint-interval', dest='checkpoint_interval', type=int, default=1800, metavar='SECONDS', help='write checkpoint every SECONDS seconds (Default: 1800)') archive_group.add_argument('--chunker-params', dest='chunker_params', type=ChunkerParams, default=CHUNKER_PARAMS, - metavar='CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE', - help='specify the chunker parameters. default: %d,%d,%d,%d' % CHUNKER_PARAMS) + metavar='PARAMS', + help='specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, ' + 'HASH_MASK_BITS, HASH_WINDOW_SIZE). default: %d,%d,%d,%d' % CHUNKER_PARAMS) archive_group.add_argument('-C', '--compression', dest='compression', type=CompressionSpec, default=dict(name='none'), metavar='COMPRESSION', help='select compression algorithm, see the output of the ' @@ -2348,7 +2348,7 @@ class Archiver: Also, prune automatically removes checkpoint archives (incomplete archives left behind by interrupted backup runs) except if the checkpoint is the latest archive (and thus still needed). Checkpoint archives are not considered when - comparing archive counts against the retention limits (--keep-*). + comparing archive counts against the retention limits (--keep-X). If a prefix is set with -P, then only archives that start with the prefix are considered for deletion and only those archives count towards the totals @@ -2607,8 +2607,8 @@ class Archiver: help='add a comment text to the archive') archive_group.add_argument('--timestamp', dest='timestamp', type=timestamp, default=None, - metavar='yyyy-mm-ddThh:mm:ss', - help='manually specify the archive creation date/time (UTC). ' + metavar='TIMESTAMP', + help='manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). ' 'alternatively, give a reference file/directory.') archive_group.add_argument('-C', '--compression', dest='compression', type=CompressionSpec, default=None, metavar='COMPRESSION', @@ -2623,9 +2623,11 @@ class Archiver: help='read compression patterns from COMPRESSIONCONFIG, see the output of the ' '"borg help compression" command for details.') archive_group.add_argument('--chunker-params', dest='chunker_params', - type=ChunkerParams, default=None, - metavar='CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE', - help='specify the chunker parameters (or "default").') + type=ChunkerParams, default=CHUNKER_PARAMS, + metavar='PARAMS', + help='specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, ' + 'HASH_MASK_BITS, HASH_WINDOW_SIZE) or "default" to use the current defaults. ' + 'default: %d,%d,%d,%d' % CHUNKER_PARAMS) subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='', type=location_validator(), From e5ea876f900f59e4aea718e0764c2e4212213033 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 5 Feb 2017 11:53:02 +0100 Subject: [PATCH 0602/1387] man pages: remove reference to borg(1) -- would have to be written first A compact introduction and perhaps a condensed quickstart would be good for borg(1). --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 979c49ef..54318b01 100644 --- a/setup.py +++ b/setup.py @@ -460,7 +460,7 @@ class build_man(Command): def write_see_also(self, write, man_title): see_also = self.see_also.get(man_title.replace('borg-', ''), ()) see_also = ['`borg-%s(1)`' % s for s in see_also] - see_also.insert(0, '`borg(1)`') + see_also.insert(0, '`borg-common(1)`') self.write_heading(write, 'SEE ALSO') write(', '.join(see_also)) From 83bb25d8481407ec3363c9c07342e7ba38cec2bd Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 5 Feb 2017 14:08:53 +0100 Subject: [PATCH 0603/1387] man pages: add EXAMPLES hacky, but works. Better would be to make a separate docs/examples dir with only the examples in them, separated by command. Or, putting these different sections; DESCRIPTION, EXAMPLES and NOTES into the --help doc, but separately of course, so that they can be aptly formatted for different media (html, --help, man). --- setup.py | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 54318b01..2062221e 100644 --- a/setup.py +++ b/setup.py @@ -347,6 +347,12 @@ class build_man(Command): 'extract': ('mount', ), } + rst_prelude = textwrap.dedent(""" + .. role:: ref(title) + + .. |project_name| replace:: Borg + """) + def initialize_options(self): pass @@ -379,14 +385,12 @@ class build_man(Command): continue man_title = 'borg-' + command.replace(' ', '-') - print('building man page %-40s' % (man_title + '(1)'), end='\r', file=sys.stderr) + print('building man page', man_title + '(1)', file=sys.stderr) if self.generate_level(command + ' ', parser, Archiver): continue - doc = io.StringIO() - write = self.printer(doc) - + doc, write = self.new_doc() self.write_man_header(write, man_title, parser.description) self.write_heading(write, 'SYNOPSIS') @@ -402,14 +406,15 @@ class build_man(Command): write() self.write_options(write, parser) + self.write_examples(write, command) + self.write_see_also(write, man_title) self.gen_man_page(man_title, doc.getvalue()) # Generate the borg-common(1) man page with the common options. if 'create' in choices: - doc = io.StringIO() - write = self.printer(doc) + doc, write = self.new_doc() man_title = 'borg-common' self.write_man_header(write, man_title, 'Common options of Borg commands') @@ -424,16 +429,21 @@ class build_man(Command): def build_topic_pages(self, Archiver): for topic, text in Archiver.helptext.items(): - doc = io.StringIO() - write = self.printer(doc) + doc, write = self.new_doc() man_title = 'borg-' + topic - print('building man page %-40s' % (man_title + '(1)'), end='\r', file=sys.stderr) + print('building man page', man_title + '(1)', file=sys.stderr) self.write_man_header(write, man_title, 'Details regarding ' + topic) self.write_heading(write, 'DESCRIPTION') write(text) self.gen_man_page(man_title, doc.getvalue()) + def new_doc(self): + doc = io.StringIO(self.rst_prelude) + doc.read() + write = self.printer(doc) + return doc, write + def printer(self, fd): def write(*args, **kwargs): print(*args, file=fd, **kwargs) @@ -457,6 +467,22 @@ class build_man(Command): write(':Manual group: borg backup tool') write() + def write_examples(self, write, command): + with open('docs/usage.rst') as fd: + usage = fd.read() + usage_include = '.. include:: usage/%s.rst.inc' % command + begin = usage.find(usage_include) + end = usage.find('.. include', begin + 1) + examples = usage[begin:end] + examples = examples.replace(usage_include, '') + examples = examples.replace('Examples\n~~~~~~~~', '') + examples = examples.replace('Miscellaneous Help\n------------------', '') + examples = re.sub('^(~+)$', lambda matches: '+' * len(matches.group(0)), examples, flags=re.MULTILINE) + examples = examples.strip() + if examples: + self.write_heading(write, 'EXAMPLES', '-') + write(examples) + def write_see_also(self, write, man_title): see_also = self.see_also.get(man_title.replace('borg-', ''), ()) see_also = ['`borg-%s(1)`' % s for s in see_also] From 7e486074e8eab870a19b13ff86097e142d82d923 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 5 Feb 2017 13:43:19 +0100 Subject: [PATCH 0604/1387] docs: list: don't print key listings in fat (html + man) --- src/borg/archiver.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index dafb76be..61f26a56 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2204,12 +2204,15 @@ class Archiver: See the "borg help patterns" command for more help on exclude patterns. The following keys are available for --format: + """) + BaseFormatter.keys_help() + textwrap.dedent(""" - -- Keys for listing repository archives: + Keys for listing repository archives: + """) + ArchiveFormatter.keys_help() + textwrap.dedent(""" - -- Keys for listing archive files: + Keys for listing archive files: + """) + ItemFormatter.keys_help() subparser = subparsers.add_parser('list', parents=[common_parser], add_help=False, description=self.do_list.__doc__, From d6a26ca26df63af3e5dd6381f37d475991664ba5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 5 Feb 2017 14:16:01 +0100 Subject: [PATCH 0605/1387] docs: fix examples using borg init without -e/--encryption --- docs/usage.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index db1acc83..d160bc0a 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -359,14 +359,14 @@ Examples ~~~~~~~~ :: - # Local repository (default is to use encryption in repokey mode) - $ borg init /path/to/repo + # Local repository, repokey encryption, BLAKE2b (often faster, since Borg 1.1) + $ borg init --encryption=repokey-blake2 /path/to/repo # Local repository (no encryption) $ borg init --encryption=none /path/to/repo # Remote repository (accesses a remote borg via ssh) - $ borg init user@hostname:backup + $ borg init --encryption=repokey-blake2 user@hostname:backup # Remote repository (store the key your home dir) $ borg init --encryption=keyfile user@hostname:backup @@ -528,7 +528,7 @@ Examples ~~~~~~~~ :: - $ borg init testrepo + $ borg init -e=none testrepo $ mkdir testdir $ cd testdir $ echo asdf > file1 @@ -719,7 +719,7 @@ Fully automated using environment variables: :: - $ BORG_NEW_PASSPHRASE=old borg init repo + $ BORG_NEW_PASSPHRASE=old borg init -e=repokey repo # now "old" is the current passphrase. $ BORG_PASSPHRASE=old BORG_NEW_PASSPHRASE=new borg key change-passphrase repo # now "new" is the current passphrase. From 1d91d2699ca5f24404fe8cc2014452dafd31affb Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 5 Feb 2017 14:22:06 +0100 Subject: [PATCH 0606/1387] commit man pages --- docs/man/borg-break-lock.1 | 56 ++++++ docs/man/borg-change-passphrase.1 | 52 ++++++ docs/man/borg-check.1 | 148 +++++++++++++++ docs/man/borg-common.1 | 87 +++++++++ docs/man/borg-compression.1 | 156 ++++++++++++++++ docs/man/borg-create.1 | 233 +++++++++++++++++++++++ docs/man/borg-delete.1 | 109 +++++++++++ docs/man/borg-diff.1 | 133 ++++++++++++++ docs/man/borg-extract.1 | 131 +++++++++++++ docs/man/borg-info.1 | 107 +++++++++++ docs/man/borg-init.1 | 157 ++++++++++++++++ docs/man/borg-key-change-passphrase.1 | 52 ++++++ docs/man/borg-key-export.1 | 78 ++++++++ docs/man/borg-key-import.1 | 67 +++++++ docs/man/borg-key-migrate-to-repokey.1 | 66 +++++++ docs/man/borg-list.1 | 245 +++++++++++++++++++++++++ docs/man/borg-mount.1 | 115 ++++++++++++ docs/man/borg-patterns.1 | 144 +++++++++++++++ docs/man/borg-placeholders.1 | 126 +++++++++++++ docs/man/borg-prune.1 | 177 ++++++++++++++++++ docs/man/borg-recreate.1 | 192 +++++++++++++++++++ docs/man/borg-rename.1 | 76 ++++++++ docs/man/borg-serve.1 | 80 ++++++++ docs/man/borg-umount.1 | 115 ++++++++++++ docs/man/borg-upgrade.1 | 170 +++++++++++++++++ docs/man/borg-with-lock.1 | 71 +++++++ 26 files changed, 3143 insertions(+) create mode 100644 docs/man/borg-break-lock.1 create mode 100644 docs/man/borg-change-passphrase.1 create mode 100644 docs/man/borg-check.1 create mode 100644 docs/man/borg-common.1 create mode 100644 docs/man/borg-compression.1 create mode 100644 docs/man/borg-create.1 create mode 100644 docs/man/borg-delete.1 create mode 100644 docs/man/borg-diff.1 create mode 100644 docs/man/borg-extract.1 create mode 100644 docs/man/borg-info.1 create mode 100644 docs/man/borg-init.1 create mode 100644 docs/man/borg-key-change-passphrase.1 create mode 100644 docs/man/borg-key-export.1 create mode 100644 docs/man/borg-key-import.1 create mode 100644 docs/man/borg-key-migrate-to-repokey.1 create mode 100644 docs/man/borg-list.1 create mode 100644 docs/man/borg-mount.1 create mode 100644 docs/man/borg-patterns.1 create mode 100644 docs/man/borg-placeholders.1 create mode 100644 docs/man/borg-prune.1 create mode 100644 docs/man/borg-recreate.1 create mode 100644 docs/man/borg-rename.1 create mode 100644 docs/man/borg-serve.1 create mode 100644 docs/man/borg-umount.1 create mode 100644 docs/man/borg-upgrade.1 create mode 100644 docs/man/borg-with-lock.1 diff --git a/docs/man/borg-break-lock.1 b/docs/man/borg-break-lock.1 new file mode 100644 index 00000000..b5870e56 --- /dev/null +++ b/docs/man/borg-break-lock.1 @@ -0,0 +1,56 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-BREAK-LOCK 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-break-lock \- Break the repository lock (e.g. in case it was left by a dead borg. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg break\-lock REPOSITORY +.SH DESCRIPTION +.sp +This command breaks the repository and cache locks. +Please use carefully and only while no borg process (on any machine) is +trying to access the Cache or the Repository. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B REPOSITORY +repository for which to break the locks +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-change-passphrase.1 b/docs/man/borg-change-passphrase.1 new file mode 100644 index 00000000..fb32bfa2 --- /dev/null +++ b/docs/man/borg-change-passphrase.1 @@ -0,0 +1,52 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-CHANGE-PASSPHRASE 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-change-passphrase \- Change repository key file passphrase +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg change\-passphrase REPOSITORY +.SH DESCRIPTION +.sp +The key files used for repository encryption are optionally passphrase +protected. This command can be used to change this passphrase. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.sp +REPOSITORY +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-check.1 b/docs/man/borg-check.1 new file mode 100644 index 00000000..6608887b --- /dev/null +++ b/docs/man/borg-check.1 @@ -0,0 +1,148 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-CHECK 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-check \- Check repository consistency +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg check REPOSITORY_OR_ARCHIVE +.SH DESCRIPTION +.sp +The check command verifies the consistency of a repository and the corresponding archives. +.sp +First, the underlying repository data files are checked: +.INDENT 0.0 +.IP \(bu 2 +For all segments the segment magic (header) is checked +.IP \(bu 2 +For all objects stored in the segments, all metadata (e.g. crc and size) and +all data is read. The read data is checked by size and CRC. Bit rot and other +types of accidental damage can be detected this way. +.IP \(bu 2 +If we are in repair mode and a integrity error is detected for a segment, +we try to recover as many objects from the segment as possible. +.IP \(bu 2 +In repair mode, it makes sure that the index is consistent with the data +stored in the segments. +.IP \(bu 2 +If you use a remote repo server via ssh:, the repo check is executed on the +repo server without causing significant network traffic. +.IP \(bu 2 +The repository check can be skipped using the \-\-archives\-only option. +.UNINDENT +.sp +Second, the consistency and correctness of the archive metadata is verified: +.INDENT 0.0 +.IP \(bu 2 +Is the repo manifest present? If not, it is rebuilt from archive metadata +chunks (this requires reading and decrypting of all metadata and data). +.IP \(bu 2 +Check if archive metadata chunk is present. if not, remove archive from +manifest. +.IP \(bu 2 +For all files (items) in the archive, for all chunks referenced by these +files, check if chunk is present. +If a chunk is not present and we are in repair mode, replace it with a same\-size +replacement chunk of zeros. +If a previously lost chunk reappears (e.g. via a later backup) and we are in +repair mode, the all\-zero replacement chunk will be replaced by the correct chunk. +This requires reading of archive and file metadata, but not data. +.IP \(bu 2 +If we are in repair mode and we checked all the archives: delete orphaned +chunks from the repo. +.IP \(bu 2 +if you use a remote repo server via ssh:, the archive check is executed on +the client machine (because if encryption is enabled, the checks will require +decryption and this is always done client\-side, because key access will be +required). +.IP \(bu 2 +The archive checks can be time consuming, they can be skipped using the +\-\-repository\-only option. +.UNINDENT +.sp +The \-\-verify\-data option will perform a full integrity verification (as opposed to +checking the CRC32 of the segment) of data, which means reading the data from the +repository, decrypting and decompressing it. This is a cryptographic verification, +which will detect (accidental) corruption. For encrypted repositories it is +tamper\-resistant as well, unless the attacker has access to the keys. +.sp +It is also very slow. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B REPOSITORY_OR_ARCHIVE +repository or archive to check consistency of +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-\-repository\-only +only perform repository checks +.TP +.B \-\-archives\-only +only perform archives checks +.TP +.B \-\-verify\-data +perform cryptographic archive data integrity verification (conflicts with \-\-repository\-only) +.TP +.B \-\-repair +attempt to repair any inconsistencies found +.TP +.B \-\-save\-space +work slower, but using less space +.TP +.B \-p\fP,\fB \-\-progress +show progress display while checking +.UNINDENT +.SS filters +.INDENT 0.0 +.TP +.B \-P\fP,\fB \-\-prefix +only consider archive names starting with this prefix +.TP +.B \-\-sort\-by +Comma\-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp +.TP +.BI \-\-first \ N +consider first N archives after other filters were applied +.TP +.BI \-\-last \ N +consider last N archives after other filters were applied +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-common.1 b/docs/man/borg-common.1 new file mode 100644 index 00000000..17afcff9 --- /dev/null +++ b/docs/man/borg-common.1 @@ -0,0 +1,87 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-COMMON 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-common \- Common options of Borg commands +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.INDENT 0.0 +.TP +.B \-h\fP,\fB \-\-help +show this help message and exit +.TP +.B \-\-critical +work on log level CRITICAL +.TP +.B \-\-error +work on log level ERROR +.TP +.B \-\-warning +work on log level WARNING (default) +.TP +.B \-\-info\fP,\fB \-v\fP,\fB \-\-verbose +work on log level INFO +.TP +.B \-\-debug +enable debug output, work on log level DEBUG +.TP +.BI \-\-debug\-topic \ TOPIC +enable TOPIC debugging (can be specified multiple times). The logger path is borg.debug. if TOPIC is not fully qualified. +.TP +.BI \-\-lock\-wait \ N +wait for the lock, but max. N seconds (default: 1). +.TP +.B \-\-show\-version +show/log the borg version +.TP +.B \-\-show\-rc +show/log the return code (rc) +.TP +.B \-\-no\-files\-cache +do not load/update the file metadata cache used to detect unchanged files +.TP +.BI \-\-umask \ M +set umask to M (local and remote, default: 0077) +.TP +.BI \-\-remote\-path \ PATH +set remote path to executable (default: "borg") +.TP +.BI \-\-remote\-ratelimit \ rate +set remote network upload rate limit in kiByte/s (default: 0=unlimited) +.TP +.B \-\-consider\-part\-files +treat part files like normal files (e.g. to list/extract them) +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-compression.1 b/docs/man/borg-compression.1 new file mode 100644 index 00000000..cc5fbafd --- /dev/null +++ b/docs/man/borg-compression.1 @@ -0,0 +1,156 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-COMPRESSION 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-compression \- Details regarding compression +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH DESCRIPTION +.sp +Compression is off by default, if you want some, you have to specify what you want. +.sp +Valid compression specifiers are: +.sp +none +.INDENT 0.0 +.INDENT 3.5 +Do not compress. (default) +.UNINDENT +.UNINDENT +.sp +lz4 +.INDENT 0.0 +.INDENT 3.5 +Use lz4 compression. High speed, low compression. +.UNINDENT +.UNINDENT +.sp +zlib[,L] +.INDENT 0.0 +.INDENT 3.5 +Use zlib ("gz") compression. Medium speed, medium compression. +If you do not explicitely give the compression level L (ranging from 0 +to 9), it will use level 6. +Giving level 0 (means "no compression", but still has zlib protocol +overhead) is usually pointless, you better use "none" compression. +.UNINDENT +.UNINDENT +.sp +lzma[,L] +.INDENT 0.0 +.INDENT 3.5 +Use lzma ("xz") compression. Low speed, high compression. +If you do not explicitely give the compression level L (ranging from 0 +to 9), it will use level 6. +Giving levels above 6 is pointless and counterproductive because it does +not compress better due to the buffer size used by borg \- but it wastes +lots of CPU cycles and RAM. +.UNINDENT +.UNINDENT +.sp +auto,C[,L] +.INDENT 0.0 +.INDENT 3.5 +Use a built\-in heuristic to decide per chunk whether to compress or not. +The heuristic tries with lz4 whether the data is compressible. +For incompressible data, it will not use compression (uses "none"). +For compressible data, it uses the given C[,L] compression \- with C[,L] +being any valid compression specifier. +.UNINDENT +.UNINDENT +.sp +The decision about which compression to use is done by borg like this: +.INDENT 0.0 +.IP 1. 3 +find a compression specifier (per file): +match the path/filename against all patterns in all \-\-compression\-from +files (if any). If a pattern matches, use the compression spec given for +that pattern. If no pattern matches (and also if you do not give any +\-\-compression\-from option), default to the compression spec given by +\-\-compression. See docs/misc/compression.conf for an example config. +.IP 2. 3 +if the found compression spec is not "auto", the decision is taken: +use the found compression spec. +.IP 3. 3 +if the found compression spec is "auto", test compressibility of each +chunk using lz4. +If it is compressible, use the C,[L] compression spec given within the +"auto" specifier. If it is not compressible, use no compression. +.UNINDENT +.sp +Examples: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +borg create \-\-compression lz4 REPO::ARCHIVE data +borg create \-\-compression zlib REPO::ARCHIVE data +borg create \-\-compression zlib,1 REPO::ARCHIVE data +borg create \-\-compression auto,lzma,6 REPO::ARCHIVE data +borg create \-\-compression\-from compression.conf \-\-compression auto,lzma ... +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +compression.conf has entries like: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# example config file for \-\-compression\-from option +# +# Format of non\-comment / non\-empty lines: +# : +# compression\-spec is same format as for \-\-compression option +# path/filename pattern is same format as for \-\-exclude option +none:*.gz +none:*.zip +none:*.mp3 +none:*.ogg +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +General remarks: +.sp +It is no problem to mix different compression methods in one repo, +deduplication is done on the source data chunks (not on the compressed +or encrypted data). +.sp +If some specific chunk was once compressed and stored into the repo, creating +another backup that also uses this chunk will not change the stored chunk. +So if you use different compression specs for the backups, whichever stores a +chunk first determines its compression. See also borg recreate. +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-create.1 b/docs/man/borg-create.1 new file mode 100644 index 00000000..f3e3c9be --- /dev/null +++ b/docs/man/borg-create.1 @@ -0,0 +1,233 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-CREATE 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-create \- Create new archive +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg create ARCHIVE PATH +.SH DESCRIPTION +.sp +This command creates a backup archive containing all files found while recursively +traversing all paths specified. When giving \(aq\-\(aq as path, borg will read data +from standard input and create a file \(aqstdin\(aq in the created archive from that +data. +.sp +The archive will consume almost no disk space for files or parts of files that +have already been stored in other archives. +.sp +The archive name needs to be unique. It must not end in \(aq.checkpoint\(aq or +\(aq.checkpoint.N\(aq (with N being a number), because these names are used for +checkpoints and treated in special ways. +.sp +In the archive name, you may use the following placeholders: +{now}, {utcnow}, {fqdn}, {hostname}, {user} and some others. +.sp +To speed up pulling backups over sshfs and similar network file systems which do +not provide correct inode information the \-\-ignore\-inode flag can be used. This +potentially decreases reliability of change detection, while avoiding always reading +all files on these file systems. +.sp +See the output of the "borg help patterns" command for more help on exclude patterns. +See the output of the "borg help placeholders" command for more help on placeholders. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B ARCHIVE +name of archive to create (must be also a valid directory name) +.TP +.B PATH +paths to archive +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-n\fP,\fB \-\-dry\-run +do not create a backup archive +.TP +.B \-s\fP,\fB \-\-stats +print statistics for the created archive +.TP +.B \-p\fP,\fB \-\-progress +show progress display while creating the archive, showing Original, Compressed and Deduplicated sizes, followed by the Number of files seen and the path being processed, default: False +.TP +.B \-\-list +output verbose list of items (files, dirs, ...) +.TP +.BI \-\-filter \ STATUSCHARS +only display items with the given status characters +.UNINDENT +.SS Exclusion options +.INDENT 0.0 +.TP +.BI \-e \ PATTERN\fP,\fB \ \-\-exclude \ PATTERN +exclude paths matching PATTERN +.TP +.BI \-\-exclude\-from \ EXCLUDEFILE +read exclude patterns from EXCLUDEFILE, one per line +.TP +.B \-\-exclude\-caches +exclude directories that contain a CACHEDIR.TAG file (\fI\%http://www.brynosaurus.com/cachedir/spec.html\fP) +.TP +.BI \-\-exclude\-if\-present \ NAME +exclude directories that are tagged by containing a filesystem object with the given NAME +.TP +.B \-\-keep\-exclude\-tags\fP,\fB \-\-keep\-tag\-files +keep tag objects (i.e.: arguments to \-\-exclude\-if\-present) in otherwise excluded caches/directories +.UNINDENT +.SS Filesystem options +.INDENT 0.0 +.TP +.B \-x\fP,\fB \-\-one\-file\-system +stay in same file system, do not cross mount points +.TP +.B \-\-numeric\-owner +only store numeric user and group identifiers +.TP +.B \-\-noatime +do not store atime into archive +.TP +.B \-\-noctime +do not store ctime into archive +.TP +.B \-\-ignore\-inode +ignore inode data in the file metadata cache used to detect unchanged files. +.TP +.B \-\-read\-special +open and read block and char device files as well as FIFOs as if they were regular files. Also follows symlinks pointing to these kinds of files. +.UNINDENT +.SS Archive options +.INDENT 0.0 +.TP +.BI \-\-comment \ COMMENT +add a comment text to the archive +.TP +.BI \-\-timestamp \ TIMESTAMP +manually specify the archive creation date/time (UTC, yyyy\-mm\-ddThh:mm:ss format). alternatively, give a reference file/directory. +.TP +.BI \-c \ SECONDS\fP,\fB \ \-\-checkpoint\-interval \ SECONDS +write checkpoint every SECONDS seconds (Default: 1800) +.TP +.BI \-\-chunker\-params \ PARAMS +specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: 19,23,21,4095 +.TP +.BI \-C \ COMPRESSION\fP,\fB \ \-\-compression \ COMPRESSION +select compression algorithm, see the output of the "borg help compression" command for details. +.TP +.BI \-\-compression\-from \ COMPRESSIONCONFIG +read compression patterns from COMPRESSIONCONFIG, see the output of the "borg help compression" command for details. +.UNINDENT +.SH EXAMPLES +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# Backup ~/Documents into an archive named "my\-documents" +$ borg create /path/to/repo::my\-documents ~/Documents + +# same, but list all files as we process them +$ borg create \-\-list /path/to/repo::my\-documents ~/Documents + +# Backup ~/Documents and ~/src but exclude pyc files +$ borg create /path/to/repo::my\-files \e + ~/Documents \e + ~/src \e + \-\-exclude \(aq*.pyc\(aq + +# Backup home directories excluding image thumbnails (i.e. only +# /home/*/.thumbnails is excluded, not /home/*/*/.thumbnails) +$ borg create /path/to/repo::my\-files /home \e + \-\-exclude \(aqre:^/home/[^/]+/\e.thumbnails/\(aq + +# Do the same using a shell\-style pattern +$ borg create /path/to/repo::my\-files /home \e + \-\-exclude \(aqsh:/home/*/.thumbnails\(aq + +# Backup the root filesystem into an archive named "root\-YYYY\-MM\-DD" +# use zlib compression (good, but slow) \- default is no compression +$ borg create \-C zlib,6 /path/to/repo::root\-{now:%Y\-%m\-%d} / \-\-one\-file\-system + +# Backup a remote host locally ("pull" style) using sshfs +$ mkdir sshfs\-mount +$ sshfs root@example.com:/ sshfs\-mount +$ cd sshfs\-mount +$ borg create /path/to/repo::example.com\-root\-{now:%Y\-%m\-%d} . +$ cd .. +$ fusermount \-u sshfs\-mount + +# Make a big effort in fine granular deduplication (big chunk management +# overhead, needs a lot of RAM and disk space, see formula in internals +# docs \- same parameters as borg < 1.0 or attic): +$ borg create \-\-chunker\-params 10,23,16,4095 /path/to/repo::small /smallstuff + +# Backup a raw device (must not be active/in use/mounted at that time) +$ dd if=/dev/sdx bs=10M | borg create /path/to/repo::my\-sdx \- + +# No compression (default) +$ borg create /path/to/repo::arch ~ + +# Super fast, low compression +$ borg create \-\-compression lz4 /path/to/repo::arch ~ + +# Less fast, higher compression (N = 0..9) +$ borg create \-\-compression zlib,N /path/to/repo::arch ~ + +# Even slower, even higher compression (N = 0..9) +$ borg create \-\-compression lzma,N /path/to/repo::arch ~ + +# Use short hostname, user name and current time in archive name +$ borg create /path/to/repo::{hostname}\-{user}\-{now} ~ +# Similar, use the same datetime format as borg 1.1 will have as default +$ borg create /path/to/repo::{hostname}\-{user}\-{now:%Y\-%m\-%dT%H:%M:%S} ~ +# As above, but add nanoseconds +$ borg create /path/to/repo::{hostname}\-{user}\-{now:%Y\-%m\-%dT%H:%M:%S.%f} ~ +.ft P +.fi +.UNINDENT +.UNINDENT +.SS Notes +.INDENT 0.0 +.IP \(bu 2 +the \-\-exclude patterns are not like tar. In tar \-\-exclude .bundler/gems will +exclude foo/.bundler/gems. In borg it will not, you need to use \-\-exclude +\(aq*/.bundler/gems\(aq to get the same effect. See \fBborg help patterns\fP for +more information. +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP, \fIborg\-delete(1)\fP, \fIborg\-prune(1)\fP, \fIborg\-check(1)\fP, \fIborg\-patterns(1)\fP, \fIborg\-placeholders(1)\fP, \fIborg\-compression(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-delete.1 b/docs/man/borg-delete.1 new file mode 100644 index 00000000..e3a73797 --- /dev/null +++ b/docs/man/borg-delete.1 @@ -0,0 +1,109 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-DELETE 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-delete \- Delete an existing repository or archives +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg delete TARGET +.SH DESCRIPTION +.sp +This command deletes an archive from the repository or the complete repository. +Disk space is reclaimed accordingly. If you delete the complete repository, the +local cache for it (if any) is also deleted. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B TARGET +archive or repository to delete +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-p\fP,\fB \-\-progress +show progress display while deleting a single archive +.TP +.B \-s\fP,\fB \-\-stats +print statistics for the deleted archive +.TP +.B \-c\fP,\fB \-\-cache\-only +delete only the local cache for the given repository +.TP +.B \-\-force +force deletion of corrupted archives +.TP +.B \-\-save\-space +work slower, but using less space +.UNINDENT +.SS filters +.INDENT 0.0 +.TP +.B \-P\fP,\fB \-\-prefix +only consider archive names starting with this prefix +.TP +.B \-\-sort\-by +Comma\-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp +.TP +.BI \-\-first \ N +consider first N archives after other filters were applied +.TP +.BI \-\-last \ N +consider last N archives after other filters were applied +.UNINDENT +.SH EXAMPLES +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# delete a single backup archive: +$ borg delete /path/to/repo::Monday + +# delete the whole repository and the related local cache: +$ borg delete /path/to/repo +You requested to completely DELETE the repository *including* all archives it contains: +repo Mon, 2016\-02\-15 19:26:54 +root\-2016\-02\-15 Mon, 2016\-02\-15 19:36:29 +newname Mon, 2016\-02\-15 19:50:19 +Type \(aqYES\(aq if you understand this and want to continue: YES +.ft P +.fi +.UNINDENT +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-diff.1 b/docs/man/borg-diff.1 new file mode 100644 index 00000000..dc3d9be9 --- /dev/null +++ b/docs/man/borg-diff.1 @@ -0,0 +1,133 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-DIFF 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-diff \- Diff contents of two archives +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg diff REPO_ARCHIVE1 ARCHIVE2 PATH +.SH DESCRIPTION +.sp +This command finds differences (file contents, user/group/mode) between archives. +.sp +A repository location and an archive name must be specified for REPO_ARCHIVE1. +ARCHIVE2 is just another archive name in same repository (no repository location +allowed). +.sp +For archives created with Borg 1.1 or newer diff automatically detects whether +the archives are created with the same chunker params. If so, only chunk IDs +are compared, which is very fast. +.sp +For archives prior to Borg 1.1 chunk contents are compared by default. +If you did not create the archives with different chunker params, +pass \-\-same\-chunker\-params. +Note that the chunker params changed from Borg 0.xx to 1.0. +.sp +See the output of the "borg help patterns" command for more help on exclude patterns. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B REPO_ARCHIVE1 +repository location and ARCHIVE1 name +.TP +.B ARCHIVE2 +ARCHIVE2 name (no repository location allowed) +.TP +.B PATH +paths of items inside the archives to compare; patterns are supported +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.BI \-e \ PATTERN\fP,\fB \ \-\-exclude \ PATTERN +exclude paths matching PATTERN +.TP +.BI \-\-exclude\-from \ EXCLUDEFILE +read exclude patterns from EXCLUDEFILE, one per line +.TP +.B \-\-numeric\-owner +only consider numeric user and group identifiers +.TP +.B \-\-same\-chunker\-params +Override check of chunker parameters. +.TP +.B \-\-sort +Sort the output lines by file path. +.UNINDENT +.SH EXAMPLES +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg init \-e=none testrepo +$ mkdir testdir +$ cd testdir +$ echo asdf > file1 +$ dd if=/dev/urandom bs=1M count=4 > file2 +$ touch file3 +$ borg create ../testrepo::archive1 . + +$ chmod a+x file1 +$ echo "something" >> file2 +$ borg create ../testrepo::archive2 . + +$ rm file3 +$ touch file4 +$ borg create ../testrepo::archive3 . + +$ cd .. +$ borg diff testrepo::archive1 archive2 +[\-rw\-r\-\-r\-\- \-> \-rwxr\-xr\-x] file1 + +135 B \-252 B file2 + +$ borg diff testrepo::archive2 archive3 +added 0 B file4 +removed 0 B file3 + +$ borg diff testrepo::archive1 archive3 +[\-rw\-r\-\-r\-\- \-> \-rwxr\-xr\-x] file1 + +135 B \-252 B file2 +added 0 B file4 +removed 0 B file3 +.ft P +.fi +.UNINDENT +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-extract.1 b/docs/man/borg-extract.1 new file mode 100644 index 00000000..2770a514 --- /dev/null +++ b/docs/man/borg-extract.1 @@ -0,0 +1,131 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-EXTRACT 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-extract \- Extract archive contents +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg extract ARCHIVE PATH +.SH DESCRIPTION +.sp +This command extracts the contents of an archive. By default the entire +archive is extracted but a subset of files and directories can be selected +by passing a list of \fBPATHs\fP as arguments. The file selection can further +be restricted by using the \fB\-\-exclude\fP option. +.sp +See the output of the "borg help patterns" command for more help on exclude patterns. +.sp +By using \fB\-\-dry\-run\fP, you can do all extraction steps except actually writing the +output data: reading metadata and data chunks from the repo, checking the hash/hmac, +decrypting, decompressing. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B ARCHIVE +archive to extract +.TP +.B PATH +paths to extract; patterns are supported +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-p\fP,\fB \-\-progress +show progress while extracting (may be slower) +.TP +.B \-\-list +output verbose list of items (files, dirs, ...) +.TP +.B \-n\fP,\fB \-\-dry\-run +do not actually change any files +.TP +.BI \-e \ PATTERN\fP,\fB \ \-\-exclude \ PATTERN +exclude paths matching PATTERN +.TP +.BI \-\-exclude\-from \ EXCLUDEFILE +read exclude patterns from EXCLUDEFILE, one per line +.TP +.B \-\-numeric\-owner +only obey numeric user and group identifiers +.TP +.BI \-\-strip\-components \ NUMBER +Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. +.TP +.B \-\-stdout +write all extracted data to stdout +.TP +.B \-\-sparse +create holes in output sparse file from all\-zero chunks +.UNINDENT +.SH EXAMPLES +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# Extract entire archive +$ borg extract /path/to/repo::my\-files + +# Extract entire archive and list files while processing +$ borg extract \-\-list /path/to/repo::my\-files + +# Verify whether an archive could be successfully extracted, but do not write files to disk +$ borg extract \-\-dry\-run /path/to/repo::my\-files + +# Extract the "src" directory +$ borg extract /path/to/repo::my\-files home/USERNAME/src + +# Extract the "src" directory but exclude object files +$ borg extract /path/to/repo::my\-files home/USERNAME/src \-\-exclude \(aq*.o\(aq + +# Restore a raw device (must not be active/in use/mounted at that time) +$ borg extract \-\-stdout /path/to/repo::my\-sdx | dd of=/dev/sdx bs=10M +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +\fBNOTE:\fP +.INDENT 0.0 +.INDENT 3.5 +Currently, extract always writes into the current working directory ("."), +so make sure you \fBcd\fP to the right place before calling \fBborg extract\fP\&. +.UNINDENT +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP, \fIborg\-mount(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-info.1 b/docs/man/borg-info.1 new file mode 100644 index 00000000..2609912d --- /dev/null +++ b/docs/man/borg-info.1 @@ -0,0 +1,107 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-INFO 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-info \- Show archive details such as disk space used +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg info REPOSITORY_OR_ARCHIVE +.SH DESCRIPTION +.sp +This command displays detailed information about the specified archive or repository. +.sp +Please note that the deduplicated sizes of the individual archives do not add +up to the deduplicated size of the repository ("all archives"), because the two +are meaning different things: +.INDENT 0.0 +.TP +.B This archive / deduplicated size = amount of data stored ONLY for this archive += unique chunks of this archive. +.TP +.B All archives / deduplicated size = amount of data stored in the repo += all chunks in the repository. +.UNINDENT +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B REPOSITORY_OR_ARCHIVE +archive or repository to display information about +.UNINDENT +.SS filters +.INDENT 0.0 +.TP +.B \-P\fP,\fB \-\-prefix +only consider archive names starting with this prefix +.TP +.B \-\-sort\-by +Comma\-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp +.TP +.BI \-\-first \ N +consider first N archives after other filters were applied +.TP +.BI \-\-last \ N +consider last N archives after other filters were applied +.UNINDENT +.SH EXAMPLES +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg info /path/to/repo::root\-2016\-02\-15 +Name: root\-2016\-02\-15 +Fingerprint: 57c827621f21b000a8d363c1e163cc55983822b3afff3a96df595077a660be50 +Hostname: myhostname +Username: root +Time (start): Mon, 2016\-02\-15 19:36:29 +Time (end): Mon, 2016\-02\-15 19:39:26 +Command line: /usr/local/bin/borg create \-\-list \-C zlib,6 /path/to/repo::root\-2016\-02\-15 / \-\-one\-file\-system +Number of files: 38100 + + Original size Compressed size Deduplicated size +This archive: 1.33 GB 613.25 MB 571.64 MB +All archives: 1.63 GB 853.66 MB 584.12 MB + + Unique chunks Total chunks +Chunk index: 36858 48844 +.ft P +.fi +.UNINDENT +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP, \fIborg\-list(1)\fP, \fIborg\-diff(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-init.1 b/docs/man/borg-init.1 new file mode 100644 index 00000000..40d8a25d --- /dev/null +++ b/docs/man/borg-init.1 @@ -0,0 +1,157 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-INIT 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-init \- Initialize an empty repository +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg init REPOSITORY +.SH DESCRIPTION +.sp +This command initializes an empty repository. A repository is a filesystem +directory containing the deduplicated data from zero or more archives. +.sp +Encryption can be enabled at repository init time. +.sp +It is not recommended to work without encryption. Repository encryption protects +you e.g. against the case that an attacker has access to your backup repository. +.sp +But be careful with the key / the passphrase: +.sp +If you want "passphrase\-only" security, use one of the repokey modes. The +key will be stored inside the repository (in its "config" file). In above +mentioned attack scenario, the attacker will have the key (but not the +passphrase). +.sp +If you want "passphrase and having\-the\-key" security, use one of the keyfile +modes. The key will be stored in your home directory (in .config/borg/keys). +In the attack scenario, the attacker who has just access to your repo won\(aqt +have the key (and also not the passphrase). +.sp +Make a backup copy of the key file (keyfile mode) or repo config file +(repokey mode) and keep it at a safe place, so you still have the key in +case it gets corrupted or lost. Also keep the passphrase at a safe place. +The backup that is encrypted with that key won\(aqt help you with that, of course. +.sp +Make sure you use a good passphrase. Not too short, not too simple. The real +encryption / decryption key is encrypted with / locked by your passphrase. +If an attacker gets your key, he can\(aqt unlock and use it without knowing the +passphrase. +.sp +Be careful with special or non\-ascii characters in your passphrase: +.INDENT 0.0 +.IP \(bu 2 +Borg processes the passphrase as unicode (and encodes it as utf\-8), +so it does not have problems dealing with even the strangest characters. +.IP \(bu 2 +BUT: that does not necessarily apply to your OS / VM / keyboard configuration. +.UNINDENT +.sp +So better use a long passphrase made from simple ascii chars than one that +includes non\-ascii stuff or characters that are hard/impossible to enter on +a different keyboard layout. +.sp +You can change your passphrase for existing repos at any time, it won\(aqt affect +the encryption/decryption key or other secrets. +.SS Encryption modes +.sp +repokey and keyfile use AES\-CTR\-256 for encryption and HMAC\-SHA256 for +authentication in an encrypt\-then\-MAC (EtM) construction. The chunk ID hash +is HMAC\-SHA256 as well (with a separate key). +These modes are compatible with borg 1.0.x. +.sp +repokey\-blake2 and keyfile\-blake2 are also authenticated encryption modes, +but use BLAKE2b\-256 instead of HMAC\-SHA256 for authentication. The chunk ID +hash is a keyed BLAKE2b\-256 hash. +These modes are new and not compatible with borg 1.0.x. +.sp +"authenticated" mode uses no encryption, but authenticates repository contents +through the same keyed BLAKE2b\-256 hash as the other blake2 modes (it uses it +as chunk ID hash). The key is stored like repokey. +This mode is new and not compatible with borg 1.0.x. +.sp +"none" mode uses no encryption and no authentication. It uses sha256 as chunk +ID hash. Not recommended, rather consider using an authenticated or +authenticated/encrypted mode. +This mode is compatible with borg 1.0.x. +.sp +Hardware acceleration will be used automatically. +.sp +On modern Intel/AMD CPUs (except very cheap ones), AES is usually hw +accelerated. BLAKE2b is faster than sha256 on Intel/AMD 64bit CPUs. +.sp +On modern ARM CPUs, NEON provides hw acceleration for sha256 making it faster +than BLAKE2b\-256 there. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B REPOSITORY +repository to create +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-e\fP,\fB \-\-encryption +select encryption key mode (default: "None") +.TP +.B \-a\fP,\fB \-\-append\-only +create an append\-only mode repository +.UNINDENT +.SH EXAMPLES +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# Local repository, repokey encryption, BLAKE2b (often faster, since Borg 1.1) +$ borg init \-\-encryption=repokey\-blake2 /path/to/repo + +# Local repository (no encryption) +$ borg init \-\-encryption=none /path/to/repo + +# Remote repository (accesses a remote borg via ssh) +$ borg init \-\-encryption=repokey\-blake2 user@hostname:backup + +# Remote repository (store the key your home dir) +$ borg init \-\-encryption=keyfile user@hostname:backup +.ft P +.fi +.UNINDENT +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP, \fIborg\-create(1)\fP, \fIborg\-delete(1)\fP, \fIborg\-check(1)\fP, \fIborg\-list(1)\fP, \fIborg\-key\-import(1)\fP, \fIborg\-key\-export(1)\fP, \fIborg\-key\-change\-passphrase(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-key-change-passphrase.1 b/docs/man/borg-key-change-passphrase.1 new file mode 100644 index 00000000..63a58b0c --- /dev/null +++ b/docs/man/borg-key-change-passphrase.1 @@ -0,0 +1,52 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-KEY-CHANGE-PASSPHRASE 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-key-change-passphrase \- Change repository key file passphrase +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg key change\-passphrase REPOSITORY +.SH DESCRIPTION +.sp +The key files used for repository encryption are optionally passphrase +protected. This command can be used to change this passphrase. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.sp +REPOSITORY +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-key-export.1 b/docs/man/borg-key-export.1 new file mode 100644 index 00000000..3a815814 --- /dev/null +++ b/docs/man/borg-key-export.1 @@ -0,0 +1,78 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-KEY-EXPORT 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-key-export \- Export the repository key for backup +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg key export REPOSITORY PATH +.SH DESCRIPTION +.sp +If repository encryption is used, the repository is inaccessible +without the key. This command allows to backup this essential key. +.sp +There are two backup formats. The normal backup format is suitable for +digital storage as a file. The \fB\-\-paper\fP backup format is optimized +for printing and typing in while importing, with per line checks to +reduce problems with manual input. +.sp +For repositories using keyfile encryption the key is saved locally +on the system that is capable of doing backups. To guard against loss +of this key, the key needs to be backed up independently of the main +data backup. +.sp +For repositories using the repokey encryption the key is saved in the +repository in the config file. A backup is thus not strictly needed, +but guards against the repository becoming inaccessible if the file +is damaged for some reason. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.sp +REPOSITORY +.INDENT 0.0 +.TP +.B PATH +where to store the backup +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-\-paper +Create an export suitable for printing and later type\-in +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP, \fIborg\-key\-import(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-key-import.1 b/docs/man/borg-key-import.1 new file mode 100644 index 00000000..7215df92 --- /dev/null +++ b/docs/man/borg-key-import.1 @@ -0,0 +1,67 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-KEY-IMPORT 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-key-import \- Import the repository key from backup +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg key import REPOSITORY PATH +.SH DESCRIPTION +.sp +This command allows to restore a key previously backed up with the +export command. +.sp +If the \fB\-\-paper\fP option is given, the import will be an interactive +process in which each line is checked for plausibility before +proceeding to the next line. For this format PATH must not be given. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.sp +REPOSITORY +.INDENT 0.0 +.TP +.B PATH +path to the backup +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-\-paper +interactively import from a backup done with \-\-paper +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP, \fIborg\-key\-export(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-key-migrate-to-repokey.1 b/docs/man/borg-key-migrate-to-repokey.1 new file mode 100644 index 00000000..c1c5e95f --- /dev/null +++ b/docs/man/borg-key-migrate-to-repokey.1 @@ -0,0 +1,66 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-KEY-MIGRATE-TO-REPOKEY 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-key-migrate-to-repokey \- Migrate passphrase -> repokey +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg key migrate\-to\-repokey REPOSITORY +.SH DESCRIPTION +.sp +This command migrates a repository from passphrase mode (removed in Borg 1.0) +to repokey mode. +.sp +You will be first asked for the repository passphrase (to open it in passphrase +mode). This is the same passphrase as you used to use for this repo before 1.0. +.sp +It will then derive the different secrets from this passphrase. +.sp +Then you will be asked for a new passphrase (twice, for safety). This +passphrase will be used to protect the repokey (which contains these same +secrets in encrypted form). You may use the same passphrase as you used to +use, but you may also use a different one. +.sp +After migrating to repokey mode, you can change the passphrase at any time. +But please note: the secrets will always stay the same and they could always +be derived from your (old) passphrase\-mode passphrase. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.sp +REPOSITORY +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-list.1 b/docs/man/borg-list.1 new file mode 100644 index 00000000..d6e5a4ec --- /dev/null +++ b/docs/man/borg-list.1 @@ -0,0 +1,245 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-LIST 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-list \- List archive or repository contents +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg list REPOSITORY_OR_ARCHIVE PATH +.SH DESCRIPTION +.sp +This command lists the contents of a repository or an archive. +.sp +See the "borg help patterns" command for more help on exclude patterns. +.sp +The following keys are available for \-\-format: +.INDENT 0.0 +.INDENT 3.5 +.INDENT 0.0 +.IP \(bu 2 +NEWLINE: OS dependent line separator +.IP \(bu 2 +NL: alias of NEWLINE +.IP \(bu 2 +NUL: NUL character for creating print0 / xargs \-0 like output, see barchive/bpath +.IP \(bu 2 +SPACE +.IP \(bu 2 +TAB +.IP \(bu 2 +CR +.IP \(bu 2 +LF +.UNINDENT +.UNINDENT +.UNINDENT +.sp +Keys for listing repository archives: +.INDENT 0.0 +.INDENT 3.5 +.INDENT 0.0 +.IP \(bu 2 +archive: archive name interpreted as text (might be missing non\-text characters, see barchive) +.IP \(bu 2 +barchive: verbatim archive name, can contain any character except NUL +.IP \(bu 2 +time: time of creation of the archive +.IP \(bu 2 +id: internal ID of the archive +.UNINDENT +.UNINDENT +.UNINDENT +.sp +Keys for listing archive files: +.INDENT 0.0 +.INDENT 3.5 +.INDENT 0.0 +.IP \(bu 2 +type +.IP \(bu 2 +mode +.IP \(bu 2 +uid +.IP \(bu 2 +gid +.IP \(bu 2 +user +.IP \(bu 2 +group +.IP \(bu 2 +path: path interpreted as text (might be missing non\-text characters, see bpath) +.IP \(bu 2 +bpath: verbatim POSIX path, can contain any character except NUL +.IP \(bu 2 +source: link target for links (identical to linktarget) +.IP \(bu 2 +linktarget +.IP \(bu 2 +flags +.IP \(bu 2 +size +.IP \(bu 2 +csize: compressed size +.IP \(bu 2 +num_chunks: number of chunks in this file +.IP \(bu 2 +unique_chunks: number of unique chunks in this file +.IP \(bu 2 +mtime +.IP \(bu 2 +ctime +.IP \(bu 2 +atime +.IP \(bu 2 +isomtime +.IP \(bu 2 +isoctime +.IP \(bu 2 +isoatime +.IP \(bu 2 +blake2b +.IP \(bu 2 +blake2s +.IP \(bu 2 +md5 +.IP \(bu 2 +sha1 +.IP \(bu 2 +sha224 +.IP \(bu 2 +sha256 +.IP \(bu 2 +sha384 +.IP \(bu 2 +sha3_224 +.IP \(bu 2 +sha3_256 +.IP \(bu 2 +sha3_384 +.IP \(bu 2 +sha3_512 +.IP \(bu 2 +sha512 +.IP \(bu 2 +shake_128 +.IP \(bu 2 +shake_256 +.IP \(bu 2 +archiveid +.IP \(bu 2 +archivename +.IP \(bu 2 +extra: prepends {source} with " \-> " for soft links and " link to " for hard links +.IP \(bu 2 +health: either "healthy" (file ok) or "broken" (if file has all\-zero replacement chunks) +.UNINDENT +.UNINDENT +.UNINDENT +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B REPOSITORY_OR_ARCHIVE +repository/archive to list contents of +.TP +.B PATH +paths to list; patterns are supported +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-\-short +only print file/directory names, nothing else +.TP +.B \-\-format\fP,\fB \-\-list\-format +specify format for file listing +(default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") +.TP +.BI \-e \ PATTERN\fP,\fB \ \-\-exclude \ PATTERN +exclude paths matching PATTERN +.TP +.BI \-\-exclude\-from \ EXCLUDEFILE +read exclude patterns from EXCLUDEFILE, one per line +.UNINDENT +.SS filters +.INDENT 0.0 +.TP +.B \-P\fP,\fB \-\-prefix +only consider archive names starting with this prefix +.TP +.B \-\-sort\-by +Comma\-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp +.TP +.BI \-\-first \ N +consider first N archives after other filters were applied +.TP +.BI \-\-last \ N +consider last N archives after other filters were applied +.UNINDENT +.SH EXAMPLES +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg list /path/to/repo +Monday Mon, 2016\-02\-15 19:15:11 +repo Mon, 2016\-02\-15 19:26:54 +root\-2016\-02\-15 Mon, 2016\-02\-15 19:36:29 +newname Mon, 2016\-02\-15 19:50:19 +\&... + +$ borg list /path/to/repo::root\-2016\-02\-15 +drwxr\-xr\-x root root 0 Mon, 2016\-02\-15 17:44:27 . +drwxrwxr\-x root root 0 Mon, 2016\-02\-15 19:04:49 bin +\-rwxr\-xr\-x root root 1029624 Thu, 2014\-11\-13 00:08:51 bin/bash +lrwxrwxrwx root root 0 Fri, 2015\-03\-27 20:24:26 bin/bzcmp \-> bzdiff +\-rwxr\-xr\-x root root 2140 Fri, 2015\-03\-27 20:24:22 bin/bzdiff +\&... + +$ borg list /path/to/repo::archiveA \-\-list\-format="{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}" +drwxrwxr\-x user user 0 Sun, 2015\-02\-01 11:00:00 . +drwxrwxr\-x user user 0 Sun, 2015\-02\-01 11:00:00 code +drwxrwxr\-x user user 0 Sun, 2015\-02\-01 11:00:00 code/myproject +\-rw\-rw\-r\-\- user user 1416192 Sun, 2015\-02\-01 11:00:00 code/myproject/file.ext +\&... +.ft P +.fi +.UNINDENT +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP, \fIborg\-info(1)\fP, \fIborg\-diff(1)\fP, \fIborg\-prune(1)\fP, \fIborg\-patterns(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-mount.1 b/docs/man/borg-mount.1 new file mode 100644 index 00000000..e788bdba --- /dev/null +++ b/docs/man/borg-mount.1 @@ -0,0 +1,115 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-MOUNT 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-mount \- Mount archive or an entire repository as a FUSE filesystem +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg mount REPOSITORY_OR_ARCHIVE MOUNTPOINT +.SH DESCRIPTION +.sp +This command mounts an archive as a FUSE filesystem. This can be useful for +browsing an archive or restoring individual files. Unless the \fB\-\-foreground\fP +option is given the command will run in the background until the filesystem +is \fBumounted\fP\&. +.sp +The command \fBborgfs\fP provides a wrapper for \fBborg mount\fP\&. This can also be +used in fstab entries: +\fB/path/to/repo /mnt/point fuse.borgfs defaults,noauto 0 0\fP +.sp +To allow a regular user to use fstab entries, add the \fBuser\fP option: +\fB/path/to/repo /mnt/point fuse.borgfs defaults,noauto,user 0 0\fP +.sp +For mount options, see the fuse(8) manual page. Additional mount options +supported by borg: +.INDENT 0.0 +.IP \(bu 2 +versions: when used with a repository mount, this gives a merged, versioned +view of the files in the archives. EXPERIMENTAL, layout may change in future. +.IP \(bu 2 +allow_damaged_files: by default damaged files (where missing chunks were +replaced with runs of zeros by borg check \-\-repair) are not readable and +return EIO (I/O error). Set this option to read such files. +.UNINDENT +.sp +The BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is meant for advanced users +to tweak the performance. It sets the number of cached data chunks; additional +memory usage can be up to ~8 MiB times this number. The default is the number +of CPU cores. +.sp +When the daemonized process receives a signal or crashes, it does not unmount. +Unmounting in these cases could cause an active rsync or similar process +to unintentionally delete data. +.sp +When running in the foreground ^C/SIGINT unmounts cleanly, but other +signals or crashes do not. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B REPOSITORY_OR_ARCHIVE +repository/archive to mount +.TP +.B MOUNTPOINT +where to mount filesystem +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-f\fP,\fB \-\-foreground +stay in foreground, do not daemonize +.TP +.B \-o +Extra mount options +.UNINDENT +.SS filters +.INDENT 0.0 +.TP +.B \-P\fP,\fB \-\-prefix +only consider archive names starting with this prefix +.TP +.B \-\-sort\-by +Comma\-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp +.TP +.BI \-\-first \ N +consider first N archives after other filters were applied +.TP +.BI \-\-last \ N +consider last N archives after other filters were applied +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP, \fIborg\-umount(1)\fP, \fIborg\-extract(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-patterns.1 b/docs/man/borg-patterns.1 new file mode 100644 index 00000000..f814edc3 --- /dev/null +++ b/docs/man/borg-patterns.1 @@ -0,0 +1,144 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-PATTERNS 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-patterns \- Details regarding patterns +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH DESCRIPTION +.sp +Exclusion patterns support four separate styles, fnmatch, shell, regular +expressions and path prefixes. By default, fnmatch is used. If followed +by a colon (\(aq:\(aq) the first two characters of a pattern are used as a +style selector. Explicit style selection is necessary when a +non\-default style is desired or when the desired pattern starts with +two alphanumeric characters followed by a colon (i.e. \fIaa:something/*\fP). +.sp +\fI\%Fnmatch\fP, selector \fIfm:\fP +.INDENT 0.0 +.INDENT 3.5 +This is the default style. These patterns use a variant of shell +pattern syntax, with \(aq*\(aq matching any number of characters, \(aq?\(aq +matching any single character, \(aq[...]\(aq matching any single +character specified, including ranges, and \(aq[!...]\(aq matching any +character not specified. For the purpose of these patterns, the +path separator (\(aq\(aq for Windows and \(aq/\(aq on other systems) is not +treated specially. Wrap meta\-characters in brackets for a literal +match (i.e. \fI[?]\fP to match the literal character \fI?\fP). For a path +to match a pattern, it must completely match from start to end, or +must match from the start to just before a path separator. Except +for the root path, paths will never end in the path separator when +matching is attempted. Thus, if a given pattern ends in a path +separator, a \(aq*\(aq is appended before matching is attempted. +.UNINDENT +.UNINDENT +.sp +Shell\-style patterns, selector \fIsh:\fP +.INDENT 0.0 +.INDENT 3.5 +Like fnmatch patterns these are similar to shell patterns. The difference +is that the pattern may include \fI**/\fP for matching zero or more directory +levels, \fI*\fP for matching zero or more arbitrary characters with the +exception of any path separator. +.UNINDENT +.UNINDENT +.sp +Regular expressions, selector \fIre:\fP +.INDENT 0.0 +.INDENT 3.5 +Regular expressions similar to those found in Perl are supported. Unlike +shell patterns regular expressions are not required to match the complete +path and any substring match is sufficient. It is strongly recommended to +anchor patterns to the start (\(aq^\(aq), to the end (\(aq$\(aq) or both. Path +separators (\(aq\(aq for Windows and \(aq/\(aq on other systems) in paths are +always normalized to a forward slash (\(aq/\(aq) before applying a pattern. The +regular expression syntax is described in the \fI\%Python documentation for +the re module\fP\&. +.UNINDENT +.UNINDENT +.sp +Prefix path, selector \fIpp:\fP +.INDENT 0.0 +.INDENT 3.5 +This pattern style is useful to match whole sub\-directories. The pattern +\fIpp:/data/bar\fP matches \fI/data/bar\fP and everything therein. +.UNINDENT +.UNINDENT +.sp +Exclusions can be passed via the command line option \fI\-\-exclude\fP\&. When used +from within a shell the patterns should be quoted to protect them from +expansion. +.sp +The \fI\-\-exclude\-from\fP option permits loading exclusion patterns from a text +file with one pattern per line. Lines empty or starting with the number sign +(\(aq#\(aq) after removing whitespace on both ends are ignored. The optional style +selector prefix is also supported for patterns loaded from a file. Due to +whitespace removal paths with whitespace at the beginning or end can only be +excluded using regular expressions. +.sp +Examples: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# Exclude \(aq/home/user/file.o\(aq but not \(aq/home/user/file.odt\(aq: +$ borg create \-e \(aq*.o\(aq backup / + +# Exclude \(aq/home/user/junk\(aq and \(aq/home/user/subdir/junk\(aq but +# not \(aq/home/user/importantjunk\(aq or \(aq/etc/junk\(aq: +$ borg create \-e \(aq/home/*/junk\(aq backup / + +# Exclude the contents of \(aq/home/user/cache\(aq but not the directory itself: +$ borg create \-e /home/user/cache/ backup / + +# The file \(aq/home/user/cache/important\(aq is *not* backed up: +$ borg create \-e /home/user/cache/ backup / /home/user/cache/important + +# The contents of directories in \(aq/home\(aq are not backed up when their name +# ends in \(aq.tmp\(aq +$ borg create \-\-exclude \(aqre:^/home/[^/]+\e.tmp/\(aq backup / + +# Load exclusions from file +$ cat >exclude.txt < REPOSITORY +.SH DESCRIPTION +.sp +The prune command prunes a repository by deleting all archives not matching +any of the specified retention options. This command is normally used by +automated backup scripts wanting to keep a certain number of historic backups. +.sp +Also, prune automatically removes checkpoint archives (incomplete archives left +behind by interrupted backup runs) except if the checkpoint is the latest +archive (and thus still needed). Checkpoint archives are not considered when +comparing archive counts against the retention limits (\-\-keep\-X). +.sp +If a prefix is set with \-P, then only archives that start with the prefix are +considered for deletion and only those archives count towards the totals +specified by the rules. +Otherwise, \fIall\fP archives in the repository are candidates for deletion! +There is no automatic distinction between archives representing different +contents. These need to be distinguished by specifying matching prefixes. +.sp +If you have multiple sequences of archives with different data sets (e.g. +from different machines) in one shared repository, use one prune call per +data set that matches only the respective archives using the \-P option. +.sp +The "\-\-keep\-within" option takes an argument of the form "", +where char is "H", "d", "w", "m", "y". For example, "\-\-keep\-within 2d" means +to keep all archives that were created within the past 48 hours. +"1m" is taken to mean "31d". The archives kept with this option do not +count towards the totals specified by any other options. +.sp +A good procedure is to thin out more and more the older your backups get. +As an example, "\-\-keep\-daily 7" means to keep the latest backup on each day, +up to 7 most recent days with backups (days without backups do not count). +The rules are applied from secondly to yearly, and backups selected by previous +rules do not count towards those of later rules. The time that each backup +starts is used for pruning purposes. Dates and times are interpreted in +the local timezone, and weeks go from Monday to Sunday. Specifying a +negative number of archives to keep means that there is no limit. +.sp +The "\-\-keep\-last N" option is doing the same as "\-\-keep\-secondly N" (and it will +keep the last N archives under the assumption that you do not create more than one +backup archive in the same second). +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B REPOSITORY +repository to prune +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-n\fP,\fB \-\-dry\-run +do not change repository +.TP +.B \-\-force +force pruning of corrupted archives +.TP +.B \-p\fP,\fB \-\-progress +show progress display while deleting archives +.TP +.B \-s\fP,\fB \-\-stats +print statistics for the deleted archive +.TP +.B \-\-list +output verbose list of archives it keeps/prunes +.TP +.BI \-\-keep\-within \ WITHIN +keep all archives within this time interval +.TP +.B \-\-keep\-last\fP,\fB \-\-keep\-secondly +number of secondly archives to keep +.TP +.B \-\-keep\-minutely +number of minutely archives to keep +.TP +.B \-H\fP,\fB \-\-keep\-hourly +number of hourly archives to keep +.TP +.B \-d\fP,\fB \-\-keep\-daily +number of daily archives to keep +.TP +.B \-w\fP,\fB \-\-keep\-weekly +number of weekly archives to keep +.TP +.B \-m\fP,\fB \-\-keep\-monthly +number of monthly archives to keep +.TP +.B \-y\fP,\fB \-\-keep\-yearly +number of yearly archives to keep +.TP +.B \-P\fP,\fB \-\-prefix +only consider archive names starting with this prefix +.TP +.B \-\-save\-space +work slower, but using less space +.UNINDENT +.SH EXAMPLES +.sp +Be careful, prune is a potentially dangerous command, it will remove backup +archives. +.sp +The default of prune is to apply to \fBall archives in the repository\fP unless +you restrict its operation to a subset of the archives using \fB\-\-prefix\fP\&. +When using \fB\-\-prefix\fP, be careful to choose a good prefix \- e.g. do not use a +prefix "foo" if you do not also want to match "foobar". +.sp +It is strongly recommended to always run \fBprune \-v \-\-list \-\-dry\-run ...\fP +first so you will see what it would do without it actually doing anything. +.sp +There is also a visualized prune example in \fBdocs/misc/prune\-example.txt\fP\&. +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# Keep 7 end of day and 4 additional end of week archives. +# Do a dry\-run without actually deleting anything. +$ borg prune \-v \-\-list \-\-dry\-run \-\-keep\-daily=7 \-\-keep\-weekly=4 /path/to/repo + +# Same as above but only apply to archive names starting with the hostname +# of the machine followed by a "\-" character: +$ borg prune \-v \-\-list \-\-keep\-daily=7 \-\-keep\-weekly=4 \-\-prefix=\(aq{hostname}\-\(aq /path/to/repo + +# Keep 7 end of day, 4 additional end of week archives, +# and an end of month archive for every month: +$ borg prune \-v \-\-list \-\-keep\-daily=7 \-\-keep\-weekly=4 \-\-keep\-monthly=\-1 /path/to/repo + +# Keep all backups in the last 10 days, 4 additional end of week archives, +# and an end of month archive for every month: +$ borg prune \-v \-\-list \-\-keep\-within=10d \-\-keep\-weekly=4 \-\-keep\-monthly=\-1 /path/to/repo +.ft P +.fi +.UNINDENT +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-recreate.1 b/docs/man/borg-recreate.1 new file mode 100644 index 00000000..55b01cc8 --- /dev/null +++ b/docs/man/borg-recreate.1 @@ -0,0 +1,192 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-RECREATE 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-recreate \- Re-create archives +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg recreate REPOSITORY_OR_ARCHIVE PATH +.SH DESCRIPTION +.sp +Recreate the contents of existing archives. +.sp +This is an \fIexperimental\fP feature. Do \fInot\fP use this on your only backup. +.sp +\-\-exclude, \-\-exclude\-from and PATH have the exact same semantics +as in "borg create". If PATHs are specified the resulting archive +will only contain files from these PATHs. +.sp +Note that all paths in an archive are relative, therefore absolute patterns/paths +will \fInot\fP match (\-\-exclude, \-\-exclude\-from, \-\-compression\-from, PATHs). +.sp +\-\-compression: all chunks seen will be stored using the given method. +Due to how Borg stores compressed size information this might display +incorrect information for archives that were not recreated at the same time. +There is no risk of data loss by this. +.sp +\-\-chunker\-params will re\-chunk all files in the archive, this can be +used to have upgraded Borg 0.xx or Attic archives deduplicate with +Borg 1.x archives. +.sp +USE WITH CAUTION. +Depending on the PATHs and patterns given, recreate can be used to permanently +delete files from archives. +When in doubt, use "\-\-dry\-run \-\-verbose \-\-list" to see how patterns/PATHS are +interpreted. +.sp +The archive being recreated is only removed after the operation completes. The +archive that is built during the operation exists at the same time at +".recreate". The new archive will have a different archive ID. +.sp +With \-\-target the original archive is not replaced, instead a new archive is created. +.sp +When rechunking space usage can be substantial, expect at least the entire +deduplicated size of the archives using the previous chunker params. +When recompressing expect approx. (throughput / checkpoint\-interval) in space usage, +assuming all chunks are recompressed. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B REPOSITORY_OR_ARCHIVE +repository/archive to recreate +.TP +.B PATH +paths to recreate; patterns are supported +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-\-list +output verbose list of items (files, dirs, ...) +.TP +.BI \-\-filter \ STATUSCHARS +only display items with the given status characters +.TP +.B \-p\fP,\fB \-\-progress +show progress display while recreating archives +.TP +.B \-n\fP,\fB \-\-dry\-run +do not change anything +.TP +.B \-s\fP,\fB \-\-stats +print statistics at end +.UNINDENT +.SS Exclusion options +.INDENT 0.0 +.TP +.BI \-e \ PATTERN\fP,\fB \ \-\-exclude \ PATTERN +exclude paths matching PATTERN +.TP +.BI \-\-exclude\-from \ EXCLUDEFILE +read exclude patterns from EXCLUDEFILE, one per line +.TP +.B \-\-exclude\-caches +exclude directories that contain a CACHEDIR.TAG file (\fI\%http://www.brynosaurus.com/cachedir/spec.html\fP) +.TP +.BI \-\-exclude\-if\-present \ NAME +exclude directories that are tagged by containing a filesystem object with the given NAME +.TP +.B \-\-keep\-exclude\-tags\fP,\fB \-\-keep\-tag\-files +keep tag objects (i.e.: arguments to \-\-exclude\-if\-present) in otherwise excluded caches/directories +.UNINDENT +.SS Archive options +.INDENT 0.0 +.TP +.BI \-\-target \ TARGET +create a new archive with the name ARCHIVE, do not replace existing archive (only applies for a single archive) +.TP +.BI \-c \ SECONDS\fP,\fB \ \-\-checkpoint\-interval \ SECONDS +write checkpoint every SECONDS seconds (Default: 1800) +.TP +.BI \-\-comment \ COMMENT +add a comment text to the archive +.TP +.BI \-\-timestamp \ TIMESTAMP +manually specify the archive creation date/time (UTC, yyyy\-mm\-ddThh:mm:ss format). alternatively, give a reference file/directory. +.TP +.BI \-C \ COMPRESSION\fP,\fB \ \-\-compression \ COMPRESSION +select compression algorithm, see the output of the "borg help compression" command for details. +.TP +.B \-\-always\-recompress +always recompress chunks, don\(aqt skip chunks already compressed with the same algorithm. +.TP +.BI \-\-compression\-from \ COMPRESSIONCONFIG +read compression patterns from COMPRESSIONCONFIG, see the output of the "borg help compression" command for details. +.TP +.BI \-\-chunker\-params \ PARAMS +specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or "default" to use the current defaults. default: 19,23,21,4095 +.UNINDENT +.SH EXAMPLES +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# Make old (Attic / Borg 0.xx) archives deduplicate with Borg 1.x archives +# Archives created with Borg 1.1+ and the default chunker params are skipped (archive ID stays the same) +$ borg recreate /mnt/backup \-\-chunker\-params default \-\-progress + +# Create a backup with little but fast compression +$ borg create /mnt/backup::archive /some/files \-\-compression lz4 +# Then compress it \- this might take longer, but the backup has already completed, so no inconsistencies +# from a long\-running backup job. +$ borg recreate /mnt/backup::archive \-\-compression zlib,9 + +# Remove unwanted files from all archives in a repository +$ borg recreate /mnt/backup \-e /home/icke/Pictures/drunk_photos + + +# Change archive comment +$ borg create \-\-comment "This is a comment" /mnt/backup::archivename ~ +$ borg info /mnt/backup::archivename +Name: archivename +Fingerprint: ... +Comment: This is a comment +\&... +$ borg recreate \-\-comment "This is a better comment" /mnt/backup::archivename +$ borg info /mnt/backup::archivename +Name: archivename +Fingerprint: ... +Comment: This is a better comment +\&... +.ft P +.fi +.UNINDENT +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP, \fIborg\-patterns(1)\fP, \fIborg\-placeholders(1)\fP, \fIborg\-compression(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-rename.1 b/docs/man/borg-rename.1 new file mode 100644 index 00000000..ee568f2f --- /dev/null +++ b/docs/man/borg-rename.1 @@ -0,0 +1,76 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-RENAME 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-rename \- Rename an existing archive +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg rename ARCHIVE NEWNAME +.SH DESCRIPTION +.sp +This command renames an archive in the repository. +.sp +This results in a different archive ID. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B ARCHIVE +archive to rename +.TP +.B NEWNAME +the new archive name to use +.UNINDENT +.SH EXAMPLES +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg create /path/to/repo::archivename ~ +$ borg list /path/to/repo +archivename Mon, 2016\-02\-15 19:50:19 + +$ borg rename /path/to/repo::archivename newname +$ borg list /path/to/repo +newname Mon, 2016\-02\-15 19:50:19 +.ft P +.fi +.UNINDENT +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-serve.1 b/docs/man/borg-serve.1 new file mode 100644 index 00000000..673ae269 --- /dev/null +++ b/docs/man/borg-serve.1 @@ -0,0 +1,80 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-SERVE 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-serve \- Start in server mode. This command is usually not used manually. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg serve +.SH DESCRIPTION +.sp +This command starts a repository server process. This command is usually not used manually. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS optional arguments +.INDENT 0.0 +.TP +.BI \-\-restrict\-to\-path \ PATH +restrict repository access to PATH. Can be specified multiple times to allow the client access to several directories. Access to all sub\-directories is granted implicitly; PATH doesn\(aqt need to directly point to a repository. +.TP +.B \-\-append\-only +only allow appending to repository segment files +.UNINDENT +.SH EXAMPLES +.sp +borg serve has special support for ssh forced commands (see \fBauthorized_keys\fP +example below): it will detect that you use such a forced command and extract +the value of the \fB\-\-restrict\-to\-path\fP option(s). +It will then parse the original command that came from the client, makes sure +that it is also \fBborg serve\fP and enforce path restriction(s) as given by the +forced command. That way, other options given by the client (like \fB\-\-info\fP or +\fB\-\-umask\fP) are preserved (and are not fixed by the forced command). +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# Allow an SSH keypair to only run borg, and only have access to /path/to/repo. +# Use key options to disable unneeded and potentially dangerous SSH functionality. +# This will help to secure an automated remote backup system. +$ cat ~/.ssh/authorized_keys +command="borg serve \-\-restrict\-to\-path /path/to/repo",no\-pty,no\-agent\-forwarding,no\-port\-forwarding,no\-X11\-forwarding,no\-user\-rc ssh\-rsa AAAAB3[...] +.ft P +.fi +.UNINDENT +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-umount.1 b/docs/man/borg-umount.1 new file mode 100644 index 00000000..4de127a5 --- /dev/null +++ b/docs/man/borg-umount.1 @@ -0,0 +1,115 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-UMOUNT 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-umount \- un-mount the FUSE filesystem +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg umount MOUNTPOINT +.SH DESCRIPTION +.sp +This command un\-mounts a FUSE filesystem that was mounted with \fBborg mount\fP\&. +.sp +This is a convenience wrapper that just calls the platform\-specific shell +command \- usually this is either umount or fusermount \-u. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B MOUNTPOINT +mountpoint of the filesystem to umount +.UNINDENT +.SH EXAMPLES +.SS borg mount +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg mount /path/to/repo::root\-2016\-02\-15 /tmp/mymountpoint +$ ls /tmp/mymountpoint +bin boot etc home lib lib64 lost+found media mnt opt root sbin srv tmp usr var +$ borg umount /tmp/mymountpoint +.ft P +.fi +.UNINDENT +.UNINDENT +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg mount \-o versions /path/to/repo /tmp/mymountpoint +$ ls \-l /tmp/mymountpoint/home/user/doc.txt/ +total 24 +\-rw\-rw\-r\-\- 1 user group 12357 Aug 26 21:19 doc.txt.cda00bc9 +\-rw\-rw\-r\-\- 1 user group 12204 Aug 26 21:04 doc.txt.fa760f28 +$ fusermount \-u /tmp/mymountpoint +.ft P +.fi +.UNINDENT +.UNINDENT +.SS borgfs +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ echo \(aq/mnt/backup /tmp/myrepo fuse.borgfs defaults,noauto 0 0\(aq >> /etc/fstab +$ echo \(aq/mnt/backup::root\-2016\-02\-15 /tmp/myarchive fuse.borgfs defaults,noauto 0 0\(aq >> /etc/fstab +$ mount /tmp/myrepo +$ mount /tmp/myarchive +$ ls /tmp/myrepo +root\-2016\-02\-01 root\-2016\-02\-2015 +$ ls /tmp/myarchive +bin boot etc home lib lib64 lost+found media mnt opt root sbin srv tmp usr var +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +\fBNOTE:\fP +.INDENT 0.0 +.INDENT 3.5 +\fBborgfs\fP will be automatically provided if you used a distribution +package, \fBpip\fP or \fBsetup.py\fP to install Borg\&. Users of the +standalone binary will have to manually create a symlink (see +\fIpyinstaller\-binary\fP). +.UNINDENT +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP, \fIborg\-mount(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-upgrade.1 b/docs/man/borg-upgrade.1 new file mode 100644 index 00000000..d329fa88 --- /dev/null +++ b/docs/man/borg-upgrade.1 @@ -0,0 +1,170 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-UPGRADE 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-upgrade \- upgrade a repository from a previous version +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg upgrade REPOSITORY +.SH DESCRIPTION +.sp +Upgrade an existing Borg repository. +.SS Borg 1.x.y upgrades +.sp +Use \fBborg upgrade \-\-tam REPO\fP to require manifest authentication +introduced with Borg 1.0.9 to address security issues. This means +that modifying the repository after doing this with a version prior +to 1.0.9 will raise a validation error, so only perform this upgrade +after updating all clients using the repository to 1.0.9 or newer. +.sp +This upgrade should be done on each client for safety reasons. +.sp +If a repository is accidentally modified with a pre\-1.0.9 client after +this upgrade, use \fBborg upgrade \-\-tam \-\-force REPO\fP to remedy it. +.sp +If you routinely do this you might not want to enable this upgrade +(which will leave you exposed to the security issue). You can +reverse the upgrade by issuing \fBborg upgrade \-\-disable\-tam REPO\fP\&. +.sp +See +\fI\%https://borgbackup.readthedocs.io/en/stable/changes.html#pre\-1\-0\-9\-manifest\-spoofing\-vulnerability\fP +for details. +.SS Attic and Borg 0.xx to Borg 1.x +.sp +This currently supports converting an Attic repository to Borg and also +helps with converting Borg 0.xx to 1.0. +.sp +Currently, only LOCAL repositories can be upgraded (issue #465). +.sp +It will change the magic strings in the repository\(aqs segments +to match the new Borg magic strings. The keyfiles found in +$ATTIC_KEYS_DIR or ~/.attic/keys/ will also be converted and +copied to $BORG_KEYS_DIR or ~/.config/borg/keys. +.sp +The cache files are converted, from $ATTIC_CACHE_DIR or +~/.cache/attic to $BORG_CACHE_DIR or ~/.cache/borg, but the +cache layout between Borg and Attic changed, so it is possible +the first backup after the conversion takes longer than expected +due to the cache resync. +.sp +Upgrade should be able to resume if interrupted, although it +will still iterate over all segments. If you want to start +from scratch, use \fIborg delete\fP over the copied repository to +make sure the cache files are also removed: +.INDENT 0.0 +.INDENT 3.5 +borg delete borg +.UNINDENT +.UNINDENT +.sp +Unless \fB\-\-inplace\fP is specified, the upgrade process first +creates a backup copy of the repository, in +REPOSITORY.upgrade\-DATETIME, using hardlinks. This takes +longer than in place upgrades, but is much safer and gives +progress information (as opposed to \fBcp \-al\fP). Once you are +satisfied with the conversion, you can safely destroy the +backup copy. +.sp +WARNING: Running the upgrade in place will make the current +copy unusable with older version, with no way of going back +to previous versions. This can PERMANENTLY DAMAGE YOUR +REPOSITORY! Attic CAN NOT READ BORG REPOSITORIES, as the +magic strings have changed. You have been warned. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B REPOSITORY +path to the repository to be upgraded +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-p\fP,\fB \-\-progress +show progress display while upgrading the repository +.TP +.B \-n\fP,\fB \-\-dry\-run +do not change repository +.TP +.B \-i\fP,\fB \-\-inplace +rewrite repository in place, with no chance of going back to older +versions of the repository. +.TP +.B \-\-force +Force upgrade +.TP +.B \-\-tam +Enable manifest authentication (in key and cache) (Borg 1.0.9 and later) +.TP +.B \-\-disable\-tam +Disable manifest authentication (in key and cache) +.UNINDENT +.SH EXAMPLES +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# Upgrade the borg repository to the most recent version. +$ borg upgrade \-v /path/to/repo +making a hardlink copy in /path/to/repo.upgrade\-2016\-02\-15\-20:51:55 +opening attic repository with borg and converting +no key file found for repository +converting repo index /path/to/repo/index.0 +converting 1 segments... +converting borg 0.xx to borg current +no key file found for repository +.ft P +.fi +.UNINDENT +.UNINDENT +.SS Upgrading a passphrase encrypted attic repo +.sp +attic offered a "passphrase" encryption mode, but this was removed in borg 1.0 +and replaced by the "repokey" mode (which stores the passphrase\-protected +encryption key into the repository config). +.sp +Thus, to upgrade a "passphrase" attic repo to a "repokey" borg repo, 2 steps +are needed, in this order: +.INDENT 0.0 +.IP \(bu 2 +borg upgrade repo +.IP \(bu 2 +borg key migrate\-to\-repokey repo +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-with-lock.1 b/docs/man/borg-with-lock.1 new file mode 100644 index 00000000..d6e19958 --- /dev/null +++ b/docs/man/borg-with-lock.1 @@ -0,0 +1,71 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-WITH-LOCK 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg-with-lock \- run a user specified command with the repository lock held +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg with\-lock REPOSITORY COMMAND ARGS +.SH DESCRIPTION +.sp +This command runs a user\-specified command while the repository lock is held. +.sp +It will first try to acquire the lock (make sure that no other operation is +running in the repo), then execute the given command as a subprocess and wait +for its termination, release the lock and return the user command\(aqs return +code as borg\(aqs return code. +.INDENT 0.0 +.TP +.B Note: if you copy a repository with the lock held, the lock will be present in +the copy, obviously. Thus, before using borg on the copy, you need to +use "borg break\-lock" on it. +.UNINDENT +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B REPOSITORY +repository to lock +.TP +.B COMMAND +command to run +.TP +.B ARGS +command arguments +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. From e8335dba0f007060b7cdf92ca27b6e1c9f72d6d0 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 29 Jan 2017 21:31:35 +0100 Subject: [PATCH 0607/1387] archiver: Add 'debug dump-manifest' and 'debug dump-archive' commands. --- src/borg/archiver.py | 104 ++++++++++++++++++++++++++++++++- src/borg/helpers.py | 44 ++++++++++++++ src/borg/testsuite/archiver.py | 30 ++++++++++ 3 files changed, 177 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 61f26a56..b5fb165c 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -4,6 +4,7 @@ import faulthandler import functools import hashlib import inspect +import json import logging import os import re @@ -22,6 +23,8 @@ from itertools import zip_longest from .logger import create_logger, setup_logging logger = create_logger() +import msgpack + from . import __version__ from . import helpers from .archive import Archive, ArchiveChecker, ArchiveRecreater, Statistics, is_special @@ -34,11 +37,12 @@ from .helpers import Error, NoManifestError from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec from .helpers import PrefixSpec, SortBySpec, HUMAN_SORT_KEYS from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter, format_time, format_file_size, format_archive -from .helpers import safe_encode, remove_surrogates, bin_to_hex +from .helpers import safe_encode, remove_surrogates, bin_to_hex, prepare_dump_dict from .helpers import prune_within, prune_split from .helpers import to_localtime, timestamp from .helpers import get_cache_dir from .helpers import Manifest +from .helpers import StableDict from .helpers import update_excludes, check_extension_modules from .helpers import dir_is_tagged, is_slow_msgpack, yes, sysinfo from .helpers import log_multi @@ -1226,6 +1230,74 @@ class Archiver: print('Done.') return EXIT_SUCCESS + @with_repository() + def do_debug_dump_archive(self, args, repository, manifest, key): + """dump decoded archive metadata (not: data)""" + + try: + archive_meta_orig = manifest.archives.get_raw_dict()[safe_encode(args.location.archive)] + except KeyError: + raise Archive.DoesNotExist(args.location.archive) + + indent = 4 + + def do_indent(d): + return textwrap.indent(json.dumps(d, indent=indent), prefix=' ' * indent) + + def output(fd): + # this outputs megabytes of data for a modest sized archive, so some manual streaming json output + fd.write('{\n') + fd.write(' "_name": ' + json.dumps(args.location.archive) + ",\n") + fd.write(' "_manifest_entry":\n') + fd.write(do_indent(prepare_dump_dict(archive_meta_orig))) + fd.write(',\n') + + _, data = key.decrypt(archive_meta_orig[b'id'], repository.get(archive_meta_orig[b'id'])) + archive_org_dict = msgpack.unpackb(data, object_hook=StableDict, unicode_errors='surrogateescape') + + fd.write(' "_meta":\n') + fd.write(do_indent(prepare_dump_dict(archive_org_dict))) + fd.write(',\n') + fd.write(' "_items": [\n') + + unpacker = msgpack.Unpacker(use_list=False, object_hook=StableDict) + first = True + for item_id in archive_org_dict[b'items']: + _, data = key.decrypt(item_id, repository.get(item_id)) + unpacker.feed(data) + for item in unpacker: + item = prepare_dump_dict(item) + if first: + first = False + else: + fd.write(',\n') + fd.write(do_indent(item)) + + fd.write('\n') + fd.write(' ]\n}\n') + + if args.path == '-': + output(sys.stdout) + else: + with open(args.path, 'w') as fd: + output(fd) + return EXIT_SUCCESS + + @with_repository() + def do_debug_dump_manifest(self, args, repository, manifest, key): + """dump decoded repository manifest""" + + _, data = key.decrypt(None, repository.get(manifest.MANIFEST_ID)) + + meta = prepare_dump_dict(msgpack.fallback.unpackb(data, object_hook=StableDict, unicode_errors='surrogateescape')) + + if args.path == '-': + json.dump(meta, sys.stdout, indent=4) + else: + with open(args.path, 'w') as fd: + json.dump(meta, fd, indent=4) + return EXIT_SUCCESS + @with_repository() def do_debug_dump_repo_objs(self, args, repository, manifest, key): """dump (decrypted, decompressed) repo objects""" @@ -2716,6 +2788,36 @@ class Archiver: type=location_validator(archive=True), help='archive to dump') + debug_dump_archive_epilog = textwrap.dedent(""" + This command dumps all metadata of an archive in a decoded form to a file. + """) + subparser = debug_parsers.add_parser('dump-archive', parents=[common_parser], add_help=False, + description=self.do_debug_dump_archive.__doc__, + epilog=debug_dump_archive_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='dump decoded archive metadata (debug)') + subparser.set_defaults(func=self.do_debug_dump_archive) + subparser.add_argument('location', metavar='ARCHIVE', + type=location_validator(archive=True), + help='archive to dump') + subparser.add_argument('path', metavar='PATH', type=str, + help='file to dump data into') + + debug_dump_manifest_epilog = textwrap.dedent(""" + This command dumps manifest metadata of a repository in a decoded form to a file. + """) + subparser = debug_parsers.add_parser('dump-manifest', parents=[common_parser], add_help=False, + description=self.do_debug_dump_manifest.__doc__, + epilog=debug_dump_manifest_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='dump decoded repository metadata (debug)') + subparser.set_defaults(func=self.do_debug_dump_manifest) + subparser.add_argument('location', metavar='REPOSITORY', + type=location_validator(archive=False), + help='repository to dump') + subparser.add_argument('path', metavar='PATH', type=str, + help='file to dump data into') + debug_dump_repo_objs_epilog = textwrap.dedent(""" This command dumps raw (but decrypted and decompressed) repo objects to files. """) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 19293f15..df2a136f 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1,5 +1,6 @@ import argparse import contextlib +import collections import grp import hashlib import logging @@ -1093,6 +1094,49 @@ def decode_dict(d, keys, encoding='utf-8', errors='surrogateescape'): return d +def prepare_dump_dict(d): + def decode_bytes(value): + # this should somehow be reversable later, but usual strings should + # look nice and chunk ids should mostly show in hex. Use a special + # inband signaling character (ASCII DEL) to distinguish between + # decoded and hex mode. + if not value.startswith(b'\x7f'): + try: + value = value.decode() + return value + except UnicodeDecodeError: + pass + return '\u007f' + bin_to_hex(value) + + def decode_tuple(t): + res = [] + for value in t: + if isinstance(value, dict): + value = decode(value) + elif isinstance(value, tuple) or isinstance(value, list): + value = decode_tuple(value) + elif isinstance(value, bytes): + value = decode_bytes(value) + res.append(value) + return res + + def decode(d): + res = collections.OrderedDict() + for key, value in d.items(): + if isinstance(value, dict): + value = decode(value) + elif isinstance(value, (tuple, list)): + value = decode_tuple(value) + elif isinstance(value, bytes): + value = decode_bytes(value) + if isinstance(key, bytes): + key = key.decode() + res[key] = value + return res + + return decode(d) + + def remove_surrogates(s, errors='replace'): """Replace surrogates generated by fsdecode with '?' """ diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index f5b1fe8c..a9ad8ecf 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -3,6 +3,7 @@ from configparser import ConfigParser import errno import os import inspect +import json from datetime import datetime from datetime import timedelta from io import StringIO @@ -2020,6 +2021,35 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 2: 737475 - 88 """ + def test_debug_dump_manifest(self): + self.create_regular_file('file1', size=1024 * 80) + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + dump_file = self.output_path + '/dump' + output = self.cmd('debug', 'dump-manifest', self.repository_location, dump_file) + assert output == "" + with open(dump_file, "r") as f: + result = json.load(f) + assert 'archives' in result + assert 'config' in result + assert 'item_keys' in result + assert 'timestamp' in result + assert 'version' in result + + def test_debug_dump_archive(self): + self.create_regular_file('file1', size=1024 * 80) + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + dump_file = self.output_path + '/dump' + output = self.cmd('debug', 'dump-archive', self.repository_location + "::test", dump_file) + assert output == "" + with open(dump_file, "r") as f: + result = json.load(f) + assert '_name' in result + assert '_manifest_entry' in result + assert '_meta' in result + assert '_items' in result + @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available') class ArchiverTestCaseBinary(ArchiverTestCase): From 497da8df0469c6669660a9fd6f5a34bf24412999 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 5 Feb 2017 15:53:32 +0100 Subject: [PATCH 0608/1387] docs: init: fix markup/typos --- src/borg/archiver.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index b5fb165c..0746cbdc 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1765,32 +1765,33 @@ class Archiver: Encryption modes ++++++++++++++++ - repokey and keyfile use AES-CTR-256 for encryption and HMAC-SHA256 for + `repokey` and `keyfile` use AES-CTR-256 for encryption and HMAC-SHA256 for authentication in an encrypt-then-MAC (EtM) construction. The chunk ID hash is HMAC-SHA256 as well (with a separate key). These modes are compatible with borg 1.0.x. - repokey-blake2 and keyfile-blake2 are also authenticated encryption modes, + `repokey-blake2` and `keyfile-blake2` are also authenticated encryption modes, but use BLAKE2b-256 instead of HMAC-SHA256 for authentication. The chunk ID hash is a keyed BLAKE2b-256 hash. - These modes are new and not compatible with borg 1.0.x. + These modes are new and *not* compatible with borg 1.0.x. - "authenticated" mode uses no encryption, but authenticates repository contents + `authenticated` mode uses no encryption, but authenticates repository contents through the same keyed BLAKE2b-256 hash as the other blake2 modes (it uses it as chunk ID hash). The key is stored like repokey. This mode is new and not compatible with borg 1.0.x. - "none" mode uses no encryption and no authentication. It uses sha256 as chunk + `none` mode uses no encryption and no authentication. It uses sha256 as chunk ID hash. Not recommended, rather consider using an authenticated or authenticated/encrypted mode. This mode is compatible with borg 1.0.x. Hardware acceleration will be used automatically. - On modern Intel/AMD CPUs (except very cheap ones), AES is usually hw - accelerated. BLAKE2b is faster than sha256 on Intel/AMD 64bit CPUs. + On modern Intel/AMD CPUs (except very cheap ones), AES is usually + hardware-accelerated. BLAKE2b is faster than SHA256 on Intel/AMD 64bit CPUs, + which makes `authenticated` faster than `none`. - On modern ARM CPUs, NEON provides hw acceleration for sha256 making it faster + On modern ARM CPUs, NEON provides hardware acceleration for SHA256 making it faster than BLAKE2b-256 there. """) subparser = subparsers.add_parser('init', parents=[common_parser], add_help=False, @@ -1804,7 +1805,7 @@ class Archiver: subparser.add_argument('-e', '--encryption', dest='encryption', choices=('none', 'keyfile', 'repokey', 'keyfile-blake2', 'repokey-blake2', 'authenticated'), default=None, - help='select encryption key mode (default: "%(default)s")') + help='select encryption key mode') subparser.add_argument('-a', '--append-only', dest='append_only', action='store_true', help='create an append-only mode repository') From 0710bbd40e07f9423e2977778fd67e3159a0e465 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 5 Feb 2017 21:17:21 +0100 Subject: [PATCH 0609/1387] docs: create: move item flags to main doc --- docs/man/borg-create.1 | 57 +++++++++++++++- docs/man/borg-list.1 | 151 +++++++++++++++++++++-------------------- docs/usage.rst | 40 ----------- setup.py | 11 ++- src/borg/archiver.py | 117 ++++++++++++++++++++++--------- 5 files changed, 228 insertions(+), 148 deletions(-) diff --git a/docs/man/borg-create.1 b/docs/man/borg-create.1 index f3e3c9be..3e30a343 100644 --- a/docs/man/borg-create.1 +++ b/docs/man/borg-create.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CREATE 1 "2017-02-05" "" "borg backup tool" +.TH BORG-CREATE 1 "2017-02-12" "" "borg backup tool" .SH NAME borg-create \- Create new archive . @@ -224,6 +224,61 @@ exclude foo/.bundler/gems. In borg it will not, you need to use \-\-exclude \(aq*/.bundler/gems\(aq to get the same effect. See \fBborg help patterns\fP for more information. .UNINDENT +.SH NOTES +.SS Item flags +.sp +\fB\-\-list\fP outputs a list of all files, directories and other +file system items it considered (no matter whether they had content changes +or not). For each item, it prefixes a single\-letter flag that indicates type +and/or status of the item. +.sp +If you are interested only in a subset of that output, you can give e.g. +\fB\-\-filter=AME\fP and it will only show regular files with A, M or E status (see +below). +.sp +A uppercase character represents the status of a regular file relative to the +"files" cache (not relative to the repo \-\- this is an issue if the files cache +is not used). Metadata is stored in any case and for \(aqA\(aq and \(aqM\(aq also new data +chunks are stored. For \(aqU\(aq all data chunks refer to already existing chunks. +.INDENT 0.0 +.IP \(bu 2 +\(aqA\(aq = regular file, added (see also \fIa_status_oddity\fP in the FAQ) +.IP \(bu 2 +\(aqM\(aq = regular file, modified +.IP \(bu 2 +\(aqU\(aq = regular file, unchanged +.IP \(bu 2 +\(aqE\(aq = regular file, an error happened while accessing/reading \fIthis\fP file +.UNINDENT +.sp +A lowercase character means a file type other than a regular file, +borg usually just stores their metadata: +.INDENT 0.0 +.IP \(bu 2 +\(aqd\(aq = directory +.IP \(bu 2 +\(aqb\(aq = block device +.IP \(bu 2 +\(aqc\(aq = char device +.IP \(bu 2 +\(aqh\(aq = regular file, hardlink (to already seen inodes) +.IP \(bu 2 +\(aqs\(aq = symlink +.IP \(bu 2 +\(aqf\(aq = fifo +.UNINDENT +.sp +Other flags used include: +.INDENT 0.0 +.IP \(bu 2 +\(aqi\(aq = backup data was read from standard input (stdin) +.IP \(bu 2 +\(aq\-\(aq = dry run, item was \fInot\fP backed up +.IP \(bu 2 +\(aqx\(aq = excluded, item was \fInot\fP backed up +.IP \(bu 2 +\(aq?\(aq = missing status code (if you see this, please file a bug report!) +.UNINDENT .SH SEE ALSO .sp \fIborg\-common(1)\fP, \fIborg\-delete(1)\fP, \fIborg\-prune(1)\fP, \fIborg\-check(1)\fP, \fIborg\-patterns(1)\fP, \fIborg\-placeholders(1)\fP, \fIborg\-compression(1)\fP diff --git a/docs/man/borg-list.1 b/docs/man/borg-list.1 index d6e5a4ec..74e5a278 100644 --- a/docs/man/borg-list.1 +++ b/docs/man/borg-list.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-LIST 1 "2017-02-05" "" "borg backup tool" +.TH BORG-LIST 1 "2017-02-12" "" "borg backup tool" .SH NAME borg-list \- List archive or repository contents . @@ -38,6 +38,81 @@ borg list REPOSITORY_OR_ARCHIVE PATH This command lists the contents of a repository or an archive. .sp See the "borg help patterns" command for more help on exclude patterns. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B REPOSITORY_OR_ARCHIVE +repository/archive to list contents of +.TP +.B PATH +paths to list; patterns are supported +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-\-short +only print file/directory names, nothing else +.TP +.B \-\-format\fP,\fB \-\-list\-format +specify format for file listing +(default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") +.TP +.BI \-e \ PATTERN\fP,\fB \ \-\-exclude \ PATTERN +exclude paths matching PATTERN +.TP +.BI \-\-exclude\-from \ EXCLUDEFILE +read exclude patterns from EXCLUDEFILE, one per line +.UNINDENT +.SS filters +.INDENT 0.0 +.TP +.B \-P\fP,\fB \-\-prefix +only consider archive names starting with this prefix +.TP +.B \-\-sort\-by +Comma\-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp +.TP +.BI \-\-first \ N +consider first N archives after other filters were applied +.TP +.BI \-\-last \ N +consider last N archives after other filters were applied +.UNINDENT +.SH EXAMPLES +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg list /path/to/repo +Monday Mon, 2016\-02\-15 19:15:11 +repo Mon, 2016\-02\-15 19:26:54 +root\-2016\-02\-15 Mon, 2016\-02\-15 19:36:29 +newname Mon, 2016\-02\-15 19:50:19 +\&... + +$ borg list /path/to/repo::root\-2016\-02\-15 +drwxr\-xr\-x root root 0 Mon, 2016\-02\-15 17:44:27 . +drwxrwxr\-x root root 0 Mon, 2016\-02\-15 19:04:49 bin +\-rwxr\-xr\-x root root 1029624 Thu, 2014\-11\-13 00:08:51 bin/bash +lrwxrwxrwx root root 0 Fri, 2015\-03\-27 20:24:26 bin/bzcmp \-> bzdiff +\-rwxr\-xr\-x root root 2140 Fri, 2015\-03\-27 20:24:22 bin/bzdiff +\&... + +$ borg list /path/to/repo::archiveA \-\-list\-format="{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}" +drwxrwxr\-x user user 0 Sun, 2015\-02\-01 11:00:00 . +drwxrwxr\-x user user 0 Sun, 2015\-02\-01 11:00:00 code +drwxrwxr\-x user user 0 Sun, 2015\-02\-01 11:00:00 code/myproject +\-rw\-rw\-r\-\- user user 1416192 Sun, 2015\-02\-01 11:00:00 code/myproject/file.ext +\&... +.ft P +.fi +.UNINDENT +.UNINDENT +.SH NOTES .sp The following keys are available for \-\-format: .INDENT 0.0 @@ -162,80 +237,6 @@ health: either "healthy" (file ok) or "broken" (if file has all\-zero replacemen .UNINDENT .UNINDENT .UNINDENT -.SH OPTIONS -.sp -See \fIborg\-common(1)\fP for common options of Borg commands. -.SS arguments -.INDENT 0.0 -.TP -.B REPOSITORY_OR_ARCHIVE -repository/archive to list contents of -.TP -.B PATH -paths to list; patterns are supported -.UNINDENT -.SS optional arguments -.INDENT 0.0 -.TP -.B \-\-short -only print file/directory names, nothing else -.TP -.B \-\-format\fP,\fB \-\-list\-format -specify format for file listing -(default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") -.TP -.BI \-e \ PATTERN\fP,\fB \ \-\-exclude \ PATTERN -exclude paths matching PATTERN -.TP -.BI \-\-exclude\-from \ EXCLUDEFILE -read exclude patterns from EXCLUDEFILE, one per line -.UNINDENT -.SS filters -.INDENT 0.0 -.TP -.B \-P\fP,\fB \-\-prefix -only consider archive names starting with this prefix -.TP -.B \-\-sort\-by -Comma\-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp -.TP -.BI \-\-first \ N -consider first N archives after other filters were applied -.TP -.BI \-\-last \ N -consider last N archives after other filters were applied -.UNINDENT -.SH EXAMPLES -.INDENT 0.0 -.INDENT 3.5 -.sp -.nf -.ft C -$ borg list /path/to/repo -Monday Mon, 2016\-02\-15 19:15:11 -repo Mon, 2016\-02\-15 19:26:54 -root\-2016\-02\-15 Mon, 2016\-02\-15 19:36:29 -newname Mon, 2016\-02\-15 19:50:19 -\&... - -$ borg list /path/to/repo::root\-2016\-02\-15 -drwxr\-xr\-x root root 0 Mon, 2016\-02\-15 17:44:27 . -drwxrwxr\-x root root 0 Mon, 2016\-02\-15 19:04:49 bin -\-rwxr\-xr\-x root root 1029624 Thu, 2014\-11\-13 00:08:51 bin/bash -lrwxrwxrwx root root 0 Fri, 2015\-03\-27 20:24:26 bin/bzcmp \-> bzdiff -\-rwxr\-xr\-x root root 2140 Fri, 2015\-03\-27 20:24:22 bin/bzdiff -\&... - -$ borg list /path/to/repo::archiveA \-\-list\-format="{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}" -drwxrwxr\-x user user 0 Sun, 2015\-02\-01 11:00:00 . -drwxrwxr\-x user user 0 Sun, 2015\-02\-01 11:00:00 code -drwxrwxr\-x user user 0 Sun, 2015\-02\-01 11:00:00 code/myproject -\-rw\-rw\-r\-\- user user 1416192 Sun, 2015\-02\-01 11:00:00 code/myproject/file.ext -\&... -.ft P -.fi -.UNINDENT -.UNINDENT .SH SEE ALSO .sp \fIborg\-common(1)\fP, \fIborg\-info(1)\fP, \fIborg\-diff(1)\fP, \fIborg\-prune(1)\fP, \fIborg\-patterns(1)\fP diff --git a/docs/usage.rst b/docs/usage.rst index d160bc0a..bdc6b23d 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -851,46 +851,6 @@ Additional Notes Here are misc. notes about topics that are maybe not covered in enough detail in the usage section. -Item flags -~~~~~~~~~~ - -``borg create --list`` outputs a list of all files, directories and other -file system items it considered (no matter whether they had content changes -or not). For each item, it prefixes a single-letter flag that indicates type -and/or status of the item. - -If you are interested only in a subset of that output, you can give e.g. -``--filter=AME`` and it will only show regular files with A, M or E status (see -below). - -A uppercase character represents the status of a regular file relative to the -"files" cache (not relative to the repo -- this is an issue if the files cache -is not used). Metadata is stored in any case and for 'A' and 'M' also new data -chunks are stored. For 'U' all data chunks refer to already existing chunks. - -- 'A' = regular file, added (see also :ref:`a_status_oddity` in the FAQ) -- 'M' = regular file, modified -- 'U' = regular file, unchanged -- 'E' = regular file, an error happened while accessing/reading *this* file - -A lowercase character means a file type other than a regular file, -borg usually just stores their metadata: - -- 'd' = directory -- 'b' = block device -- 'c' = char device -- 'h' = regular file, hardlink (to already seen inodes) -- 's' = symlink -- 'f' = fifo - -Other flags used include: - -- 'i' = backup data was read from standard input (stdin) -- '-' = dry run, item was *not* backed up -- 'x' = excluded, item was *not* backed up -- '?' = missing status code (if you see this, please file a bug report!) - - --chunker-params ~~~~~~~~~~~~~~~~ The chunker params influence how input files are cut into pieces (chunks) diff --git a/setup.py b/setup.py index 2062221e..6e57a5ed 100644 --- a/setup.py +++ b/setup.py @@ -214,6 +214,8 @@ class build_usage(Command): def run(self): print('generating usage docs') + import borg + borg.doc_mode = 'build_man' if not os.path.exists('docs/usage'): os.mkdir('docs/usage') # allows us to build docs without the C modules fully loaded during help generation @@ -361,6 +363,8 @@ class build_man(Command): def run(self): print('building man pages (in docs/man)', file=sys.stderr) + import borg + borg.doc_mode = 'build_man' os.makedirs('docs/man', exist_ok=True) # allows us to build docs without the C modules fully loaded during help generation from borg.archiver import Archiver @@ -399,7 +403,8 @@ class build_man(Command): write('\n') self.write_heading(write, 'DESCRIPTION') - write(parser.epilog) + description, _, notes = parser.epilog.partition('\n.. man NOTES') + write(description) self.write_heading(write, 'OPTIONS') write('See `borg-common(1)` for common options of Borg commands.') @@ -408,6 +413,10 @@ class build_man(Command): self.write_examples(write, command) + if notes: + self.write_heading(write, 'NOTES') + write(notes) + self.write_see_also(write, man_title) self.gen_man_page(man_title, doc.getvalue()) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 0746cbdc..65d39bd9 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -25,6 +25,7 @@ logger = create_logger() import msgpack +import borg from . import __version__ from . import helpers from .archive import Archive, ArchiveChecker, ArchiveRecreater, Statistics, is_special @@ -1657,6 +1658,16 @@ class Archiver: return args def build_parser(self, prog=None): + def process_epilog(epilog): + epilog = textwrap.dedent(epilog).splitlines() + try: + mode = borg.doc_mode + except AttributeError: + mode = 'command-line' + if mode in ('command-line', 'build_usage'): + epilog = [line for line in epilog if not line.startswith('.. man')] + return '\n'.join(epilog) + common_parser = argparse.ArgumentParser(add_help=False, prog=prog) common_group = common_parser.add_argument_group('Common options') @@ -1703,7 +1714,7 @@ class Archiver: help='show version number and exit') subparsers = parser.add_subparsers(title='required arguments', metavar='') - serve_epilog = textwrap.dedent(""" + serve_epilog = process_epilog(""" This command starts a repository server process. This command is usually not used manually. """) subparser = subparsers.add_parser('serve', parents=[common_parser], add_help=False, @@ -1718,7 +1729,7 @@ class Archiver: subparser.add_argument('--append-only', dest='append_only', action='store_true', help='only allow appending to repository segment files') - init_epilog = textwrap.dedent(""" + init_epilog = process_epilog(""" This command initializes an empty repository. A repository is a filesystem directory containing the deduplicated data from zero or more archives. @@ -1809,7 +1820,7 @@ class Archiver: subparser.add_argument('-a', '--append-only', dest='append_only', action='store_true', help='create an append-only mode repository') - check_epilog = textwrap.dedent(""" + check_epilog = process_epilog(""" The check command verifies the consistency of a repository and the corresponding archives. First, the underlying repository data files are checked: @@ -1895,7 +1906,7 @@ class Archiver: key_parsers = subparser.add_subparsers(title='required arguments', metavar='') subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser)) - key_export_epilog = textwrap.dedent(""" + key_export_epilog = process_epilog(""" If repository encryption is used, the repository is inaccessible without the key. This command allows to backup this essential key. @@ -1928,7 +1939,7 @@ class Archiver: default=False, help='Create an export suitable for printing and later type-in') - key_import_epilog = textwrap.dedent(""" + key_import_epilog = process_epilog(""" This command allows to restore a key previously backed up with the export command. @@ -1950,7 +1961,7 @@ class Archiver: default=False, help='interactively import from a backup done with --paper') - change_passphrase_epilog = textwrap.dedent(""" + change_passphrase_epilog = process_epilog(""" The key files used for repository encryption are optionally passphrase protected. This command can be used to change this passphrase. """) @@ -1973,7 +1984,7 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False)) - migrate_to_repokey_epilog = textwrap.dedent(""" + migrate_to_repokey_epilog = process_epilog(""" This command migrates a repository from passphrase mode (removed in Borg 1.0) to repokey mode. @@ -2000,7 +2011,7 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False)) - create_epilog = textwrap.dedent(""" + create_epilog = process_epilog(""" This command creates a backup archive containing all files found while recursively traversing all paths specified. When giving '-' as path, borg will read data from standard input and create a file 'stdin' in the created archive from that @@ -2023,6 +2034,47 @@ class Archiver: See the output of the "borg help patterns" command for more help on exclude patterns. See the output of the "borg help placeholders" command for more help on placeholders. + + .. man NOTES + + Item flags + ++++++++++ + + ``--list`` outputs a list of all files, directories and other + file system items it considered (no matter whether they had content changes + or not). For each item, it prefixes a single-letter flag that indicates type + and/or status of the item. + + If you are interested only in a subset of that output, you can give e.g. + ``--filter=AME`` and it will only show regular files with A, M or E status (see + below). + + A uppercase character represents the status of a regular file relative to the + "files" cache (not relative to the repo -- this is an issue if the files cache + is not used). Metadata is stored in any case and for 'A' and 'M' also new data + chunks are stored. For 'U' all data chunks refer to already existing chunks. + + - 'A' = regular file, added (see also :ref:`a_status_oddity` in the FAQ) + - 'M' = regular file, modified + - 'U' = regular file, unchanged + - 'E' = regular file, an error happened while accessing/reading *this* file + + A lowercase character means a file type other than a regular file, + borg usually just stores their metadata: + + - 'd' = directory + - 'b' = block device + - 'c' = char device + - 'h' = regular file, hardlink (to already seen inodes) + - 's' = symlink + - 'f' = fifo + + Other flags used include: + + - 'i' = backup data was read from standard input (stdin) + - '-' = dry run, item was *not* backed up + - 'x' = excluded, item was *not* backed up + - '?' = missing status code (if you see this, please file a bug report!) """) subparser = subparsers.add_parser('create', parents=[common_parser], add_help=False, @@ -2123,7 +2175,7 @@ class Archiver: subparser.add_argument('paths', metavar='PATH', nargs='+', type=str, help='paths to archive') - extract_epilog = textwrap.dedent(""" + extract_epilog = process_epilog(""" This command extracts the contents of an archive. By default the entire archive is extracted but a subset of files and directories can be selected by passing a list of ``PATHs`` as arguments. The file selection can further @@ -2174,7 +2226,7 @@ class Archiver: subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to extract; patterns are supported') - diff_epilog = textwrap.dedent(""" + diff_epilog = process_epilog(""" This command finds differences (file contents, user/group/mode) between archives. A repository location and an archive name must be specified for REPO_ARCHIVE1. @@ -2222,7 +2274,7 @@ class Archiver: subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths of items inside the archives to compare; patterns are supported') - rename_epilog = textwrap.dedent(""" + rename_epilog = process_epilog(""" This command renames an archive in the repository. This results in a different archive ID. @@ -2240,7 +2292,7 @@ class Archiver: type=archivename_validator(), help='the new archive name to use') - delete_epilog = textwrap.dedent(""" + delete_epilog = process_epilog(""" This command deletes an archive from the repository or the complete repository. Disk space is reclaimed accordingly. If you delete the complete repository, the local cache for it (if any) is also deleted. @@ -2271,13 +2323,16 @@ class Archiver: help='archive or repository to delete') self.add_archives_filters_args(subparser) - list_epilog = textwrap.dedent(""" + list_epilog = process_epilog(""" This command lists the contents of a repository or an archive. See the "borg help patterns" command for more help on exclude patterns. + .. man NOTES + The following keys are available for --format: + """) + BaseFormatter.keys_help() + textwrap.dedent(""" Keys for listing repository archives: @@ -2312,7 +2367,7 @@ class Archiver: help='paths to list; patterns are supported') self.add_archives_filters_args(subparser) - mount_epilog = textwrap.dedent(""" + mount_epilog = process_epilog(""" This command mounts an archive as a FUSE filesystem. This can be useful for browsing an archive or restoring individual files. Unless the ``--foreground`` option is given the command will run in the background until the filesystem @@ -2363,7 +2418,7 @@ class Archiver: help='Extra mount options') self.add_archives_filters_args(subparser) - umount_epilog = textwrap.dedent(""" + umount_epilog = process_epilog(""" This command un-mounts a FUSE filesystem that was mounted with ``borg mount``. This is a convenience wrapper that just calls the platform-specific shell @@ -2378,7 +2433,7 @@ class Archiver: subparser.add_argument('mountpoint', metavar='MOUNTPOINT', type=str, help='mountpoint of the filesystem to umount') - info_epilog = textwrap.dedent(""" + info_epilog = process_epilog(""" This command displays detailed information about the specified archive or repository. Please note that the deduplicated sizes of the individual archives do not add @@ -2401,7 +2456,7 @@ class Archiver: help='archive or repository to display information about') self.add_archives_filters_args(subparser) - break_lock_epilog = textwrap.dedent(""" + break_lock_epilog = process_epilog(""" This command breaks the repository and cache locks. Please use carefully and only while no borg process (on any machine) is trying to access the Cache or the Repository. @@ -2416,7 +2471,7 @@ class Archiver: type=location_validator(archive=False), help='repository for which to break the locks') - prune_epilog = textwrap.dedent(""" + prune_epilog = process_epilog(""" The prune command prunes a repository by deleting all archives not matching any of the specified retention options. This command is normally used by automated backup scripts wanting to keep a certain number of historic backups. @@ -2502,7 +2557,7 @@ class Archiver: type=location_validator(archive=False), help='repository to prune') - upgrade_epilog = textwrap.dedent(""" + upgrade_epilog = process_epilog(""" Upgrade an existing Borg repository. Borg 1.x.y upgrades @@ -2592,7 +2647,7 @@ class Archiver: type=location_validator(archive=False), help='path to the repository to be upgraded') - recreate_epilog = textwrap.dedent(""" + recreate_epilog = process_epilog(""" Recreate the contents of existing archives. This is an *experimental* feature. Do *not* use this on your only backup. @@ -2711,7 +2766,7 @@ class Archiver: subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to recreate; patterns are supported') - with_lock_epilog = textwrap.dedent(""" + with_lock_epilog = process_epilog(""" This command runs a user-specified command while the repository lock is held. It will first try to acquire the lock (make sure that no other operation is @@ -2747,7 +2802,7 @@ class Archiver: subparser.add_argument('topic', metavar='TOPIC', type=str, nargs='?', help='additional help on TOPIC') - debug_epilog = textwrap.dedent(""" + debug_epilog = process_epilog(""" These commands are not intended for normal use and potentially very dangerous if used incorrectly. @@ -2764,7 +2819,7 @@ class Archiver: debug_parsers = subparser.add_subparsers(title='required arguments', metavar='') subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser)) - debug_info_epilog = textwrap.dedent(""" + debug_info_epilog = process_epilog(""" This command displays some system information that might be useful for bug reports and debugging problems. If a traceback happens, this information is already appended at the end of the traceback. @@ -2776,7 +2831,7 @@ class Archiver: help='show system infos for debugging / bug reports (debug)') subparser.set_defaults(func=self.do_debug_info) - debug_dump_archive_items_epilog = textwrap.dedent(""" + debug_dump_archive_items_epilog = process_epilog(""" This command dumps raw (but decrypted and decompressed) archive items (only metadata) to files. """) subparser = debug_parsers.add_parser('dump-archive-items', parents=[common_parser], add_help=False, @@ -2789,7 +2844,7 @@ class Archiver: type=location_validator(archive=True), help='archive to dump') - debug_dump_archive_epilog = textwrap.dedent(""" + debug_dump_archive_epilog = process_epilog(""" This command dumps all metadata of an archive in a decoded form to a file. """) subparser = debug_parsers.add_parser('dump-archive', parents=[common_parser], add_help=False, @@ -2804,7 +2859,7 @@ class Archiver: subparser.add_argument('path', metavar='PATH', type=str, help='file to dump data into') - debug_dump_manifest_epilog = textwrap.dedent(""" + debug_dump_manifest_epilog = process_epilog(""" This command dumps manifest metadata of a repository in a decoded form to a file. """) subparser = debug_parsers.add_parser('dump-manifest', parents=[common_parser], add_help=False, @@ -2819,7 +2874,7 @@ class Archiver: subparser.add_argument('path', metavar='PATH', type=str, help='file to dump data into') - debug_dump_repo_objs_epilog = textwrap.dedent(""" + debug_dump_repo_objs_epilog = process_epilog(""" This command dumps raw (but decrypted and decompressed) repo objects to files. """) subparser = debug_parsers.add_parser('dump-repo-objs', parents=[common_parser], add_help=False, @@ -2832,7 +2887,7 @@ class Archiver: type=location_validator(archive=False), help='repo to dump') - debug_get_obj_epilog = textwrap.dedent(""" + debug_get_obj_epilog = process_epilog(""" This command gets an object from the repository. """) subparser = debug_parsers.add_parser('get-obj', parents=[common_parser], add_help=False, @@ -2849,7 +2904,7 @@ class Archiver: subparser.add_argument('path', metavar='PATH', type=str, help='file to write object data into') - debug_put_obj_epilog = textwrap.dedent(""" + debug_put_obj_epilog = process_epilog(""" This command puts objects into the repository. """) subparser = debug_parsers.add_parser('put-obj', parents=[common_parser], add_help=False, @@ -2864,7 +2919,7 @@ class Archiver: subparser.add_argument('paths', metavar='PATH', nargs='+', type=str, help='file(s) to read and create object(s) from') - debug_delete_obj_epilog = textwrap.dedent(""" + debug_delete_obj_epilog = process_epilog(""" This command deletes objects from the repository. """) subparser = debug_parsers.add_parser('delete-obj', parents=[common_parser], add_help=False, @@ -2879,7 +2934,7 @@ class Archiver: subparser.add_argument('ids', metavar='IDs', nargs='+', type=str, help='hex object ID(s) to delete from the repo') - debug_refcount_obj_epilog = textwrap.dedent(""" + debug_refcount_obj_epilog = process_epilog(""" This command displays the reference count for objects from the repository. """) subparser = debug_parsers.add_parser('refcount-obj', parents=[common_parser], add_help=False, From 15dfaae223d8ba52bbbddf9979244ef729e89922 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 5 Feb 2017 21:17:42 +0100 Subject: [PATCH 0610/1387] docs: create: move --exclude note to main doc --- docs/usage.rst | 8 -------- src/borg/archiver.py | 13 +++++++++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index bdc6b23d..b00f157b 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -437,14 +437,6 @@ Examples # As above, but add nanoseconds $ borg create /path/to/repo::{hostname}-{user}-{now:%Y-%m-%dT%H:%M:%S.%f} ~ -Notes -~~~~~ - -- the --exclude patterns are not like tar. In tar --exclude .bundler/gems will - exclude foo/.bundler/gems. In borg it will not, you need to use --exclude - '\*/.bundler/gems' to get the same effect. See ``borg help patterns`` for - more information. - .. include:: usage/extract.rst.inc diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 65d39bd9..feca6ccf 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2037,6 +2037,11 @@ class Archiver: .. man NOTES + The --exclude patterns are not like tar. In tar --exclude .bundler/gems will + exclude foo/.bundler/gems. In borg it will not, you need to use --exclude + '\*/.bundler/gems' to get the same effect. See ``borg help patterns`` for + more information. + Item flags ++++++++++ @@ -2115,12 +2120,12 @@ class Archiver: 'http://www.brynosaurus.com/cachedir/spec.html)') exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', metavar='NAME', action='append', type=str, - help='exclude directories that are tagged by containing a filesystem object with \ - the given NAME') + help='exclude directories that are tagged by containing a filesystem object with ' + 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', action='store_true', default=False, - help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise \ - excluded caches/directories') + help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise ' + 'excluded caches/directories') fs_group = subparser.add_argument_group('Filesystem options') fs_group.add_argument('-x', '--one-file-system', dest='one_file_system', From fa24e1f38fcc87f33b17dceb51d974185bd2803b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 5 Feb 2017 21:32:24 +0100 Subject: [PATCH 0611/1387] man pages: add borg(1) master/intro page --- docs/man/borg-break-lock.1 | 2 +- docs/man/borg-change-passphrase.1 | 2 +- docs/man/borg-check.1 | 2 +- docs/man/borg-common.1 | 2 +- docs/man/borg-compression.1 | 2 +- docs/man/borg-create.1 | 13 +- docs/man/borg-delete.1 | 2 +- docs/man/borg-diff.1 | 2 +- docs/man/borg-extract.1 | 2 +- docs/man/borg-info.1 | 2 +- docs/man/borg-init.1 | 21 +- docs/man/borg-key-change-passphrase.1 | 2 +- docs/man/borg-key-export.1 | 2 +- docs/man/borg-key-import.1 | 2 +- docs/man/borg-key-migrate-to-repokey.1 | 2 +- docs/man/borg-key.1 | 47 ++ docs/man/borg-mount.1 | 2 +- docs/man/borg-patterns.1 | 2 +- docs/man/borg-placeholders.1 | 2 +- docs/man/borg-prune.1 | 2 +- docs/man/borg-recreate.1 | 2 +- docs/man/borg-rename.1 | 2 +- docs/man/borg-serve.1 | 2 +- docs/man/borg-umount.1 | 2 +- docs/man/borg-upgrade.1 | 2 +- docs/man/borg-with-lock.1 | 2 +- docs/man/borg.1 | 567 ++++++++++++++++++++++ docs/man_intro.rst | 68 +++ docs/quickstart.rst | 63 +-- docs/quickstart_example.rst.inc | 62 +++ docs/usage.rst | 335 +------------ docs/usage/create.rst.inc | 62 ++- docs/usage/general.rst.inc | 329 +++++++++++++ docs/usage/init.rst.inc | 19 +- docs/usage/key_change-passphrase.rst.inc | 22 + docs/usage/key_migrate-to-repokey.rst.inc | 36 ++ docs/usage/list.rst.inc | 7 +- docs/usage/prune.rst.inc | 2 +- docs/usage/recreate.rst.inc | 16 +- setup.py | 12 +- 40 files changed, 1264 insertions(+), 463 deletions(-) create mode 100644 docs/man/borg-key.1 create mode 100644 docs/man/borg.1 create mode 100644 docs/man_intro.rst create mode 100644 docs/quickstart_example.rst.inc create mode 100644 docs/usage/general.rst.inc create mode 100644 docs/usage/key_change-passphrase.rst.inc create mode 100644 docs/usage/key_migrate-to-repokey.rst.inc diff --git a/docs/man/borg-break-lock.1 b/docs/man/borg-break-lock.1 index b5870e56..b8162436 100644 --- a/docs/man/borg-break-lock.1 +++ b/docs/man/borg-break-lock.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-BREAK-LOCK 1 "2017-02-05" "" "borg backup tool" +.TH BORG-BREAK-LOCK 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-break-lock \- Break the repository lock (e.g. in case it was left by a dead borg. . diff --git a/docs/man/borg-change-passphrase.1 b/docs/man/borg-change-passphrase.1 index fb32bfa2..b74dd336 100644 --- a/docs/man/borg-change-passphrase.1 +++ b/docs/man/borg-change-passphrase.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CHANGE-PASSPHRASE 1 "2017-02-05" "" "borg backup tool" +.TH BORG-CHANGE-PASSPHRASE 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-change-passphrase \- Change repository key file passphrase . diff --git a/docs/man/borg-check.1 b/docs/man/borg-check.1 index 6608887b..8a0ec36b 100644 --- a/docs/man/borg-check.1 +++ b/docs/man/borg-check.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CHECK 1 "2017-02-05" "" "borg backup tool" +.TH BORG-CHECK 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-check \- Check repository consistency . diff --git a/docs/man/borg-common.1 b/docs/man/borg-common.1 index 17afcff9..e480c1f8 100644 --- a/docs/man/borg-common.1 +++ b/docs/man/borg-common.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-COMMON 1 "2017-02-05" "" "borg backup tool" +.TH BORG-COMMON 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-common \- Common options of Borg commands . diff --git a/docs/man/borg-compression.1 b/docs/man/borg-compression.1 index cc5fbafd..51124fea 100644 --- a/docs/man/borg-compression.1 +++ b/docs/man/borg-compression.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-COMPRESSION 1 "2017-02-05" "" "borg backup tool" +.TH BORG-COMPRESSION 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-compression \- Details regarding compression . diff --git a/docs/man/borg-create.1 b/docs/man/borg-create.1 index 3e30a343..d01d77e2 100644 --- a/docs/man/borg-create.1 +++ b/docs/man/borg-create.1 @@ -100,10 +100,10 @@ read exclude patterns from EXCLUDEFILE, one per line exclude directories that contain a CACHEDIR.TAG file (\fI\%http://www.brynosaurus.com/cachedir/spec.html\fP) .TP .BI \-\-exclude\-if\-present \ NAME -exclude directories that are tagged by containing a filesystem object with the given NAME +exclude directories that are tagged by containing a filesystem object with the given NAME .TP .B \-\-keep\-exclude\-tags\fP,\fB \-\-keep\-tag\-files -keep tag objects (i.e.: arguments to \-\-exclude\-if\-present) in otherwise excluded caches/directories +keep tag objects (i.e.: arguments to \-\-exclude\-if\-present) in otherwise excluded caches/directories .UNINDENT .SS Filesystem options .INDENT 0.0 @@ -216,15 +216,12 @@ $ borg create /path/to/repo::{hostname}\-{user}\-{now:%Y\-%m\-%dT%H:%M:%S.%f} ~ .fi .UNINDENT .UNINDENT -.SS Notes -.INDENT 0.0 -.IP \(bu 2 -the \-\-exclude patterns are not like tar. In tar \-\-exclude .bundler/gems will +.SH NOTES +.sp +The \-\-exclude patterns are not like tar. In tar \-\-exclude .bundler/gems will exclude foo/.bundler/gems. In borg it will not, you need to use \-\-exclude \(aq*/.bundler/gems\(aq to get the same effect. See \fBborg help patterns\fP for more information. -.UNINDENT -.SH NOTES .SS Item flags .sp \fB\-\-list\fP outputs a list of all files, directories and other diff --git a/docs/man/borg-delete.1 b/docs/man/borg-delete.1 index e3a73797..5b0ec529 100644 --- a/docs/man/borg-delete.1 +++ b/docs/man/borg-delete.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-DELETE 1 "2017-02-05" "" "borg backup tool" +.TH BORG-DELETE 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-delete \- Delete an existing repository or archives . diff --git a/docs/man/borg-diff.1 b/docs/man/borg-diff.1 index dc3d9be9..f488a72a 100644 --- a/docs/man/borg-diff.1 +++ b/docs/man/borg-diff.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-DIFF 1 "2017-02-05" "" "borg backup tool" +.TH BORG-DIFF 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-diff \- Diff contents of two archives . diff --git a/docs/man/borg-extract.1 b/docs/man/borg-extract.1 index 2770a514..426be31e 100644 --- a/docs/man/borg-extract.1 +++ b/docs/man/borg-extract.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-EXTRACT 1 "2017-02-05" "" "borg backup tool" +.TH BORG-EXTRACT 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-extract \- Extract archive contents . diff --git a/docs/man/borg-info.1 b/docs/man/borg-info.1 index 2609912d..305f18fa 100644 --- a/docs/man/borg-info.1 +++ b/docs/man/borg-info.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-INFO 1 "2017-02-05" "" "borg backup tool" +.TH BORG-INFO 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-info \- Show archive details such as disk space used . diff --git a/docs/man/borg-init.1 b/docs/man/borg-init.1 index 40d8a25d..bbe020c4 100644 --- a/docs/man/borg-init.1 +++ b/docs/man/borg-init.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-INIT 1 "2017-02-05" "" "borg backup tool" +.TH BORG-INIT 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-init \- Initialize an empty repository . @@ -82,32 +82,33 @@ You can change your passphrase for existing repos at any time, it won\(aqt affec the encryption/decryption key or other secrets. .SS Encryption modes .sp -repokey and keyfile use AES\-CTR\-256 for encryption and HMAC\-SHA256 for +\fIrepokey\fP and \fIkeyfile\fP use AES\-CTR\-256 for encryption and HMAC\-SHA256 for authentication in an encrypt\-then\-MAC (EtM) construction. The chunk ID hash is HMAC\-SHA256 as well (with a separate key). These modes are compatible with borg 1.0.x. .sp -repokey\-blake2 and keyfile\-blake2 are also authenticated encryption modes, +\fIrepokey\-blake2\fP and \fIkeyfile\-blake2\fP are also authenticated encryption modes, but use BLAKE2b\-256 instead of HMAC\-SHA256 for authentication. The chunk ID hash is a keyed BLAKE2b\-256 hash. -These modes are new and not compatible with borg 1.0.x. +These modes are new and \fInot\fP compatible with borg 1.0.x. .sp -"authenticated" mode uses no encryption, but authenticates repository contents +\fIauthenticated\fP mode uses no encryption, but authenticates repository contents through the same keyed BLAKE2b\-256 hash as the other blake2 modes (it uses it as chunk ID hash). The key is stored like repokey. This mode is new and not compatible with borg 1.0.x. .sp -"none" mode uses no encryption and no authentication. It uses sha256 as chunk +\fInone\fP mode uses no encryption and no authentication. It uses sha256 as chunk ID hash. Not recommended, rather consider using an authenticated or authenticated/encrypted mode. This mode is compatible with borg 1.0.x. .sp Hardware acceleration will be used automatically. .sp -On modern Intel/AMD CPUs (except very cheap ones), AES is usually hw -accelerated. BLAKE2b is faster than sha256 on Intel/AMD 64bit CPUs. +On modern Intel/AMD CPUs (except very cheap ones), AES is usually +hardware\-accelerated. BLAKE2b is faster than SHA256 on Intel/AMD 64bit CPUs, +which makes \fIauthenticated\fP faster than \fInone\fP\&. .sp -On modern ARM CPUs, NEON provides hw acceleration for sha256 making it faster +On modern ARM CPUs, NEON provides hardware acceleration for SHA256 making it faster than BLAKE2b\-256 there. .SH OPTIONS .sp @@ -122,7 +123,7 @@ repository to create .INDENT 0.0 .TP .B \-e\fP,\fB \-\-encryption -select encryption key mode (default: "None") +select encryption key mode .TP .B \-a\fP,\fB \-\-append\-only create an append\-only mode repository diff --git a/docs/man/borg-key-change-passphrase.1 b/docs/man/borg-key-change-passphrase.1 index 63a58b0c..86a30ebf 100644 --- a/docs/man/borg-key-change-passphrase.1 +++ b/docs/man/borg-key-change-passphrase.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-CHANGE-PASSPHRASE 1 "2017-02-05" "" "borg backup tool" +.TH BORG-KEY-CHANGE-PASSPHRASE 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-key-change-passphrase \- Change repository key file passphrase . diff --git a/docs/man/borg-key-export.1 b/docs/man/borg-key-export.1 index 3a815814..67202303 100644 --- a/docs/man/borg-key-export.1 +++ b/docs/man/borg-key-export.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-EXPORT 1 "2017-02-05" "" "borg backup tool" +.TH BORG-KEY-EXPORT 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-key-export \- Export the repository key for backup . diff --git a/docs/man/borg-key-import.1 b/docs/man/borg-key-import.1 index 7215df92..91ce569d 100644 --- a/docs/man/borg-key-import.1 +++ b/docs/man/borg-key-import.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-IMPORT 1 "2017-02-05" "" "borg backup tool" +.TH BORG-KEY-IMPORT 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-key-import \- Import the repository key from backup . diff --git a/docs/man/borg-key-migrate-to-repokey.1 b/docs/man/borg-key-migrate-to-repokey.1 index c1c5e95f..774c9199 100644 --- a/docs/man/borg-key-migrate-to-repokey.1 +++ b/docs/man/borg-key-migrate-to-repokey.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-MIGRATE-TO-REPOKEY 1 "2017-02-05" "" "borg backup tool" +.TH BORG-KEY-MIGRATE-TO-REPOKEY 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-key-migrate-to-repokey \- Migrate passphrase -> repokey . diff --git a/docs/man/borg-key.1 b/docs/man/borg-key.1 new file mode 100644 index 00000000..2d82f003 --- /dev/null +++ b/docs/man/borg-key.1 @@ -0,0 +1,47 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-KEY 1 "2017-02-11" "" "borg backup tool" +.SH NAME +borg-key \- Manage a keyfile or repokey of a repository +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.nf +borg key export ... +borg key import ... +borg key change\-passphrase ... +borg key migrate\-to\-repokey ... +.fi +.sp +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP, \fIborg\-key\-export(1)\fP, \fIborg\-key\-import(1)\fP, \fIborg\-key\-change\-passphrase(1)\fP, \fIborg\-key\-migrate\-to\-repokey(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-mount.1 b/docs/man/borg-mount.1 index e788bdba..63a12cee 100644 --- a/docs/man/borg-mount.1 +++ b/docs/man/borg-mount.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-MOUNT 1 "2017-02-05" "" "borg backup tool" +.TH BORG-MOUNT 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-mount \- Mount archive or an entire repository as a FUSE filesystem . diff --git a/docs/man/borg-patterns.1 b/docs/man/borg-patterns.1 index f814edc3..646694bf 100644 --- a/docs/man/borg-patterns.1 +++ b/docs/man/borg-patterns.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PATTERNS 1 "2017-02-05" "" "borg backup tool" +.TH BORG-PATTERNS 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-patterns \- Details regarding patterns . diff --git a/docs/man/borg-placeholders.1 b/docs/man/borg-placeholders.1 index bff600a4..49cfb6dc 100644 --- a/docs/man/borg-placeholders.1 +++ b/docs/man/borg-placeholders.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PLACEHOLDERS 1 "2017-02-05" "" "borg backup tool" +.TH BORG-PLACEHOLDERS 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-placeholders \- Details regarding placeholders . diff --git a/docs/man/borg-prune.1 b/docs/man/borg-prune.1 index 81756e66..ff8afa1d 100644 --- a/docs/man/borg-prune.1 +++ b/docs/man/borg-prune.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PRUNE 1 "2017-02-05" "" "borg backup tool" +.TH BORG-PRUNE 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-prune \- Prune repository archives according to specified rules . diff --git a/docs/man/borg-recreate.1 b/docs/man/borg-recreate.1 index 55b01cc8..db822491 100644 --- a/docs/man/borg-recreate.1 +++ b/docs/man/borg-recreate.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-RECREATE 1 "2017-02-05" "" "borg backup tool" +.TH BORG-RECREATE 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-recreate \- Re-create archives . diff --git a/docs/man/borg-rename.1 b/docs/man/borg-rename.1 index ee568f2f..e3c1ee77 100644 --- a/docs/man/borg-rename.1 +++ b/docs/man/borg-rename.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-RENAME 1 "2017-02-05" "" "borg backup tool" +.TH BORG-RENAME 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-rename \- Rename an existing archive . diff --git a/docs/man/borg-serve.1 b/docs/man/borg-serve.1 index 673ae269..ee62ee7f 100644 --- a/docs/man/borg-serve.1 +++ b/docs/man/borg-serve.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-SERVE 1 "2017-02-05" "" "borg backup tool" +.TH BORG-SERVE 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-serve \- Start in server mode. This command is usually not used manually. . diff --git a/docs/man/borg-umount.1 b/docs/man/borg-umount.1 index 4de127a5..7d8af587 100644 --- a/docs/man/borg-umount.1 +++ b/docs/man/borg-umount.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-UMOUNT 1 "2017-02-05" "" "borg backup tool" +.TH BORG-UMOUNT 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-umount \- un-mount the FUSE filesystem . diff --git a/docs/man/borg-upgrade.1 b/docs/man/borg-upgrade.1 index d329fa88..0c0df0d5 100644 --- a/docs/man/borg-upgrade.1 +++ b/docs/man/borg-upgrade.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-UPGRADE 1 "2017-02-05" "" "borg backup tool" +.TH BORG-UPGRADE 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-upgrade \- upgrade a repository from a previous version . diff --git a/docs/man/borg-with-lock.1 b/docs/man/borg-with-lock.1 index d6e19958..2db9c3fc 100644 --- a/docs/man/borg-with-lock.1 +++ b/docs/man/borg-with-lock.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-WITH-LOCK 1 "2017-02-05" "" "borg backup tool" +.TH BORG-WITH-LOCK 1 "2017-02-11" "" "borg backup tool" .SH NAME borg-with-lock \- run a user specified command with the repository lock held . diff --git a/docs/man/borg.1 b/docs/man/borg.1 new file mode 100644 index 00000000..b720d71c --- /dev/null +++ b/docs/man/borg.1 @@ -0,0 +1,567 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG 1 "2017-02-05" "" "borg backup tool" +.SH NAME +borg \- deduplicating and encrypting backup tool +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg [options] [arguments] +.SH DESCRIPTION +.\" we don't include the README.rst here since we want to keep this terse. +. +.sp +BorgBackup (short: Borg) is a deduplicating backup program. +Optionally, it supports compression and authenticated encryption. +.sp +The main goal of Borg is to provide an efficient and secure way to backup data. +The data deduplication technique used makes Borg suitable for daily backups +since only changes are stored. +The authenticated encryption technique makes it suitable for backups to not +fully trusted targets. +.sp +Borg stores a set of files in an \fIarchive\fP\&. A \fIrepository\fP is a collection +of \fIarchives\fP\&. The format of repositories is Borg\-specific. Borg does not +distinguish archives from each other in a any way other than their name, +it does not matter when or where archives where created (eg. different hosts). +.SH EXAMPLES +.SS A step\-by\-step example +.INDENT 0.0 +.IP 1. 3 +Before a backup can be made a repository has to be initialized: +.INDENT 3.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg init \-\-encryption=repokey /path/to/repo +.ft P +.fi +.UNINDENT +.UNINDENT +.IP 2. 3 +Backup the \fB~/src\fP and \fB~/Documents\fP directories into an archive called +\fIMonday\fP: +.INDENT 3.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg create /path/to/repo::Monday ~/src ~/Documents +.ft P +.fi +.UNINDENT +.UNINDENT +.IP 3. 3 +The next day create a new archive called \fITuesday\fP: +.INDENT 3.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg create \-\-stats /path/to/repo::Tuesday ~/src ~/Documents +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +This backup will be a lot quicker and a lot smaller since only new never +before seen data is stored. The \fB\-\-stats\fP option causes Borg to +output statistics about the newly created archive such as the amount of unique +data (not shared with other archives): +.INDENT 3.0 +.INDENT 3.5 +.sp +.nf +.ft C +\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- +Archive name: Tuesday +Archive fingerprint: bd31004d58f51ea06ff735d2e5ac49376901b21d58035f8fb05dbf866566e3c2 +Time (start): Tue, 2016\-02\-16 18:15:11 +Time (end): Tue, 2016\-02\-16 18:15:11 + +Duration: 0.19 seconds +Number of files: 127 +\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- + Original size Compressed size Deduplicated size +This archive: 4.16 MB 4.17 MB 26.78 kB +All archives: 8.33 MB 8.34 MB 4.19 MB + + Unique chunks Total chunks +Chunk index: 132 261 +\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- +.ft P +.fi +.UNINDENT +.UNINDENT +.IP 4. 3 +List all archives in the repository: +.INDENT 3.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg list /path/to/repo +Monday Mon, 2016\-02\-15 19:14:44 +Tuesday Tue, 2016\-02\-16 19:15:11 +.ft P +.fi +.UNINDENT +.UNINDENT +.IP 5. 3 +List the contents of the \fIMonday\fP archive: +.INDENT 3.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg list /path/to/repo::Monday +drwxr\-xr\-x user group 0 Mon, 2016\-02\-15 18:22:30 home/user/Documents +\-rw\-r\-\-r\-\- user group 7961 Mon, 2016\-02\-15 18:22:30 home/user/Documents/Important.doc +\&... +.ft P +.fi +.UNINDENT +.UNINDENT +.IP 6. 3 +Restore the \fIMonday\fP archive by extracting the files relative to the current directory: +.INDENT 3.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg extract /path/to/repo::Monday +.ft P +.fi +.UNINDENT +.UNINDENT +.IP 7. 3 +Recover disk space by manually deleting the \fIMonday\fP archive: +.INDENT 3.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ borg delete /path/to/repo::Monday +.ft P +.fi +.UNINDENT +.UNINDENT +.UNINDENT +.sp +\fBNOTE:\fP +.INDENT 0.0 +.INDENT 3.5 +Borg is quiet by default (it works on WARNING log level). +You can use options like \fB\-\-progress\fP or \fB\-\-list\fP to get specific +reports during command execution. You can also add the \fB\-v\fP (or +\fB\-\-verbose\fP or \fB\-\-info\fP) option to adjust the log level to INFO to +get other informational messages. +.UNINDENT +.UNINDENT +.SH NOTES +.SS Repository URLs +.sp +\fBLocal filesystem\fP (or locally mounted network filesystem): +.sp +\fB/path/to/repo\fP \- filesystem path to repo directory, absolute path +.sp +\fBpath/to/repo\fP \- filesystem path to repo directory, relative path +.sp +Also, stuff like \fB~/path/to/repo\fP or \fB~other/path/to/repo\fP works (this is +expanded by your shell). +.sp +Note: you may also prepend a \fBfile://\fP to a filesystem path to get URL style. +.sp +\fBRemote repositories\fP accessed via ssh \fI\%user@host\fP: +.sp +\fBuser@host:/path/to/repo\fP \- remote repo, absolute path +.sp +\fBssh://user@host:port/path/to/repo\fP \- same, alternative syntax, port can be given +.sp +\fBRemote repositories with relative pathes\fP can be given using this syntax: +.sp +\fBuser@host:path/to/repo\fP \- path relative to current directory +.sp +\fBuser@host:~/path/to/repo\fP \- path relative to user\(aqs home directory +.sp +\fBuser@host:~other/path/to/repo\fP \- path relative to other\(aqs home directory +.sp +Note: giving \fBuser@host:/./path/to/repo\fP or \fBuser@host:/~/path/to/repo\fP or +\fBuser@host:/~other/path/to/repo\fP is also supported, but not required here. +.sp +\fBRemote repositories with relative pathes, alternative syntax with port\fP: +.sp +\fBssh://user@host:port/./path/to/repo\fP \- path relative to current directory +.sp +\fBssh://user@host:port/~/path/to/repo\fP \- path relative to user\(aqs home directory +.sp +\fBssh://user@host:port/~other/path/to/repo\fP \- path relative to other\(aqs home directory +.sp +If you frequently need the same repo URL, it is a good idea to set the +\fBBORG_REPO\fP environment variable to set a default for the repo URL: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +export BORG_REPO=\(aqssh://user@host:port/path/to/repo\(aq +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Then just leave away the repo URL if only a repo URL is needed and you want +to use the default \- it will be read from BORG_REPO then. +.sp +Use \fB::\fP syntax to give the repo URL when syntax requires giving a positional +argument for the repo (e.g. \fBborg mount :: /mnt\fP). +.SS Repository / Archive Locations +.sp +Many commands want either a repository (just give the repo URL, see above) or +an archive location, which is a repo URL followed by \fB::archive_name\fP\&. +.sp +Archive names must not contain the \fB/\fP (slash) character. For simplicity, +maybe also avoid blanks or other characters that have special meaning on the +shell or in a filesystem (borg mount will use the archive name as directory +name). +.sp +If you have set BORG_REPO (see above) and an archive location is needed, use +\fB::archive_name\fP \- the repo URL part is then read from BORG_REPO. +.SS Type of log output +.sp +The log level of the builtin logging configuration defaults to WARNING. +This is because we want Borg to be mostly silent and only output +warnings, errors and critical messages, unless output has been requested +by supplying an option that implies output (eg, \-\-list or \-\-progress). +.sp +Log levels: DEBUG < INFO < WARNING < ERROR < CRITICAL +.sp +Use \fB\-\-debug\fP to set DEBUG log level \- +to get debug, info, warning, error and critical level output. +.sp +Use \fB\-\-info\fP (or \fB\-v\fP or \fB\-\-verbose\fP) to set INFO log level \- +to get info, warning, error and critical level output. +.sp +Use \fB\-\-warning\fP (default) to set WARNING log level \- +to get warning, error and critical level output. +.sp +Use \fB\-\-error\fP to set ERROR log level \- +to get error and critical level output. +.sp +Use \fB\-\-critical\fP to set CRITICAL log level \- +to get critical level output. +.sp +While you can set misc. log levels, do not expect that every command will +give different output on different log levels \- it\(aqs just a possibility. +.sp +\fBWARNING:\fP +.INDENT 0.0 +.INDENT 3.5 +Options \-\-critical and \-\-error are provided for completeness, +their usage is not recommended as you might miss important information. +.UNINDENT +.UNINDENT +.SS Return codes +.sp +Borg can exit with the following return codes (rc): +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +0 = success (logged as INFO) +1 = warning (operation reached its normal end, but there were warnings \- + you should check the log, logged as WARNING) +2 = error (like a fatal error, a local or remote exception, the operation + did not reach its normal end, logged as ERROR) +128+N = killed by signal N (e.g. 137 == kill \-9) +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +If you use \fB\-\-show\-rc\fP, the return code is also logged at the indicated +level as the last log entry. +.SS Environment Variables +.sp +Borg uses some environment variables for automation: +.INDENT 0.0 +.TP +.B General: +.INDENT 7.0 +.TP +.B BORG_REPO +When set, use the value to give the default repository location. If a command needs an archive +parameter, you can abbreviate as \fI::archive\fP\&. If a command needs a repository parameter, you +can either leave it away or abbreviate as \fI::\fP, if a positional parameter is required. +.TP +.B BORG_PASSPHRASE +When set, use the value to answer the passphrase question for encrypted repositories. +It is used when a passphrase is needed to access a encrypted repo as well as when a new +passphrase should be initially set when initializing an encrypted repo. +See also BORG_NEW_PASSPHRASE. +.TP +.B BORG_NEW_PASSPHRASE +When set, use the value to answer the passphrase question when a \fBnew\fP passphrase is asked for. +This variable is checked first. If it is not set, BORG_PASSPHRASE will be checked also. +Main usecase for this is to fully automate \fBborg change\-passphrase\fP\&. +.TP +.B BORG_DISPLAY_PASSPHRASE +When set, use the value to answer the "display the passphrase for verification" question when defining a new passphrase for encrypted repositories. +.TP +.B BORG_LOGGING_CONF +When set, use the given filename as \fI\%INI\fP\-style logging configuration. +.TP +.B BORG_RSH +When set, use this command instead of \fBssh\fP\&. This can be used to specify ssh options, such as +a custom identity file \fBssh \-i /path/to/private/key\fP\&. See \fBman ssh\fP for other options. +.TP +.B BORG_REMOTE_PATH +When set, use the given path/filename as remote path (default is "borg"). +Using \fB\-\-remote\-path PATH\fP commandline option overrides the environment variable. +.TP +.B BORG_FILES_CACHE_TTL +When set to a numeric value, this determines the maximum "time to live" for the files cache +entries (default: 20). The files cache is used to quickly determine whether a file is unchanged. +The FAQ explains this more detailed in: \fIalways_chunking\fP +.TP +.B TMPDIR +where temporary files are stored (might need a lot of temporary space for some operations) +.UNINDENT +.TP +.B Some automatic "answerers" (if set, they automatically answer confirmation questions): +.INDENT 7.0 +.TP +.B BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=no (or =yes) +For "Warning: Attempting to access a previously unknown unencrypted repository" +.TP +.B BORG_RELOCATED_REPO_ACCESS_IS_OK=no (or =yes) +For "Warning: The repository at location ... was previously located at ..." +.TP +.B BORG_CHECK_I_KNOW_WHAT_I_AM_DOING=NO (or =YES) +For "Warning: \(aqcheck \-\-repair\(aq is an experimental feature that might result in data loss." +.TP +.B BORG_DELETE_I_KNOW_WHAT_I_AM_DOING=NO (or =YES) +For "You requested to completely DELETE the repository \fIincluding\fP all archives it contains:" +.TP +.B BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING=NO (or =YES) +For "recreate is an experimental feature." +.UNINDENT +.sp +Note: answers are case sensitive. setting an invalid answer value might either give the default +answer or ask you interactively, depending on whether retries are allowed (they by default are +allowed). So please test your scripts interactively before making them a non\-interactive script. +.TP +.B Directories and files: +.INDENT 7.0 +.TP +.B BORG_KEYS_DIR +Default to \(aq~/.config/borg/keys\(aq. This directory contains keys for encrypted repositories. +.TP +.B BORG_KEY_FILE +When set, use the given filename as repository key file. +.TP +.B BORG_SECURITY_DIR +Default to \(aq~/.config/borg/security\(aq. This directory contains information borg uses to +track its usage of NONCES ("numbers used once" \- usually in encryption context) and other +security relevant data. +.TP +.B BORG_CACHE_DIR +Default to \(aq~/.cache/borg\(aq. This directory contains the local cache and might need a lot +of space for dealing with big repositories). +.UNINDENT +.TP +.B Building: +.INDENT 7.0 +.TP +.B BORG_OPENSSL_PREFIX +Adds given OpenSSL header file directory to the default locations (setup.py). +.TP +.B BORG_LZ4_PREFIX +Adds given LZ4 header file directory to the default locations (setup.py). +.TP +.B BORG_LIBB2_PREFIX +Adds given prefix directory to the default locations. If a \(aqinclude/blake2.h\(aq is found Borg +will be linked against the system libb2 instead of a bundled implementation. (setup.py) +.UNINDENT +.UNINDENT +.sp +Please note: +.INDENT 0.0 +.IP \(bu 2 +be very careful when using the "yes" sayers, the warnings with prompt exist for your / your data\(aqs security/safety +.IP \(bu 2 +also be very careful when putting your passphrase into a script, make sure it has appropriate file permissions +(e.g. mode 600, root:root). +.UNINDENT +.SS File systems +.sp +We strongly recommend against using Borg (or any other database\-like +software) on non\-journaling file systems like FAT, since it is not +possible to assume any consistency in case of power failures (or a +sudden disconnect of an external drive or similar failures). +.sp +While Borg uses a data store that is resilient against these failures +when used on journaling file systems, it is not possible to guarantee +this with some hardware \-\- independent of the software used. We don\(aqt +know a list of affected hardware. +.sp +If you are suspicious whether your Borg repository is still consistent +and readable after one of the failures mentioned above occured, run +\fBborg check \-\-verify\-data\fP to make sure it is consistent. +.SS Units +.sp +To display quantities, Borg takes care of respecting the +usual conventions of scale. Disk sizes are displayed in \fI\%decimal\fP, using powers of ten (so +\fBkB\fP means 1000 bytes). For memory usage, \fI\%binary prefixes\fP are used, and are +indicated using the \fI\%IEC binary prefixes\fP, +using powers of two (so \fBKiB\fP means 1024 bytes). +.SS Date and Time +.sp +We format date and time conforming to ISO\-8601, that is: YYYY\-MM\-DD and +HH:MM:SS (24h clock). +.sp +For more information about that, see: \fI\%https://xkcd.com/1179/\fP +.sp +Unless otherwise noted, we display local date and time. +Internally, we store and process date and time as UTC. +.SS Resource Usage +.sp +Borg might use a lot of resources depending on the size of the data set it is dealing with. +.sp +If one uses Borg in a client/server way (with a ssh: repository), +the resource usage occurs in part on the client and in another part on the +server. +.sp +If one uses Borg as a single process (with a filesystem repo), +all the resource usage occurs in that one process, so just add up client + +server to get the approximate resource usage. +.INDENT 0.0 +.TP +.B CPU client: +borg create: does chunking, hashing, compression, crypto (high CPU usage) +chunks cache sync: quite heavy on CPU, doing lots of hashtable operations. +borg extract: crypto, decompression (medium to high CPU usage) +borg check: similar to extract, but depends on options given. +borg prune / borg delete archive: low to medium CPU usage +borg delete repo: done on the server +It won\(aqt go beyond 100% of 1 core as the code is currently single\-threaded. +Especially higher zlib and lzma compression levels use significant amounts +of CPU cycles. Crypto might be cheap on the CPU (if hardware accelerated) or +expensive (if not). +.TP +.B CPU server: +It usually doesn\(aqt need much CPU, it just deals with the key/value store +(repository) and uses the repository index for that. +.sp +borg check: the repository check computes the checksums of all chunks +(medium CPU usage) +borg delete repo: low CPU usage +.TP +.B CPU (only for client/server operation): +When using borg in a client/server way with a \fI\%ssh:\-type\fP repo, the ssh +processes used for the transport layer will need some CPU on the client and +on the server due to the crypto they are doing \- esp. if you are pumping +big amounts of data. +.TP +.B Memory (RAM) client: +The chunks index and the files index are read into memory for performance +reasons. Might need big amounts of memory (see below). +Compression, esp. lzma compression with high levels might need substantial +amounts of memory. +.TP +.B Memory (RAM) server: +The server process will load the repository index into memory. Might need +considerable amounts of memory, but less than on the client (see below). +.TP +.B Chunks index (client only): +Proportional to the amount of data chunks in your repo. Lots of chunks +in your repo imply a big chunks index. +It is possible to tweak the chunker params (see create options). +.TP +.B Files index (client only): +Proportional to the amount of files in your last backups. Can be switched +off (see create options), but next backup might be much slower if you do. +The speed benefit of using the files cache is proportional to file size. +.TP +.B Repository index (server only): +Proportional to the amount of data chunks in your repo. Lots of chunks +in your repo imply a big repository index. +It is possible to tweak the chunker params (see create options) to +influence the amount of chunks being created. +.TP +.B Temporary files (client): +Reading data and metadata from a FUSE mounted repository will consume up to +the size of all deduplicated, small chunks in the repository. Big chunks +won\(aqt be locally cached. +.TP +.B Temporary files (server): +None. +.TP +.B Cache files (client only): +Contains the chunks index and files index (plus a collection of single\- +archive chunk indexes which might need huge amounts of disk space, +depending on archive count and size \- see FAQ about how to reduce). +.TP +.B Network (only for client/server operation): +If your repository is remote, all deduplicated (and optionally compressed/ +encrypted) data of course has to go over the connection (ssh: repo url). +If you use a locally mounted network filesystem, additionally some copy +operations used for transaction support also go over the connection. If +you backup multiple sources to one target repository, additional traffic +happens for cache resynchronization. +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP for common command line options +.sp +\fIborg\-init(1)\fP, +\fIborg\-create(1)\fP, \fIborg\-mount(1)\fP, \fIborg\-extract(1)\fP, +\fIborg\-list(1)\fP, \fIborg\-info(1)\fP, +\fIborg\-delete(1)\fP, \fIborg\-prune(1)\fP, +\fIborg\-recreate(1)\fP +.sp +\fIborg\-compression(1)\fP, \fIborg\-patterns(1)\fP, \fIborg\-placeholders(1)\fP +.INDENT 0.0 +.IP \(bu 2 +Main web site \fI\%https://borgbackup.readthedocs.org/\fP +.IP \(bu 2 +Releases \fI\%https://github.com/borgbackup/borg/releases\fP +.IP \(bu 2 +Changelog \fI\%https://github.com/borgbackup/borg/blob/master/docs/changes.rst\fP +.IP \(bu 2 +GitHub \fI\%https://github.com/borgbackup/borg\fP +.IP \(bu 2 +Security contact \fI\%https://borgbackup.readthedocs.io/en/latest/support.html#security\-contact\fP +.UNINDENT +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man_intro.rst b/docs/man_intro.rst new file mode 100644 index 00000000..0e08f5a9 --- /dev/null +++ b/docs/man_intro.rst @@ -0,0 +1,68 @@ +==== +borg +==== + +---------------------------------------- +deduplicating and encrypting backup tool +---------------------------------------- + +:Author: The Borg Collective +:Date: 2017-02-05 +:Manual section: 1 +:Manual group: borg backup tool + +SYNOPSIS +-------- + +borg [options] [arguments] + +DESCRIPTION +----------- + +.. we don't include the README.rst here since we want to keep this terse. + +BorgBackup (short: Borg) is a deduplicating backup program. +Optionally, it supports compression and authenticated encryption. + +The main goal of Borg is to provide an efficient and secure way to backup data. +The data deduplication technique used makes Borg suitable for daily backups +since only changes are stored. +The authenticated encryption technique makes it suitable for backups to not +fully trusted targets. + +Borg stores a set of files in an *archive*. A *repository* is a collection +of *archives*. The format of repositories is Borg-specific. Borg does not +distinguish archives from each other in a any way other than their name, +it does not matter when or where archives where created (eg. different hosts). + +EXAMPLES +-------- + +A step-by-step example +~~~~~~~~~~~~~~~~~~~~~~ + +.. include:: quickstart_example.rst.inc + +NOTES +----- + +.. include:: usage/general.rst.inc + +SEE ALSO +-------- + +`borg-common(1)` for common command line options + +`borg-init(1)`, +`borg-create(1)`, `borg-mount(1)`, `borg-extract(1)`, +`borg-list(1)`, `borg-info(1)`, +`borg-delete(1)`, `borg-prune(1)`, +`borg-recreate(1)` + +`borg-compression(1)`, `borg-patterns(1)`, `borg-placeholders(1)` + +* Main web site https://borgbackup.readthedocs.org/ +* Releases https://github.com/borgbackup/borg/releases +* Changelog https://github.com/borgbackup/borg/blob/master/docs/changes.rst +* GitHub https://github.com/borgbackup/borg +* Security contact https://borgbackup.readthedocs.io/en/latest/support.html#security-contact diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f8835f29..32770c2c 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -11,68 +11,7 @@ various use cases. A step by step example ---------------------- -1. Before a backup can be made a repository has to be initialized:: - - $ borg init /path/to/repo - -2. Backup the ``~/src`` and ``~/Documents`` directories into an archive called - *Monday*:: - - $ borg create /path/to/repo::Monday ~/src ~/Documents - -3. The next day create a new archive called *Tuesday*:: - - $ borg create --stats /path/to/repo::Tuesday ~/src ~/Documents - - This backup will be a lot quicker and a lot smaller since only new never - before seen data is stored. The ``--stats`` option causes |project_name| to - output statistics about the newly created archive such as the amount of unique - data (not shared with other archives):: - - ------------------------------------------------------------------------------ - Archive name: Tuesday - Archive fingerprint: bd31004d58f51ea06ff735d2e5ac49376901b21d58035f8fb05dbf866566e3c2 - Time (start): Tue, 2016-02-16 18:15:11 - Time (end): Tue, 2016-02-16 18:15:11 - - Duration: 0.19 seconds - Number of files: 127 - ------------------------------------------------------------------------------ - Original size Compressed size Deduplicated size - This archive: 4.16 MB 4.17 MB 26.78 kB - All archives: 8.33 MB 8.34 MB 4.19 MB - - Unique chunks Total chunks - Chunk index: 132 261 - ------------------------------------------------------------------------------ - -4. List all archives in the repository:: - - $ borg list /path/to/repo - Monday Mon, 2016-02-15 19:14:44 - Tuesday Tue, 2016-02-16 19:15:11 - -5. List the contents of the *Monday* archive:: - - $ borg list /path/to/repo::Monday - drwxr-xr-x user group 0 Mon, 2016-02-15 18:22:30 home/user/Documents - -rw-r--r-- user group 7961 Mon, 2016-02-15 18:22:30 home/user/Documents/Important.doc - ... - -6. Restore the *Monday* archive by extracting the files relative to the current directory:: - - $ borg extract /path/to/repo::Monday - -7. Recover disk space by manually deleting the *Monday* archive:: - - $ borg delete /path/to/repo::Monday - -.. Note:: - Borg is quiet by default (it works on WARNING log level). - You can use options like ``--progress`` or ``--list`` to get specific - reports during command execution. You can also add the ``-v`` (or - ``--verbose`` or ``--info``) option to adjust the log level to INFO to - get other informational messages. +.. include:: quickstart_example.rst.inc Important note about free space ------------------------------- diff --git a/docs/quickstart_example.rst.inc b/docs/quickstart_example.rst.inc new file mode 100644 index 00000000..69ed9845 --- /dev/null +++ b/docs/quickstart_example.rst.inc @@ -0,0 +1,62 @@ +1. Before a backup can be made a repository has to be initialized:: + + $ borg init --encryption=repokey /path/to/repo + +2. Backup the ``~/src`` and ``~/Documents`` directories into an archive called + *Monday*:: + + $ borg create /path/to/repo::Monday ~/src ~/Documents + +3. The next day create a new archive called *Tuesday*:: + + $ borg create --stats /path/to/repo::Tuesday ~/src ~/Documents + + This backup will be a lot quicker and a lot smaller since only new never + before seen data is stored. The ``--stats`` option causes Borg to + output statistics about the newly created archive such as the amount of unique + data (not shared with other archives):: + + ------------------------------------------------------------------------------ + Archive name: Tuesday + Archive fingerprint: bd31004d58f51ea06ff735d2e5ac49376901b21d58035f8fb05dbf866566e3c2 + Time (start): Tue, 2016-02-16 18:15:11 + Time (end): Tue, 2016-02-16 18:15:11 + + Duration: 0.19 seconds + Number of files: 127 + ------------------------------------------------------------------------------ + Original size Compressed size Deduplicated size + This archive: 4.16 MB 4.17 MB 26.78 kB + All archives: 8.33 MB 8.34 MB 4.19 MB + + Unique chunks Total chunks + Chunk index: 132 261 + ------------------------------------------------------------------------------ + +4. List all archives in the repository:: + + $ borg list /path/to/repo + Monday Mon, 2016-02-15 19:14:44 + Tuesday Tue, 2016-02-16 19:15:11 + +5. List the contents of the *Monday* archive:: + + $ borg list /path/to/repo::Monday + drwxr-xr-x user group 0 Mon, 2016-02-15 18:22:30 home/user/Documents + -rw-r--r-- user group 7961 Mon, 2016-02-15 18:22:30 home/user/Documents/Important.doc + ... + +6. Restore the *Monday* archive by extracting the files relative to the current directory:: + + $ borg extract /path/to/repo::Monday + +7. Recover disk space by manually deleting the *Monday* archive:: + + $ borg delete /path/to/repo::Monday + +.. Note:: + Borg is quiet by default (it works on WARNING log level). + You can use options like ``--progress`` or ``--list`` to get specific + reports during command execution. You can also add the ``-v`` (or + ``--verbose`` or ``--info``) option to adjust the log level to INFO to + get other informational messages. diff --git a/docs/usage.rst b/docs/usage.rst index b00f157b..cd43c65e 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -12,342 +12,13 @@ command in detail. General ------- -Repository URLs -~~~~~~~~~~~~~~~ - -**Local filesystem** (or locally mounted network filesystem): - -``/path/to/repo`` - filesystem path to repo directory, absolute path - -``path/to/repo`` - filesystem path to repo directory, relative path - -Also, stuff like ``~/path/to/repo`` or ``~other/path/to/repo`` works (this is -expanded by your shell). - -Note: you may also prepend a ``file://`` to a filesystem path to get URL style. - -**Remote repositories** accessed via ssh user@host: - -``user@host:/path/to/repo`` - remote repo, absolute path - -``ssh://user@host:port/path/to/repo`` - same, alternative syntax, port can be given - - -**Remote repositories with relative pathes** can be given using this syntax: - -``user@host:path/to/repo`` - path relative to current directory - -``user@host:~/path/to/repo`` - path relative to user's home directory - -``user@host:~other/path/to/repo`` - path relative to other's home directory - -Note: giving ``user@host:/./path/to/repo`` or ``user@host:/~/path/to/repo`` or -``user@host:/~other/path/to/repo`` is also supported, but not required here. - - -**Remote repositories with relative pathes, alternative syntax with port**: - -``ssh://user@host:port/./path/to/repo`` - path relative to current directory - -``ssh://user@host:port/~/path/to/repo`` - path relative to user's home directory - -``ssh://user@host:port/~other/path/to/repo`` - path relative to other's home directory - - -If you frequently need the same repo URL, it is a good idea to set the -``BORG_REPO`` environment variable to set a default for the repo URL: - -:: - - export BORG_REPO='ssh://user@host:port/path/to/repo' - -Then just leave away the repo URL if only a repo URL is needed and you want -to use the default - it will be read from BORG_REPO then. - -Use ``::`` syntax to give the repo URL when syntax requires giving a positional -argument for the repo (e.g. ``borg mount :: /mnt``). - - -Repository / Archive Locations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Many commands want either a repository (just give the repo URL, see above) or -an archive location, which is a repo URL followed by ``::archive_name``. - -Archive names must not contain the ``/`` (slash) character. For simplicity, -maybe also avoid blanks or other characters that have special meaning on the -shell or in a filesystem (borg mount will use the archive name as directory -name). - -If you have set BORG_REPO (see above) and an archive location is needed, use -``::archive_name`` - the repo URL part is then read from BORG_REPO. - - -Type of log output -~~~~~~~~~~~~~~~~~~ - -The log level of the builtin logging configuration defaults to WARNING. -This is because we want |project_name| to be mostly silent and only output -warnings, errors and critical messages, unless output has been requested -by supplying an option that implies output (eg, --list or --progress). - -Log levels: DEBUG < INFO < WARNING < ERROR < CRITICAL - -Use ``--debug`` to set DEBUG log level - -to get debug, info, warning, error and critical level output. - -Use ``--info`` (or ``-v`` or ``--verbose``) to set INFO log level - -to get info, warning, error and critical level output. - -Use ``--warning`` (default) to set WARNING log level - -to get warning, error and critical level output. - -Use ``--error`` to set ERROR log level - -to get error and critical level output. - -Use ``--critical`` to set CRITICAL log level - -to get critical level output. - -While you can set misc. log levels, do not expect that every command will -give different output on different log levels - it's just a possibility. - -.. warning:: Options --critical and --error are provided for completeness, - their usage is not recommended as you might miss important information. - -Return codes -~~~~~~~~~~~~ - -|project_name| can exit with the following return codes (rc): - -:: - - 0 = success (logged as INFO) - 1 = warning (operation reached its normal end, but there were warnings - - you should check the log, logged as WARNING) - 2 = error (like a fatal error, a local or remote exception, the operation - did not reach its normal end, logged as ERROR) - 128+N = killed by signal N (e.g. 137 == kill -9) - -If you use ``--show-rc``, the return code is also logged at the indicated -level as the last log entry. - - -Environment Variables -~~~~~~~~~~~~~~~~~~~~~ - -|project_name| uses some environment variables for automation: - -General: - BORG_REPO - When set, use the value to give the default repository location. If a command needs an archive - parameter, you can abbreviate as `::archive`. If a command needs a repository parameter, you - can either leave it away or abbreviate as `::`, if a positional parameter is required. - BORG_PASSPHRASE - When set, use the value to answer the passphrase question for encrypted repositories. - It is used when a passphrase is needed to access a encrypted repo as well as when a new - passphrase should be initially set when initializing an encrypted repo. - See also BORG_NEW_PASSPHRASE. - BORG_NEW_PASSPHRASE - When set, use the value to answer the passphrase question when a **new** passphrase is asked for. - This variable is checked first. If it is not set, BORG_PASSPHRASE will be checked also. - Main usecase for this is to fully automate ``borg change-passphrase``. - BORG_DISPLAY_PASSPHRASE - When set, use the value to answer the "display the passphrase for verification" question when defining a new passphrase for encrypted repositories. - BORG_LOGGING_CONF - When set, use the given filename as INI_-style logging configuration. - BORG_RSH - When set, use this command instead of ``ssh``. This can be used to specify ssh options, such as - a custom identity file ``ssh -i /path/to/private/key``. See ``man ssh`` for other options. - BORG_REMOTE_PATH - When set, use the given path/filename as remote path (default is "borg"). - Using ``--remote-path PATH`` commandline option overrides the environment variable. - BORG_FILES_CACHE_TTL - When set to a numeric value, this determines the maximum "time to live" for the files cache - entries (default: 20). The files cache is used to quickly determine whether a file is unchanged. - The FAQ explains this more detailed in: :ref:`always_chunking` - TMPDIR - where temporary files are stored (might need a lot of temporary space for some operations) - -Some automatic "answerers" (if set, they automatically answer confirmation questions): - BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=no (or =yes) - For "Warning: Attempting to access a previously unknown unencrypted repository" - BORG_RELOCATED_REPO_ACCESS_IS_OK=no (or =yes) - For "Warning: The repository at location ... was previously located at ..." - BORG_CHECK_I_KNOW_WHAT_I_AM_DOING=NO (or =YES) - For "Warning: 'check --repair' is an experimental feature that might result in data loss." - BORG_DELETE_I_KNOW_WHAT_I_AM_DOING=NO (or =YES) - For "You requested to completely DELETE the repository *including* all archives it contains:" - BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING=NO (or =YES) - For "recreate is an experimental feature." - - Note: answers are case sensitive. setting an invalid answer value might either give the default - answer or ask you interactively, depending on whether retries are allowed (they by default are - allowed). So please test your scripts interactively before making them a non-interactive script. - -Directories and files: - BORG_KEYS_DIR - Default to '~/.config/borg/keys'. This directory contains keys for encrypted repositories. - BORG_KEY_FILE - When set, use the given filename as repository key file. - BORG_SECURITY_DIR - Default to '~/.config/borg/security'. This directory contains information borg uses to - track its usage of NONCES ("numbers used once" - usually in encryption context) and other - security relevant data. - BORG_CACHE_DIR - Default to '~/.cache/borg'. This directory contains the local cache and might need a lot - of space for dealing with big repositories). - -Building: - BORG_OPENSSL_PREFIX - Adds given OpenSSL header file directory to the default locations (setup.py). - BORG_LZ4_PREFIX - Adds given LZ4 header file directory to the default locations (setup.py). - BORG_LIBB2_PREFIX - Adds given prefix directory to the default locations. If a 'include/blake2.h' is found Borg - will be linked against the system libb2 instead of a bundled implementation. (setup.py) - - -Please note: - -- be very careful when using the "yes" sayers, the warnings with prompt exist for your / your data's security/safety -- also be very careful when putting your passphrase into a script, make sure it has appropriate file permissions - (e.g. mode 600, root:root). - - -.. _INI: https://docs.python.org/3.4/library/logging.config.html#configuration-file-format - -Resource Usage -~~~~~~~~~~~~~~ - -|project_name| might use a lot of resources depending on the size of the data set it is dealing with. - -If one uses |project_name| in a client/server way (with a ssh: repository), -the resource usage occurs in part on the client and in another part on the -server. - -If one uses |project_name| as a single process (with a filesystem repo), -all the resource usage occurs in that one process, so just add up client + -server to get the approximate resource usage. - -CPU client: - borg create: does chunking, hashing, compression, crypto (high CPU usage) - chunks cache sync: quite heavy on CPU, doing lots of hashtable operations. - borg extract: crypto, decompression (medium to high CPU usage) - borg check: similar to extract, but depends on options given. - borg prune / borg delete archive: low to medium CPU usage - borg delete repo: done on the server - It won't go beyond 100% of 1 core as the code is currently single-threaded. - Especially higher zlib and lzma compression levels use significant amounts - of CPU cycles. Crypto might be cheap on the CPU (if hardware accelerated) or - expensive (if not). - -CPU server: - It usually doesn't need much CPU, it just deals with the key/value store - (repository) and uses the repository index for that. - - borg check: the repository check computes the checksums of all chunks - (medium CPU usage) - borg delete repo: low CPU usage - -CPU (only for client/server operation): - When using borg in a client/server way with a ssh:-type repo, the ssh - processes used for the transport layer will need some CPU on the client and - on the server due to the crypto they are doing - esp. if you are pumping - big amounts of data. - -Memory (RAM) client: - The chunks index and the files index are read into memory for performance - reasons. Might need big amounts of memory (see below). - Compression, esp. lzma compression with high levels might need substantial - amounts of memory. - -Memory (RAM) server: - The server process will load the repository index into memory. Might need - considerable amounts of memory, but less than on the client (see below). - -Chunks index (client only): - Proportional to the amount of data chunks in your repo. Lots of chunks - in your repo imply a big chunks index. - It is possible to tweak the chunker params (see create options). - -Files index (client only): - Proportional to the amount of files in your last backups. Can be switched - off (see create options), but next backup might be much slower if you do. - The speed benefit of using the files cache is proportional to file size. - -Repository index (server only): - Proportional to the amount of data chunks in your repo. Lots of chunks - in your repo imply a big repository index. - It is possible to tweak the chunker params (see create options) to - influence the amount of chunks being created. - -Temporary files (client): - Reading data and metadata from a FUSE mounted repository will consume up to - the size of all deduplicated, small chunks in the repository. Big chunks - won't be locally cached. - -Temporary files (server): - None. - -Cache files (client only): - Contains the chunks index and files index (plus a collection of single- - archive chunk indexes which might need huge amounts of disk space, - depending on archive count and size - see FAQ about how to reduce). - -Network (only for client/server operation): - If your repository is remote, all deduplicated (and optionally compressed/ - encrypted) data of course has to go over the connection (ssh: repo url). - If you use a locally mounted network filesystem, additionally some copy - operations used for transaction support also go over the connection. If - you backup multiple sources to one target repository, additional traffic - happens for cache resynchronization. +.. include:: usage/general.rst.inc In case you are interested in more details (like formulas), please see :ref:`internals`. -File systems -~~~~~~~~~~~~ - -We strongly recommend against using Borg (or any other database-like -software) on non-journaling file systems like FAT, since it is not -possible to assume any consistency in case of power failures (or a -sudden disconnect of an external drive or similar failures). - -While Borg uses a data store that is resilient against these failures -when used on journaling file systems, it is not possible to guarantee -this with some hardware -- independent of the software used. We don't -know a list of affected hardware. - -If you are suspicious whether your Borg repository is still consistent -and readable after one of the failures mentioned above occured, run -``borg check --verify-data`` to make sure it is consistent. - -Units -~~~~~ - -To display quantities, |project_name| takes care of respecting the -usual conventions of scale. Disk sizes are displayed in `decimal -`_, using powers of ten (so -``kB`` means 1000 bytes). For memory usage, `binary prefixes -`_ are used, and are -indicated using the `IEC binary prefixes -`_, -using powers of two (so ``KiB`` means 1024 bytes). - - -Date and Time -~~~~~~~~~~~~~ - -We format date and time conforming to ISO-8601, that is: YYYY-MM-DD and -HH:MM:SS (24h clock). - -For more information about that, see: https://xkcd.com/1179/ - -Unless otherwise noted, we display local date and time. -Internally, we store and process date and time as UTC. - Common options -~~~~~~~~~~~~~~ +++++++++++++++ All |project_name| commands share these options: @@ -636,6 +307,7 @@ Examples Examples ~~~~~~~~ + borg mount ++++++++++ :: @@ -845,6 +517,7 @@ Here are misc. notes about topics that are maybe not covered in enough detail in --chunker-params ~~~~~~~~~~~~~~~~ + The chunker params influence how input files are cut into pieces (chunks) which are then considered for deduplication. They also have a big impact on resource usage (RAM and disk space) as the amount of resources needed is diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 83c8810e..6b34278d 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -36,10 +36,10 @@ Exclusion options | read exclude patterns from EXCLUDEFILE, one per line ``--exclude-caches`` | exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) - ``--exclude-if-present FILENAME`` - | exclude directories that contain the specified file - ``--keep-tag-files`` - | keep tag files of excluded caches/directories + ``--exclude-if-present NAME`` + | exclude directories that are tagged by containing a filesystem object with the given NAME + ``--keep-exclude-tags``, ``--keep-tag-files`` + | keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise excluded caches/directories Filesystem options ``-x``, ``--one-file-system`` @@ -58,12 +58,12 @@ Filesystem options Archive options ``--comment COMMENT`` | add a comment text to the archive - ``--timestamp yyyy-mm-ddThh:mm:ss`` - | manually specify the archive creation date/time (UTC). alternatively, give a reference file/directory. + ``--timestamp TIMESTAMP`` + | manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). alternatively, give a reference file/directory. ``-c SECONDS``, ``--checkpoint-interval SECONDS`` | write checkpoint every SECONDS seconds (Default: 1800) - ``--chunker-params CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE`` - | specify the chunker parameters. default: 19,23,21,4095 + ``--chunker-params PARAMS`` + | specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: 19,23,21,4095 ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the "borg help compression" command for details. ``--compression-from COMPRESSIONCONFIG`` @@ -94,3 +94,49 @@ all files on these file systems. See the output of the "borg help patterns" command for more help on exclude patterns. See the output of the "borg help placeholders" command for more help on placeholders. + +.. man NOTES + +The --exclude patterns are not like tar. In tar --exclude .bundler/gems will +exclude foo/.bundler/gems. In borg it will not, you need to use --exclude +'\*/.bundler/gems' to get the same effect. See ``borg help patterns`` for +more information. + +Item flags +++++++++++ + +``--list`` outputs a list of all files, directories and other +file system items it considered (no matter whether they had content changes +or not). For each item, it prefixes a single-letter flag that indicates type +and/or status of the item. + +If you are interested only in a subset of that output, you can give e.g. +``--filter=AME`` and it will only show regular files with A, M or E status (see +below). + +A uppercase character represents the status of a regular file relative to the +"files" cache (not relative to the repo -- this is an issue if the files cache +is not used). Metadata is stored in any case and for 'A' and 'M' also new data +chunks are stored. For 'U' all data chunks refer to already existing chunks. + +- 'A' = regular file, added (see also :ref:`a_status_oddity` in the FAQ) +- 'M' = regular file, modified +- 'U' = regular file, unchanged +- 'E' = regular file, an error happened while accessing/reading *this* file + +A lowercase character means a file type other than a regular file, +borg usually just stores their metadata: + +- 'd' = directory +- 'b' = block device +- 'c' = char device +- 'h' = regular file, hardlink (to already seen inodes) +- 's' = symlink +- 'f' = fifo + +Other flags used include: + +- 'i' = backup data was read from standard input (stdin) +- '-' = dry run, item was *not* backed up +- 'x' = excluded, item was *not* backed up +- '?' = missing status code (if you see this, please file a bug report!) \ No newline at end of file diff --git a/docs/usage/general.rst.inc b/docs/usage/general.rst.inc new file mode 100644 index 00000000..7cbe7cae --- /dev/null +++ b/docs/usage/general.rst.inc @@ -0,0 +1,329 @@ +Repository URLs +~~~~~~~~~~~~~~~ + +**Local filesystem** (or locally mounted network filesystem): + +``/path/to/repo`` - filesystem path to repo directory, absolute path + +``path/to/repo`` - filesystem path to repo directory, relative path + +Also, stuff like ``~/path/to/repo`` or ``~other/path/to/repo`` works (this is +expanded by your shell). + +Note: you may also prepend a ``file://`` to a filesystem path to get URL style. + +**Remote repositories** accessed via ssh user@host: + +``user@host:/path/to/repo`` - remote repo, absolute path + +``ssh://user@host:port/path/to/repo`` - same, alternative syntax, port can be given + + +**Remote repositories with relative pathes** can be given using this syntax: + +``user@host:path/to/repo`` - path relative to current directory + +``user@host:~/path/to/repo`` - path relative to user's home directory + +``user@host:~other/path/to/repo`` - path relative to other's home directory + +Note: giving ``user@host:/./path/to/repo`` or ``user@host:/~/path/to/repo`` or +``user@host:/~other/path/to/repo`` is also supported, but not required here. + + +**Remote repositories with relative pathes, alternative syntax with port**: + +``ssh://user@host:port/./path/to/repo`` - path relative to current directory + +``ssh://user@host:port/~/path/to/repo`` - path relative to user's home directory + +``ssh://user@host:port/~other/path/to/repo`` - path relative to other's home directory + + +If you frequently need the same repo URL, it is a good idea to set the +``BORG_REPO`` environment variable to set a default for the repo URL: + +:: + + export BORG_REPO='ssh://user@host:port/path/to/repo' + +Then just leave away the repo URL if only a repo URL is needed and you want +to use the default - it will be read from BORG_REPO then. + +Use ``::`` syntax to give the repo URL when syntax requires giving a positional +argument for the repo (e.g. ``borg mount :: /mnt``). + + +Repository / Archive Locations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Many commands want either a repository (just give the repo URL, see above) or +an archive location, which is a repo URL followed by ``::archive_name``. + +Archive names must not contain the ``/`` (slash) character. For simplicity, +maybe also avoid blanks or other characters that have special meaning on the +shell or in a filesystem (borg mount will use the archive name as directory +name). + +If you have set BORG_REPO (see above) and an archive location is needed, use +``::archive_name`` - the repo URL part is then read from BORG_REPO. + + +Type of log output +~~~~~~~~~~~~~~~~~~ + +The log level of the builtin logging configuration defaults to WARNING. +This is because we want Borg to be mostly silent and only output +warnings, errors and critical messages, unless output has been requested +by supplying an option that implies output (eg, --list or --progress). + +Log levels: DEBUG < INFO < WARNING < ERROR < CRITICAL + +Use ``--debug`` to set DEBUG log level - +to get debug, info, warning, error and critical level output. + +Use ``--info`` (or ``-v`` or ``--verbose``) to set INFO log level - +to get info, warning, error and critical level output. + +Use ``--warning`` (default) to set WARNING log level - +to get warning, error and critical level output. + +Use ``--error`` to set ERROR log level - +to get error and critical level output. + +Use ``--critical`` to set CRITICAL log level - +to get critical level output. + +While you can set misc. log levels, do not expect that every command will +give different output on different log levels - it's just a possibility. + +.. warning:: Options --critical and --error are provided for completeness, + their usage is not recommended as you might miss important information. + +Return codes +~~~~~~~~~~~~ + +Borg can exit with the following return codes (rc): + +:: + + 0 = success (logged as INFO) + 1 = warning (operation reached its normal end, but there were warnings - + you should check the log, logged as WARNING) + 2 = error (like a fatal error, a local or remote exception, the operation + did not reach its normal end, logged as ERROR) + 128+N = killed by signal N (e.g. 137 == kill -9) + +If you use ``--show-rc``, the return code is also logged at the indicated +level as the last log entry. + + +Environment Variables +~~~~~~~~~~~~~~~~~~~~~ + +Borg uses some environment variables for automation: + +General: + BORG_REPO + When set, use the value to give the default repository location. If a command needs an archive + parameter, you can abbreviate as `::archive`. If a command needs a repository parameter, you + can either leave it away or abbreviate as `::`, if a positional parameter is required. + BORG_PASSPHRASE + When set, use the value to answer the passphrase question for encrypted repositories. + It is used when a passphrase is needed to access a encrypted repo as well as when a new + passphrase should be initially set when initializing an encrypted repo. + See also BORG_NEW_PASSPHRASE. + BORG_NEW_PASSPHRASE + When set, use the value to answer the passphrase question when a **new** passphrase is asked for. + This variable is checked first. If it is not set, BORG_PASSPHRASE will be checked also. + Main usecase for this is to fully automate ``borg change-passphrase``. + BORG_DISPLAY_PASSPHRASE + When set, use the value to answer the "display the passphrase for verification" question when defining a new passphrase for encrypted repositories. + BORG_LOGGING_CONF + When set, use the given filename as INI_-style logging configuration. + BORG_RSH + When set, use this command instead of ``ssh``. This can be used to specify ssh options, such as + a custom identity file ``ssh -i /path/to/private/key``. See ``man ssh`` for other options. + BORG_REMOTE_PATH + When set, use the given path/filename as remote path (default is "borg"). + Using ``--remote-path PATH`` commandline option overrides the environment variable. + BORG_FILES_CACHE_TTL + When set to a numeric value, this determines the maximum "time to live" for the files cache + entries (default: 20). The files cache is used to quickly determine whether a file is unchanged. + The FAQ explains this more detailed in: :ref:`always_chunking` + TMPDIR + where temporary files are stored (might need a lot of temporary space for some operations) + +Some automatic "answerers" (if set, they automatically answer confirmation questions): + BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=no (or =yes) + For "Warning: Attempting to access a previously unknown unencrypted repository" + BORG_RELOCATED_REPO_ACCESS_IS_OK=no (or =yes) + For "Warning: The repository at location ... was previously located at ..." + BORG_CHECK_I_KNOW_WHAT_I_AM_DOING=NO (or =YES) + For "Warning: 'check --repair' is an experimental feature that might result in data loss." + BORG_DELETE_I_KNOW_WHAT_I_AM_DOING=NO (or =YES) + For "You requested to completely DELETE the repository *including* all archives it contains:" + BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING=NO (or =YES) + For "recreate is an experimental feature." + + Note: answers are case sensitive. setting an invalid answer value might either give the default + answer or ask you interactively, depending on whether retries are allowed (they by default are + allowed). So please test your scripts interactively before making them a non-interactive script. + +Directories and files: + BORG_KEYS_DIR + Default to '~/.config/borg/keys'. This directory contains keys for encrypted repositories. + BORG_KEY_FILE + When set, use the given filename as repository key file. + BORG_SECURITY_DIR + Default to '~/.config/borg/security'. This directory contains information borg uses to + track its usage of NONCES ("numbers used once" - usually in encryption context) and other + security relevant data. + BORG_CACHE_DIR + Default to '~/.cache/borg'. This directory contains the local cache and might need a lot + of space for dealing with big repositories). + +Building: + BORG_OPENSSL_PREFIX + Adds given OpenSSL header file directory to the default locations (setup.py). + BORG_LZ4_PREFIX + Adds given LZ4 header file directory to the default locations (setup.py). + BORG_LIBB2_PREFIX + Adds given prefix directory to the default locations. If a 'include/blake2.h' is found Borg + will be linked against the system libb2 instead of a bundled implementation. (setup.py) + + +Please note: + +- be very careful when using the "yes" sayers, the warnings with prompt exist for your / your data's security/safety +- also be very careful when putting your passphrase into a script, make sure it has appropriate file permissions + (e.g. mode 600, root:root). + + +.. _INI: https://docs.python.org/3.4/library/logging.config.html#configuration-file-format + +File systems +~~~~~~~~~~~~ + +We strongly recommend against using Borg (or any other database-like +software) on non-journaling file systems like FAT, since it is not +possible to assume any consistency in case of power failures (or a +sudden disconnect of an external drive or similar failures). + +While Borg uses a data store that is resilient against these failures +when used on journaling file systems, it is not possible to guarantee +this with some hardware -- independent of the software used. We don't +know a list of affected hardware. + +If you are suspicious whether your Borg repository is still consistent +and readable after one of the failures mentioned above occured, run +``borg check --verify-data`` to make sure it is consistent. + +Units +~~~~~ + +To display quantities, Borg takes care of respecting the +usual conventions of scale. Disk sizes are displayed in `decimal +`_, using powers of ten (so +``kB`` means 1000 bytes). For memory usage, `binary prefixes +`_ are used, and are +indicated using the `IEC binary prefixes +`_, +using powers of two (so ``KiB`` means 1024 bytes). + +Date and Time +~~~~~~~~~~~~~ + +We format date and time conforming to ISO-8601, that is: YYYY-MM-DD and +HH:MM:SS (24h clock). + +For more information about that, see: https://xkcd.com/1179/ + +Unless otherwise noted, we display local date and time. +Internally, we store and process date and time as UTC. + +Resource Usage +~~~~~~~~~~~~~~ + +Borg might use a lot of resources depending on the size of the data set it is dealing with. + +If one uses Borg in a client/server way (with a ssh: repository), +the resource usage occurs in part on the client and in another part on the +server. + +If one uses Borg as a single process (with a filesystem repo), +all the resource usage occurs in that one process, so just add up client + +server to get the approximate resource usage. + +CPU client: + borg create: does chunking, hashing, compression, crypto (high CPU usage) + chunks cache sync: quite heavy on CPU, doing lots of hashtable operations. + borg extract: crypto, decompression (medium to high CPU usage) + borg check: similar to extract, but depends on options given. + borg prune / borg delete archive: low to medium CPU usage + borg delete repo: done on the server + It won't go beyond 100% of 1 core as the code is currently single-threaded. + Especially higher zlib and lzma compression levels use significant amounts + of CPU cycles. Crypto might be cheap on the CPU (if hardware accelerated) or + expensive (if not). + +CPU server: + It usually doesn't need much CPU, it just deals with the key/value store + (repository) and uses the repository index for that. + + borg check: the repository check computes the checksums of all chunks + (medium CPU usage) + borg delete repo: low CPU usage + +CPU (only for client/server operation): + When using borg in a client/server way with a ssh:-type repo, the ssh + processes used for the transport layer will need some CPU on the client and + on the server due to the crypto they are doing - esp. if you are pumping + big amounts of data. + +Memory (RAM) client: + The chunks index and the files index are read into memory for performance + reasons. Might need big amounts of memory (see below). + Compression, esp. lzma compression with high levels might need substantial + amounts of memory. + +Memory (RAM) server: + The server process will load the repository index into memory. Might need + considerable amounts of memory, but less than on the client (see below). + +Chunks index (client only): + Proportional to the amount of data chunks in your repo. Lots of chunks + in your repo imply a big chunks index. + It is possible to tweak the chunker params (see create options). + +Files index (client only): + Proportional to the amount of files in your last backups. Can be switched + off (see create options), but next backup might be much slower if you do. + The speed benefit of using the files cache is proportional to file size. + +Repository index (server only): + Proportional to the amount of data chunks in your repo. Lots of chunks + in your repo imply a big repository index. + It is possible to tweak the chunker params (see create options) to + influence the amount of chunks being created. + +Temporary files (client): + Reading data and metadata from a FUSE mounted repository will consume up to + the size of all deduplicated, small chunks in the repository. Big chunks + won't be locally cached. + +Temporary files (server): + None. + +Cache files (client only): + Contains the chunks index and files index (plus a collection of single- + archive chunk indexes which might need huge amounts of disk space, + depending on archive count and size - see FAQ about how to reduce). + +Network (only for client/server operation): + If your repository is remote, all deduplicated (and optionally compressed/ + encrypted) data of course has to go over the connection (ssh: repo url). + If you use a locally mounted network filesystem, additionally some copy + operations used for transaction support also go over the connection. If + you backup multiple sources to one target repository, additional traffic + happens for cache resynchronization. diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index c8633b23..9fb3264a 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -14,7 +14,7 @@ positional arguments optional arguments ``-e``, ``--encryption`` - | select encryption key mode (default: "None") + | select encryption key mode ``-a``, ``--append-only`` | create an append-only mode repository @@ -70,30 +70,31 @@ the encryption/decryption key or other secrets. Encryption modes ++++++++++++++++ -repokey and keyfile use AES-CTR-256 for encryption and HMAC-SHA256 for +`repokey` and `keyfile` use AES-CTR-256 for encryption and HMAC-SHA256 for authentication in an encrypt-then-MAC (EtM) construction. The chunk ID hash is HMAC-SHA256 as well (with a separate key). These modes are compatible with borg 1.0.x. -repokey-blake2 and keyfile-blake2 are also authenticated encryption modes, +`repokey-blake2` and `keyfile-blake2` are also authenticated encryption modes, but use BLAKE2b-256 instead of HMAC-SHA256 for authentication. The chunk ID hash is a keyed BLAKE2b-256 hash. -These modes are new and not compatible with borg 1.0.x. +These modes are new and *not* compatible with borg 1.0.x. -"authenticated" mode uses no encryption, but authenticates repository contents +`authenticated` mode uses no encryption, but authenticates repository contents through the same keyed BLAKE2b-256 hash as the other blake2 modes (it uses it as chunk ID hash). The key is stored like repokey. This mode is new and not compatible with borg 1.0.x. -"none" mode uses no encryption and no authentication. It uses sha256 as chunk +`none` mode uses no encryption and no authentication. It uses sha256 as chunk ID hash. Not recommended, rather consider using an authenticated or authenticated/encrypted mode. This mode is compatible with borg 1.0.x. Hardware acceleration will be used automatically. -On modern Intel/AMD CPUs (except very cheap ones), AES is usually hw -accelerated. BLAKE2b is faster than sha256 on Intel/AMD 64bit CPUs. +On modern Intel/AMD CPUs (except very cheap ones), AES is usually +hardware-accelerated. BLAKE2b is faster than SHA256 on Intel/AMD 64bit CPUs, +which makes `authenticated` faster than `none`. -On modern ARM CPUs, NEON provides hw acceleration for sha256 making it faster +On modern ARM CPUs, NEON provides hardware acceleration for SHA256 making it faster than BLAKE2b-256 there. diff --git a/docs/usage/key_change-passphrase.rst.inc b/docs/usage/key_change-passphrase.rst.inc new file mode 100644 index 00000000..cbc0d9e6 --- /dev/null +++ b/docs/usage/key_change-passphrase.rst.inc @@ -0,0 +1,22 @@ +.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! + +.. _borg_key_change-passphrase: + +borg key change-passphrase +-------------------------- +:: + + borg key change-passphrase REPOSITORY + +positional arguments + REPOSITORY + + +`Common options`_ + | + +Description +~~~~~~~~~~~ + +The key files used for repository encryption are optionally passphrase +protected. This command can be used to change this passphrase. diff --git a/docs/usage/key_migrate-to-repokey.rst.inc b/docs/usage/key_migrate-to-repokey.rst.inc new file mode 100644 index 00000000..0e82f28c --- /dev/null +++ b/docs/usage/key_migrate-to-repokey.rst.inc @@ -0,0 +1,36 @@ +.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! + +.. _borg_key_migrate-to-repokey: + +borg key migrate-to-repokey +--------------------------- +:: + + borg key migrate-to-repokey REPOSITORY + +positional arguments + REPOSITORY + + +`Common options`_ + | + +Description +~~~~~~~~~~~ + +This command migrates a repository from passphrase mode (removed in Borg 1.0) +to repokey mode. + +You will be first asked for the repository passphrase (to open it in passphrase +mode). This is the same passphrase as you used to use for this repo before 1.0. + +It will then derive the different secrets from this passphrase. + +Then you will be asked for a new passphrase (twice, for safety). This +passphrase will be used to protect the repokey (which contains these same +secrets in encrypted form). You may use the same passphrase as you used to +use, but you may also use a different one. + +After migrating to repokey mode, you can change the passphrase at any time. +But please note: the secrets will always stay the same and they could always +be derived from your (old) passphrase-mode passphrase. diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index c848e592..d3a4485f 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -46,6 +46,7 @@ This command lists the contents of a repository or an archive. See the "borg help patterns" command for more help on exclude patterns. The following keys are available for --format: + - NEWLINE: OS dependent line separator - NL: alias of NEWLINE - NUL: NUL character for creating print0 / xargs -0 like output, see barchive/bpath @@ -54,13 +55,15 @@ The following keys are available for --format: - CR - LF --- Keys for listing repository archives: +Keys for listing repository archives: + - archive: archive name interpreted as text (might be missing non-text characters, see barchive) - barchive: verbatim archive name, can contain any character except NUL - time: time of creation of the archive - id: internal ID of the archive --- Keys for listing archive files: +Keys for listing archive files: + - type - mode - uid diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index 01c58d1b..b6a0052c 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -57,7 +57,7 @@ automated backup scripts wanting to keep a certain number of historic backups. Also, prune automatically removes checkpoint archives (incomplete archives left behind by interrupted backup runs) except if the checkpoint is the latest archive (and thus still needed). Checkpoint archives are not considered when -comparing archive counts against the retention limits (--keep-*). +comparing archive counts against the retention limits (--keep-X). If a prefix is set with -P, then only archives that start with the prefix are considered for deletion and only those archives count towards the totals diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index 02f06a8c..93f7f414 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -36,10 +36,10 @@ Exclusion options | read exclude patterns from EXCLUDEFILE, one per line ``--exclude-caches`` | exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) - ``--exclude-if-present FILENAME`` - | exclude directories that contain the specified file - ``--keep-tag-files`` - | keep tag files of excluded caches/directories + ``--exclude-if-present NAME`` + | exclude directories that are tagged by containing a filesystem object with the given NAME + ``--keep-exclude-tags``, ``--keep-tag-files`` + | keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise excluded caches/directories Archive options ``--target TARGET`` @@ -48,16 +48,16 @@ Archive options | write checkpoint every SECONDS seconds (Default: 1800) ``--comment COMMENT`` | add a comment text to the archive - ``--timestamp yyyy-mm-ddThh:mm:ss`` - | manually specify the archive creation date/time (UTC). alternatively, give a reference file/directory. + ``--timestamp TIMESTAMP`` + | manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). alternatively, give a reference file/directory. ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the "borg help compression" command for details. ``--always-recompress`` | always recompress chunks, don't skip chunks already compressed with the same algorithm. ``--compression-from COMPRESSIONCONFIG`` | read compression patterns from COMPRESSIONCONFIG, see the output of the "borg help compression" command for details. - ``--chunker-params CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE`` - | specify the chunker parameters (or "default"). + ``--chunker-params PARAMS`` + | specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or "default" to use the current defaults. default: 19,23,21,4095 Description ~~~~~~~~~~~ diff --git a/setup.py b/setup.py index 6e57a5ed..a2a5cb8f 100644 --- a/setup.py +++ b/setup.py @@ -353,6 +353,7 @@ class build_man(Command): .. role:: ref(title) .. |project_name| replace:: Borg + """) def initialize_options(self): @@ -372,6 +373,7 @@ class build_man(Command): self.generate_level('', parser, Archiver) self.build_topic_pages(Archiver) + self.build_intro_page() def generate_level(self, prefix, parser, Archiver): is_subcommand = False @@ -447,6 +449,12 @@ class build_man(Command): write(text) self.gen_man_page(man_title, doc.getvalue()) + def build_intro_page(self): + print('building man page borg(1)', file=sys.stderr) + with open('docs/man_intro.rst') as fd: + man_intro = fd.read() + self.gen_man_page('borg', self.rst_prelude + man_intro) + def new_doc(self): doc = io.StringIO(self.rst_prelude) doc.read() @@ -502,7 +510,9 @@ class build_man(Command): def gen_man_page(self, name, rst): from docutils.writers import manpage from docutils.core import publish_string - man_page = publish_string(source=rst, writer=manpage.Writer()) + # We give the source_path so that docutils can find relative includes + # as-if the document where located in the docs/ directory. + man_page = publish_string(source=rst, source_path='docs/virtmanpage.rst', writer=manpage.Writer()) with open('docs/man/%s.1' % name, 'wb') as fd: fd.write(man_page) From 1d9378f12ce23ed32783aecddf6881844f490089 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 11 Feb 2017 15:45:09 +0100 Subject: [PATCH 0612/1387] man pages: generate page for subparsers, eg. borg-key(1) Conflicts: docs/man/borg-key.1 --- docs/man/borg-key.1 | 2 +- setup.py | 30 +++++++++++++++++++----------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/man/borg-key.1 b/docs/man/borg-key.1 index 2d82f003..ca1ec2eb 100644 --- a/docs/man/borg-key.1 +++ b/docs/man/borg-key.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY 1 "2017-02-11" "" "borg backup tool" +.TH BORG-KEY 1 "2017-02-12" "" "borg backup tool" .SH NAME borg-key \- Manage a keyfile or repokey of a repository . diff --git a/setup.py b/setup.py index a2a5cb8f..771f11b5 100644 --- a/setup.py +++ b/setup.py @@ -393,27 +393,35 @@ class build_man(Command): man_title = 'borg-' + command.replace(' ', '-') print('building man page', man_title + '(1)', file=sys.stderr) - if self.generate_level(command + ' ', parser, Archiver): - continue + is_intermediary = self.generate_level(command + ' ', parser, Archiver) doc, write = self.new_doc() self.write_man_header(write, man_title, parser.description) self.write_heading(write, 'SYNOPSIS') - write('borg', command, end='') - self.write_usage(write, parser) + if is_intermediary: + subparsers = [action for action in parser._actions if 'SubParsersAction' in str(action.__class__)][0] + for subcommand in subparsers.choices: + write('| borg', command, subcommand, '...') + self.see_also.setdefault(command, []).append('%s-%s' % (command, subcommand)) + else: + write('borg', command, end='') + self.write_usage(write, parser) write('\n') - self.write_heading(write, 'DESCRIPTION') description, _, notes = parser.epilog.partition('\n.. man NOTES') - write(description) - self.write_heading(write, 'OPTIONS') - write('See `borg-common(1)` for common options of Borg commands.') - write() - self.write_options(write, parser) + if description: + self.write_heading(write, 'DESCRIPTION') + write(description) - self.write_examples(write, command) + if not is_intermediary: + self.write_heading(write, 'OPTIONS') + write('See `borg-common(1)` for common options of Borg commands.') + write() + self.write_options(write, parser) + + self.write_examples(write, command) if notes: self.write_heading(write, 'NOTES') From 96fa1e8414b5cd02ebc4c53b589f68f8bfd6b9fa Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 5 Feb 2017 23:01:41 +0100 Subject: [PATCH 0613/1387] docs: define "ours" merge strategy for auto-generated files --- .gitattributes | 2 ++ docs/man_intro.rst | 2 +- docs/usage.rst | 2 +- docs/{usage/general.rst.inc => usage_general.rst.inc} | 0 4 files changed, 4 insertions(+), 2 deletions(-) rename docs/{usage/general.rst.inc => usage_general.rst.inc} (100%) diff --git a/.gitattributes b/.gitattributes index 9d00a690..3724dd9d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ borg/_version.py export-subst *.py diff=python +docs/usage/* merge=ours +docs/man/* merge=ours diff --git a/docs/man_intro.rst b/docs/man_intro.rst index 0e08f5a9..b726e331 100644 --- a/docs/man_intro.rst +++ b/docs/man_intro.rst @@ -46,7 +46,7 @@ A step-by-step example NOTES ----- -.. include:: usage/general.rst.inc +.. include:: usage_general.rst.inc SEE ALSO -------- diff --git a/docs/usage.rst b/docs/usage.rst index cd43c65e..2ad55d02 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -12,7 +12,7 @@ command in detail. General ------- -.. include:: usage/general.rst.inc +.. include:: usage_general.rst.inc In case you are interested in more details (like formulas), please see :ref:`internals`. diff --git a/docs/usage/general.rst.inc b/docs/usage_general.rst.inc similarity index 100% rename from docs/usage/general.rst.inc rename to docs/usage_general.rst.inc From 95b69a433c48ba12aed1e612f93946e4d5ceb4fa Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 5 Feb 2017 23:02:43 +0100 Subject: [PATCH 0614/1387] docs: usage: remove not-updated-anymore debug* files. --- docs/usage/debug-delete-obj.rst.inc | 23 ------------------- docs/usage/debug-dump-archive-items.rst.inc | 21 ----------------- docs/usage/debug-dump-repo-objs.rst.inc | 21 ----------------- docs/usage/debug-get-obj.rst.inc | 25 --------------------- docs/usage/debug-info.rst.inc | 19 ---------------- docs/usage/debug-put-obj.rst.inc | 23 ------------------- docs/usage/debug_delete-obj.rst.inc | 23 ------------------- docs/usage/debug_dump-archive-items.rst.inc | 21 ----------------- docs/usage/debug_dump-repo-objs.rst.inc | 21 ----------------- docs/usage/debug_get-obj.rst.inc | 25 --------------------- docs/usage/debug_info.rst.inc | 19 ---------------- docs/usage/debug_put-obj.rst.inc | 23 ------------------- docs/usage/debug_refcount-obj.rst.inc | 23 ------------------- 13 files changed, 287 deletions(-) delete mode 100644 docs/usage/debug-delete-obj.rst.inc delete mode 100644 docs/usage/debug-dump-archive-items.rst.inc delete mode 100644 docs/usage/debug-dump-repo-objs.rst.inc delete mode 100644 docs/usage/debug-get-obj.rst.inc delete mode 100644 docs/usage/debug-info.rst.inc delete mode 100644 docs/usage/debug-put-obj.rst.inc delete mode 100644 docs/usage/debug_delete-obj.rst.inc delete mode 100644 docs/usage/debug_dump-archive-items.rst.inc delete mode 100644 docs/usage/debug_dump-repo-objs.rst.inc delete mode 100644 docs/usage/debug_get-obj.rst.inc delete mode 100644 docs/usage/debug_info.rst.inc delete mode 100644 docs/usage/debug_put-obj.rst.inc delete mode 100644 docs/usage/debug_refcount-obj.rst.inc diff --git a/docs/usage/debug-delete-obj.rst.inc b/docs/usage/debug-delete-obj.rst.inc deleted file mode 100644 index 4fcfb48f..00000000 --- a/docs/usage/debug-delete-obj.rst.inc +++ /dev/null @@ -1,23 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug-delete-obj: - -borg debug-delete-obj ---------------------- -:: - - borg debug-delete-obj REPOSITORY IDs - -positional arguments - REPOSITORY - repository to use - IDs - hex object ID(s) to delete from the repo - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command deletes objects from the repository. diff --git a/docs/usage/debug-dump-archive-items.rst.inc b/docs/usage/debug-dump-archive-items.rst.inc deleted file mode 100644 index 63c39546..00000000 --- a/docs/usage/debug-dump-archive-items.rst.inc +++ /dev/null @@ -1,21 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug-dump-archive-items: - -borg debug-dump-archive-items ------------------------------ -:: - - borg debug-dump-archive-items ARCHIVE - -positional arguments - ARCHIVE - archive to dump - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command dumps raw (but decrypted and decompressed) archive items (only metadata) to files. diff --git a/docs/usage/debug-dump-repo-objs.rst.inc b/docs/usage/debug-dump-repo-objs.rst.inc deleted file mode 100644 index 3910a126..00000000 --- a/docs/usage/debug-dump-repo-objs.rst.inc +++ /dev/null @@ -1,21 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug-dump-repo-objs: - -borg debug-dump-repo-objs -------------------------- -:: - - borg debug-dump-repo-objs REPOSITORY - -positional arguments - REPOSITORY - repo to dump - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command dumps raw (but decrypted and decompressed) repo objects to files. diff --git a/docs/usage/debug-get-obj.rst.inc b/docs/usage/debug-get-obj.rst.inc deleted file mode 100644 index a0b3f457..00000000 --- a/docs/usage/debug-get-obj.rst.inc +++ /dev/null @@ -1,25 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug-get-obj: - -borg debug-get-obj ------------------- -:: - - borg debug-get-obj REPOSITORY ID PATH - -positional arguments - REPOSITORY - repository to use - ID - hex object ID to get from the repo - PATH - file to write object data into - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command gets an object from the repository. diff --git a/docs/usage/debug-info.rst.inc b/docs/usage/debug-info.rst.inc deleted file mode 100644 index 4812aaf4..00000000 --- a/docs/usage/debug-info.rst.inc +++ /dev/null @@ -1,19 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug-info: - -borg debug-info ---------------- -:: - - borg debug-info - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command displays some system information that might be useful for bug -reports and debugging problems. If a traceback happens, this information is -already appended at the end of the traceback. diff --git a/docs/usage/debug-put-obj.rst.inc b/docs/usage/debug-put-obj.rst.inc deleted file mode 100644 index d03ace84..00000000 --- a/docs/usage/debug-put-obj.rst.inc +++ /dev/null @@ -1,23 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug-put-obj: - -borg debug-put-obj ------------------- -:: - - borg debug-put-obj REPOSITORY PATH - -positional arguments - REPOSITORY - repository to use - PATH - file(s) to read and create object(s) from - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command puts objects into the repository. diff --git a/docs/usage/debug_delete-obj.rst.inc b/docs/usage/debug_delete-obj.rst.inc deleted file mode 100644 index 71248e1d..00000000 --- a/docs/usage/debug_delete-obj.rst.inc +++ /dev/null @@ -1,23 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug_delete-obj: - -borg debug delete-obj ---------------------- -:: - - borg debug delete-obj REPOSITORY IDs - -positional arguments - REPOSITORY - repository to use - IDs - hex object ID(s) to delete from the repo - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command deletes objects from the repository. diff --git a/docs/usage/debug_dump-archive-items.rst.inc b/docs/usage/debug_dump-archive-items.rst.inc deleted file mode 100644 index a2871153..00000000 --- a/docs/usage/debug_dump-archive-items.rst.inc +++ /dev/null @@ -1,21 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug_dump-archive-items: - -borg debug dump-archive-items ------------------------------ -:: - - borg debug dump-archive-items ARCHIVE - -positional arguments - ARCHIVE - archive to dump - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command dumps raw (but decrypted and decompressed) archive items (only metadata) to files. diff --git a/docs/usage/debug_dump-repo-objs.rst.inc b/docs/usage/debug_dump-repo-objs.rst.inc deleted file mode 100644 index e041ecc1..00000000 --- a/docs/usage/debug_dump-repo-objs.rst.inc +++ /dev/null @@ -1,21 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug_dump-repo-objs: - -borg debug dump-repo-objs -------------------------- -:: - - borg debug dump-repo-objs REPOSITORY - -positional arguments - REPOSITORY - repo to dump - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command dumps raw (but decrypted and decompressed) repo objects to files. diff --git a/docs/usage/debug_get-obj.rst.inc b/docs/usage/debug_get-obj.rst.inc deleted file mode 100644 index 8a966314..00000000 --- a/docs/usage/debug_get-obj.rst.inc +++ /dev/null @@ -1,25 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug_get-obj: - -borg debug get-obj ------------------- -:: - - borg debug get-obj REPOSITORY ID PATH - -positional arguments - REPOSITORY - repository to use - ID - hex object ID to get from the repo - PATH - file to write object data into - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command gets an object from the repository. diff --git a/docs/usage/debug_info.rst.inc b/docs/usage/debug_info.rst.inc deleted file mode 100644 index ccfbeb19..00000000 --- a/docs/usage/debug_info.rst.inc +++ /dev/null @@ -1,19 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug_info: - -borg debug info ---------------- -:: - - borg debug info - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command displays some system information that might be useful for bug -reports and debugging problems. If a traceback happens, this information is -already appended at the end of the traceback. diff --git a/docs/usage/debug_put-obj.rst.inc b/docs/usage/debug_put-obj.rst.inc deleted file mode 100644 index 5563593b..00000000 --- a/docs/usage/debug_put-obj.rst.inc +++ /dev/null @@ -1,23 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug_put-obj: - -borg debug put-obj ------------------- -:: - - borg debug put-obj REPOSITORY PATH - -positional arguments - REPOSITORY - repository to use - PATH - file(s) to read and create object(s) from - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command puts objects into the repository. diff --git a/docs/usage/debug_refcount-obj.rst.inc b/docs/usage/debug_refcount-obj.rst.inc deleted file mode 100644 index c38fcc7a..00000000 --- a/docs/usage/debug_refcount-obj.rst.inc +++ /dev/null @@ -1,23 +0,0 @@ -.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! - -.. _borg_debug_refcount-obj: - -borg debug refcount-obj ------------------------ -:: - - borg debug refcount-obj REPOSITORY IDs - -positional arguments - REPOSITORY - repository to use - IDs - hex object ID(s) to show refcounts for - -`Common options`_ - | - -Description -~~~~~~~~~~~ - -This command displays the reference count for objects from the repository. From 847797b477a4481720c118dede44636f88f019f3 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 11 Feb 2017 16:01:26 +0100 Subject: [PATCH 0615/1387] update development.rst for docs changes --- docs/development.rst | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/development.rst b/docs/development.rst index afefed0f..b567d37a 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -141,25 +141,36 @@ Important notes: - When using ``--`` to give options to py.test, you MUST also give ``borg.testsuite[.module]``. -Regenerate usage files ----------------------- +Documentation +------------- -Usage and API documentation is currently committed directly to git, -although those files are generated automatically from the source -tree. +Generated files +~~~~~~~~~~~~~~~ -When a new module is added, the ``docs/api.rst`` file needs to be -regenerated:: - - ./setup.py build_api +Usage documentation (found in ``docs/usage/``) and man pages +(``docs/man/``) are generated automatically from the command line +parsers declared in the program and their documentation, which is +embedded in the program (see archiver.py). These are committed to git +for easier use by packagers downstream. When a command is added, a commandline flag changed, added or removed, the usage docs need to be rebuilt as well:: - ./setup.py build_usage + python setup.py build_usage + python setup.py build_man + +However, we prefer to do this as part of our :ref:`releasing` +preparations, so it is generally not necessary to update these when +submitting patches that change something about the command line. + +The code documentation (which is currently not part of the released +docs) also uses a generated file (``docs/api.rst``), that needs to be +updated when a module is added or removed:: + + python setup.py build_api Building the docs with Sphinx ------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The documentation (in reStructuredText format, .rst) is in docs/. @@ -230,6 +241,7 @@ therefore we recommend to use these merge parameters:: git merge 1.0-maint -s recursive -X rename-threshold=20% +.. _releasing: Creating a new release ---------------------- @@ -243,7 +255,7 @@ Checklist: - update ``CHANGES.rst``, based on ``git log $PREVIOUS_RELEASE..`` - check version number of upcoming release in ``CHANGES.rst`` - verify that ``MANIFEST.in`` and ``setup.py`` are complete -- ``python setup.py build_api ; python setup.py build_usage`` and commit +- ``python setup.py build_api ; python setup.py build_usage ; python setup.py build_man`` and commit - tag the release:: git tag -s -m "tagged/signed release X.Y.Z" X.Y.Z From 0971fdce8d14f337ce3f635ebd6ac78711edec21 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 11 Feb 2017 16:05:56 +0100 Subject: [PATCH 0616/1387] docs: changes: fix two rST warnings --- docs/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 084a42d6..3c317ef4 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -232,7 +232,7 @@ Version 1.0.10rc1 (2017-01-29) Bug fixes: - borg serve: fix transmission data loss of pipe writes, #1268 - This affects only the cygwin platform (not Linux, *BSD, OS X). + This affects only the cygwin platform (not Linux, BSD, OS X). - Avoid triggering an ObjectiveFS bug in xattr retrieval, #1992 - When running out of buffer memory when reading xattrs, only skip the current file, #1993 @@ -285,7 +285,7 @@ Other changes: - remove .github from pypi package, #2051 - add pip and setuptools to requirements file, #2030 - SyncFile: fix use of fd object after close (cosmetic) -- Manifest.in: simplify, exclude *.{so,dll,orig}, #2066 +- Manifest.in: simplify, exclude \*.{so,dll,orig}, #2066 - ignore posix_fadvise errors in repository.py, #2095 (works around issues with docker on ARM) - make LoggedIO.close_segment reentrant, avoid reentrance From e4486cd370a276d029429edd619d58c397787eab Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 11 Feb 2017 16:07:55 +0100 Subject: [PATCH 0617/1387] fix rST warning in Repository.scan --- src/borg/repository.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/borg/repository.py b/src/borg/repository.py index 9d4d604d..4183473e 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -788,6 +788,7 @@ class Repository: fetching data in this order does linear reads and reuses stuff from disk cache. We rely on repository.check() has run already (either now or some time before) and that: + - if we are called from a borg check command, self.index is a valid, fresh, in-sync repo index. - if we are called from elsewhere, either self.index or the on-disk index is valid and in-sync. - the repository segments are valid (no CRC errors). From 32e73e8c7e6a80bbae7a0371963ae8b865f1bb4f Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Mon, 30 Jan 2017 00:12:28 +0100 Subject: [PATCH 0618/1387] Manifest: Make sure manifest timestamp is strictly monotonically increasing. Computer clocks are often not set very accurately set, but borg assumes manifest timestamps are never going back in time. Ensure that this is actually the case. # Conflicts: # src/borg/helpers.py Original-Commit: 6b8cf0a --- src/borg/helpers.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index df2a136f..20b0d134 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -200,6 +200,7 @@ class Manifest: self.repository = repository self.item_keys = frozenset(item_keys) if item_keys is not None else ITEM_KEYS self.tam_verified = False + self.timestamp = None @property def id_str(self): @@ -245,7 +246,13 @@ class Manifest: from .item import ManifestItem if self.key.tam_required: self.config[b'tam_required'] = True - self.timestamp = datetime.utcnow().isoformat() + # self.timestamp needs to be strictly monotonically increasing. Clocks often are not set correctly + if self.timestamp is None: + self.timestamp = datetime.utcnow().isoformat() + else: + prev_ts = datetime.strptime(self.timestamp, "%Y-%m-%dT%H:%M:%S.%f") + incremented = (prev_ts + timedelta(microseconds=1)).isoformat() + self.timestamp = max(incremented, datetime.utcnow().isoformat()) manifest = ManifestItem( version=1, archives=StableDict(self.archives.get_raw_dict()), From fb44362c95c19b03c849621879147daf9bff3d3e Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Tue, 7 Feb 2017 00:32:39 +0100 Subject: [PATCH 0619/1387] Add myself to AUTHORS # Conflicts: # AUTHORS Original-Commit: bd8be26 --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 46cbc447..051f54b4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -8,6 +8,7 @@ Borg authors ("The Borg Collective") - Michael Hanselmann - Teemu Toivanen - Marian Beermann +- Martin Hostettler - Daniel Reichelt - Lauri Niskanen From b44882d10c8f28e1defde6b26a978be167188608 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 25 Sep 2016 14:35:06 +0200 Subject: [PATCH 0620/1387] paperkey.html: Add interactive html template for printing key backups. --- docs/paperkey.html | 2438 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2438 insertions(+) create mode 100644 docs/paperkey.html diff --git a/docs/paperkey.html b/docs/paperkey.html new file mode 100644 index 00000000..82c7f98a --- /dev/null +++ b/docs/paperkey.html @@ -0,0 +1,2438 @@ + + + + + + +BorgBackup Printable Key Template + + + + + + +
+
+

To create a printable key, either paste the contents of your keyfile or a key export in the text field + below, or select a key export file.

+

To create a key export use

borg key export /path/to/repository exportfile.txt

+

If you are using keyfile mode, keyfiles are usually stored in $HOME/.config/borg/keys/

+

You can edit the parts with light blue border in the print preview below by click into them.

+

Key security: This print template will never send anything to remote servers. But keep in mind, that printing + might involve computers that can store the printed image, for example with cloud printing services, or + networked printers.

+

+
+ + +
+
+ QR error correction: +
+ QR code size
+ Text size
+ Text columns +
+ +
+
+ + +
+
+ +
+
BorgBackup Printable Key Backup
+
To restore either scan the QR code below, decode it and import it using +
borg key-import /path/to/repo scannedfile
+ +Or run +
borg key import --paper /path/to/repo
and type in the text below.

+
+
+
+
Notes:
+
+
+ + + + + \ No newline at end of file From e0f36e8613fd1015dabab45c6ea602a6aee0db35 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 25 Sep 2016 17:08:40 +0200 Subject: [PATCH 0621/1387] quickstart.rst: Add link to paperkey template. --- docs/conf.py | 2 ++ docs/quickstart.rst | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 8e51e4ea..608c6947 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -140,6 +140,8 @@ html_favicon = '_static/favicon.ico' # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['borg_theme'] +html_extra_path = ['paperkey.html'] + # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%Y-%m-%d' diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 32770c2c..e1e24e84 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -188,11 +188,14 @@ For automated backups the passphrase can be specified using the You can make backups using :ref:`borg_key_export` subcommand. If you want to print a backup of your key to paper use the ``--paper`` - option of this command and print the result. + option of this command and print the result, or this print `template`_ + if you need a version with QR-Code. A backup inside of the backup that is encrypted with that key/passphrase won't help you with that, of course. +.. _template: paperkey.html + .. _remote_repos: Remote repositories From 179f1bc14794748aca4e0cd130336deb610582de Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Wed, 8 Feb 2017 00:22:37 +0100 Subject: [PATCH 0622/1387] Add qr html export mode to `key export` command --- docs/conf.py | 2 +- setup.py | 3 +++ src/borg/archiver.py | 8 +++++++- src/borg/keymanager.py | 20 ++++++++++++++++---- {docs => src/borg}/paperkey.html | 0 5 files changed, 27 insertions(+), 6 deletions(-) rename {docs => src/borg}/paperkey.html (100%) diff --git a/docs/conf.py b/docs/conf.py index 608c6947..d1d64f9f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -140,7 +140,7 @@ html_favicon = '_static/favicon.ico' # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['borg_theme'] -html_extra_path = ['paperkey.html'] +html_extra_path = ['../src/borg/paperkey.html'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/setup.py b/setup.py index 771f11b5..3864be5f 100644 --- a/setup.py +++ b/setup.py @@ -666,6 +666,9 @@ setup( 'borgfs = borg.archiver:main', ] }, + package_data={ + 'borg': ['paperkey.html'] + }, cmdclass=cmdclass, ext_modules=ext_modules, setup_requires=['setuptools_scm>=1.7'], diff --git a/src/borg/archiver.py b/src/borg/archiver.py index feca6ccf..38437b23 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -271,7 +271,10 @@ class Archiver: if not args.path: self.print_error("output file to export key to expected") return EXIT_ERROR - manager.export(args.path) + if args.qr: + manager.export_qr(args.path) + else: + manager.export(args.path) return EXIT_SUCCESS @with_repository(lock=False, exclusive=False, manifest=False, cache=False) @@ -1938,6 +1941,9 @@ class Archiver: subparser.add_argument('--paper', dest='paper', action='store_true', default=False, help='Create an export suitable for printing and later type-in') + subparser.add_argument('--qr-html', dest='qr', action='store_true', + default=False, + help='Create an html file suitable for printing and later type-in or qr scan') key_import_epilog = process_epilog(""" This command allows to restore a key previously backed up with the diff --git a/src/borg/keymanager.py b/src/borg/keymanager.py index 0b365e82..49799afc 100644 --- a/src/borg/keymanager.py +++ b/src/borg/keymanager.py @@ -2,6 +2,7 @@ from binascii import unhexlify, a2b_base64, b2a_base64 import binascii import textwrap from hashlib import sha256 +import pkgutil from .key import KeyfileKey, RepoKey, PassphraseKey, KeyfileNotFoundError, PlaintextKey from .helpers import Manifest, NoManifestError, Error, yes, bin_to_hex @@ -77,16 +78,27 @@ class KeyManager: elif self.keyblob_storage == KEYBLOB_REPO: self.repository.save_key(self.keyblob.encode('utf-8')) + def get_keyfile_data(self): + data = '%s %s\n' % (KeyfileKey.FILE_ID, bin_to_hex(self.repository.id)) + data += self.keyblob + if not self.keyblob.endswith('\n'): + data += '\n' + return data + def store_keyfile(self, target): with open(target, 'w') as fd: - fd.write('%s %s\n' % (KeyfileKey.FILE_ID, bin_to_hex(self.repository.id))) - fd.write(self.keyblob) - if not self.keyblob.endswith('\n'): - fd.write('\n') + fd.write(self.get_keyfile_data()) def export(self, path): self.store_keyfile(path) + def export_qr(self, path): + with open(path, 'wb') as fd: + key_data = self.get_keyfile_data() + html = pkgutil.get_data('borg', 'paperkey.html') + html = html.replace(b'', key_data.encode() + b'') + fd.write(html) + def export_paperkey(self, path): def grouped(s): ret = '' diff --git a/docs/paperkey.html b/src/borg/paperkey.html similarity index 100% rename from docs/paperkey.html rename to src/borg/paperkey.html From 2cdb5838797a78fb460898e83202ec28f8275256 Mon Sep 17 00:00:00 2001 From: Benedikt Heine Date: Sun, 12 Feb 2017 17:18:08 +0100 Subject: [PATCH 0623/1387] clearify doc for same filesystems # Conflicts: # src/borg/archiver.py Original-Commit: d3a2f36b03 --- src/borg/archiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 38437b23..bca7a04c 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2136,7 +2136,7 @@ class Archiver: fs_group = subparser.add_argument_group('Filesystem options') fs_group.add_argument('-x', '--one-file-system', dest='one_file_system', action='store_true', default=False, - help='stay in same file system, do not cross mount points') + help='stay in the same file system and do not store mount points of other file systems') fs_group.add_argument('--numeric-owner', dest='numeric_owner', action='store_true', default=False, help='only store numeric user and group identifiers') From 79dd920661d9df607e70ec041eef8d2c89382479 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 12 Feb 2017 22:25:12 +0100 Subject: [PATCH 0624/1387] update CHANGES (1.0.10) # Conflicts: # docs/changes.rst Original-Commit: e635f219 --- docs/changes.rst | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 3c317ef4..08f82e2e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -145,6 +145,35 @@ New features: --keep-exclude-tags, to account for the change mentioned above. +Version 1.0.10 (2017-02-13) +--------------------------- + +Bug fixes: + +- Manifest timestamps are now monotonically increasing, + this fixes issues when the system clock jumps backwards + or is set inconsistently across computers accessing the same repository, #2115 +- Fixed testing regression in 1.0.10rc1 that lead to a hard dependency on + py.test >= 3.0, #2112 + +New features: + +- "key export" can now generate a printable HTML page with both a QR code and + a human-readable "paperkey" representation (and custom text) through the + ``--qr-html`` option. + + The same functionality is also available through `paperkey.html `_, + which is the same HTML page generated by ``--qr-html``. It works with existing + "key export" files and key files. + +Other changes: + +- docs: + + - language clarification - "borg create --one-file-system" option does not respect + mount points, but considers different file systems instead, #2141 + + Version 1.1.0b3 (2017-01-15) ---------------------------- From 04bd6fb013f8139ccae40c5b98782cb55af3e465 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 12 Feb 2017 20:40:53 +0100 Subject: [PATCH 0625/1387] add test for export key --qr-html --- src/borg/testsuite/archiver.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index a9ad8ecf..0636dfc8 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1973,6 +1973,19 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert repo_key2.enc_key == repo_key2.enc_key + def test_key_export_qr(self): + export_file = self.output_path + '/exported.html' + self.cmd('init', self.repository_location, '--encryption', 'repokey') + repo_id = self._extract_repository_id(self.repository_path) + self.cmd('key', 'export', '--qr-html', self.repository_location, export_file) + + with open(export_file, 'r') as fd: + export_contents = fd.read() + + assert bin_to_hex(repo_id) in export_contents + assert export_contents.startswith('') + assert export_contents.endswith('') + def test_key_import_errors(self): export_file = self.output_path + '/exported' self.cmd('init', self.repository_location, '--encryption', 'keyfile') From 1fabb2df5808d1427ef50b3879a3d509128faf7c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 12 Feb 2017 20:45:41 +0100 Subject: [PATCH 0626/1387] key export: center QR code on the page --- src/borg/paperkey.html | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/borg/paperkey.html b/src/borg/paperkey.html index 82c7f98a..4e1e859b 100644 --- a/src/borg/paperkey.html +++ b/src/borg/paperkey.html @@ -2171,8 +2171,11 @@ if (typeof define == 'function' && define.amd) define([], function() { return Sh } } - - + /* center the QR code on the page */ + #qr { + width: 100%; + text-align: center; + } @@ -2217,7 +2220,7 @@ if (typeof define == 'function' && define.amd) define([], function() { return Sh
BorgBackup Printable Key Backup
To restore either scan the QR code below, decode it and import it using -
borg key-import /path/to/repo scannedfile
+
borg key import /path/to/repo scannedfile
Or run
borg key import --paper /path/to/repo
and type in the text below.

From 44798e0edd62bd8c1df2544f0b71c4beefb9bd97 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 12 Feb 2017 22:36:24 +0100 Subject: [PATCH 0627/1387] setup.py: build_api: sort file list for determinism # Conflicts: # docs/api.rst # setup.py Original-Commit: e208d115 --- docs/changes.rst | 1 + setup.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 08f82e2e..d8d9a351 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -172,6 +172,7 @@ Other changes: - language clarification - "borg create --one-file-system" option does not respect mount points, but considers different file systems instead, #2141 +- setup.py: build_api: sort file list for determinism Version 1.1.0b3 (2017-01-15) diff --git a/setup.py b/setup.py index 3864be5f..d432bb16 100644 --- a/setup.py +++ b/setup.py @@ -584,10 +584,13 @@ class build_api(Command): print("auto-generating API documentation") with open("docs/api.rst", "w") as doc: doc.write(""" +.. IMPORTANT: this file is auto-generated by "setup.py build_api", do not edit! + + API Documentation ================= """) - for mod in glob('src/borg/*.py') + glob('src/borg/*.pyx'): + for mod in sorted(glob('src/borg/*.py') + glob('src/borg/*.pyx')): print("examining module %s" % mod) mod = mod.replace('.pyx', '').replace('.py', '').replace('/', '.') if "._" not in mod: From 11318c94dc79ab9086acd188f51980d466b7d0f8 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 13 Feb 2017 00:37:27 +0100 Subject: [PATCH 0628/1387] add paperkey.html to pyinstaller spec --- scripts/borg.exe.spec | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/borg.exe.spec b/scripts/borg.exe.spec index 9d165c74..07dcdfbe 100644 --- a/scripts/borg.exe.spec +++ b/scripts/borg.exe.spec @@ -10,7 +10,9 @@ block_cipher = None a = Analysis([os.path.join(basepath, 'src/borg/__main__.py'), ], pathex=[basepath, ], binaries=[], - datas=[], + datas=[ + ('../src/borg/paperkey.html', 'borg'), + ], hiddenimports=['borg.platform.posix'], hookspath=[], runtime_hooks=[], From 8d432b01e1914758c474f91b20fb95ff99855979 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 13 Feb 2017 04:12:12 +0100 Subject: [PATCH 0629/1387] paperkey.html - decode as utf-8, fixes #2150 hardcoded the encoding for reading it. while utf-8 is the default encoding on many systems, it does not work everywhere. and when it tries to decode with the ascii decoder, it fails. --- src/borg/testsuite/archiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 0636dfc8..5bfaca12 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1979,7 +1979,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): repo_id = self._extract_repository_id(self.repository_path) self.cmd('key', 'export', '--qr-html', self.repository_location, export_file) - with open(export_file, 'r') as fd: + with open(export_file, 'r', encoding='utf-8') as fd: export_contents = fd.read() assert bin_to_hex(repo_id) in export_contents From 30a5c5e44b185f0d9955d93913f38d6554c5fbed Mon Sep 17 00:00:00 2001 From: Alexander 'Leo' Bergolth Date: Tue, 2 Aug 2016 16:02:02 +0200 Subject: [PATCH 0630/1387] add two new options --pattern and --patterns-from as discussed in #1406 # Conflicts: # src/borg/archiver.py # src/borg/helpers.py # src/borg/testsuite/helpers.py Original-Commit: 876b670d --- src/borg/archiver.py | 190 ++++++++++++++++++++++++--------- src/borg/helpers.py | 95 ++++++++++++++--- src/borg/testsuite/archiver.py | 47 ++++++++ src/borg/testsuite/helpers.py | 114 +++++++++++++++++++- 4 files changed, 378 insertions(+), 68 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index bca7a04c..f86fa726 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -44,7 +44,8 @@ from .helpers import to_localtime, timestamp from .helpers import get_cache_dir from .helpers import Manifest from .helpers import StableDict -from .helpers import update_excludes, check_extension_modules +from .helpers import check_extension_modules +from .helpers import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern from .helpers import dir_is_tagged, is_slow_msgpack, yes, sysinfo from .helpers import log_multi from .helpers import parse_pattern, PatternMatcher, PathPrefixPattern @@ -128,7 +129,7 @@ class Archiver: def __init__(self, lock_wait=None, prog=None): self.exit_code = EXIT_SUCCESS self.lock_wait = lock_wait - self.parser = self.build_parser(prog) + self.prog = prog def print_error(self, msg, *args): msg = args and msg % args or msg @@ -172,10 +173,10 @@ class Archiver: bi += slicelen @staticmethod - def build_matcher(excludes, paths): + def build_matcher(inclexcl_patterns, paths): matcher = PatternMatcher() - if excludes: - matcher.add(excludes, False) + if inclexcl_patterns: + matcher.add_inclexcl(inclexcl_patterns) include_patterns = [] if paths: include_patterns.extend(parse_pattern(i, PathPrefixPattern) for i in paths) @@ -316,8 +317,7 @@ class Archiver: def do_create(self, args, repository, manifest=None, key=None): """Create new archive""" matcher = PatternMatcher(fallback=True) - if args.excludes: - matcher.add(args.excludes, False) + matcher.add_inclexcl(args.patterns) def create_inner(archive, cache): # Add cache dir to inode_skip list @@ -523,7 +523,7 @@ class Archiver: if sys.platform.startswith(('linux', 'freebsd', 'netbsd', 'openbsd', 'darwin', )): logger.warning('Hint: You likely need to fix your locale setup. E.g. install locales and use: LANG=en_US.UTF-8') - matcher, include_patterns = self.build_matcher(args.excludes, args.paths) + matcher, include_patterns = self.build_matcher(args.patterns, args.paths) progress = args.progress output_list = args.output_list @@ -793,7 +793,7 @@ class Archiver: 'If you know for certain that they are the same, pass --same-chunker-params ' 'to override this check.') - matcher, include_patterns = self.build_matcher(args.excludes, args.paths) + matcher, include_patterns = self.build_matcher(args.patterns, args.paths) compare_archives(archive1, archive2, matcher) @@ -927,7 +927,7 @@ class Archiver: return self._list_repository(args, manifest, write) def _list_archive(self, args, repository, manifest, key, write): - matcher, _ = self.build_matcher(args.excludes, args.paths) + matcher, _ = self.build_matcher(args.patterns, args.paths) with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: archive = Archive(repository, key, manifest, args.location.archive, cache=cache, consider_part_files=args.consider_part_files) @@ -1157,7 +1157,7 @@ class Archiver: env_var_override='BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING'): return EXIT_ERROR - matcher, include_patterns = self.build_matcher(args.excludes, args.paths) + matcher, include_patterns = self.build_matcher(args.patterns, args.paths) self.output_list = args.output_list self.output_filter = args.output_filter @@ -1401,8 +1401,9 @@ class Archiver: helptext = collections.OrderedDict() helptext['patterns'] = textwrap.dedent(''' - Exclusion patterns support four separate styles, fnmatch, shell, regular - expressions and path prefixes. By default, fnmatch is used. If followed + File patterns support four separate styles: fnmatch, shell, regular + expressions and path prefixes. By default, fnmatch is used for + `--exclude` patterns and shell-style is used for `--pattern`. If followed by a colon (':') the first two characters of a pattern are used as a style selector. Explicit style selection is necessary when a non-default style is desired or when the desired pattern starts with @@ -1410,12 +1411,12 @@ class Archiver: `Fnmatch `_, selector `fm:` - This is the default style. These patterns use a variant of shell - pattern syntax, with '*' matching any number of characters, '?' - matching any single character, '[...]' matching any single - character specified, including ranges, and '[!...]' matching any - character not specified. For the purpose of these patterns, the - path separator ('\\' for Windows and '/' on other systems) is not + This is the default style for --exclude and --exclude-from. + These patterns use a variant of shell pattern syntax, with '*' matching + any number of characters, '?' matching any single character, '[...]' + matching any single character specified, including ranges, and '[!...]' + matching any character not specified. For the purpose of these patterns, + the path separator ('\\' for Windows and '/' on other systems) is not treated specially. Wrap meta-characters in brackets for a literal match (i.e. `[?]` to match the literal character `?`). For a path to match a pattern, it must completely match from start to end, or @@ -1426,6 +1427,7 @@ class Archiver: Shell-style patterns, selector `sh:` + This is the default style for --pattern and --patterns-from. Like fnmatch patterns these are similar to shell patterns. The difference is that the pattern may include `**/` for matching zero or more directory levels, `*` for matching zero or more arbitrary characters with the @@ -1486,7 +1488,39 @@ class Archiver: re:^/home/[^/]\.tmp/ sh:/home/*/.thumbnails EOF - $ borg create --exclude-from exclude.txt backup /\n\n''') + $ borg create --exclude-from exclude.txt backup / + + + A more general and easier to use way to define filename matching patterns exists + with the `--pattern` and `--patterns-from` options. Using these, you may specify + the backup roots (starting points) and patterns for inclusion/exclusion. A + root path starts with the prefix `R`, followed by a path (a plain path, not a + file pattern). An include rule starts with the prefix +, an exclude rule starts + with the prefix -, both followed by a pattern. + Inclusion patterns are useful to include pathes that are contained in an excluded + path. The first matching pattern is used so if an include pattern matches before + an exclude pattern, the file is backed up. + + Note that the default pattern style for `--pattern` and `--patterns-from` is + shell style (`sh:`), so those patterns behave similar to rsync include/exclude + patterns. + + Patterns (`--pattern`) and excludes (`--exclude`) from the command line are + considered first (in the order of appearance). Then patterns from `--patterns-from` + are added. Exclusion patterns from `--exclude-from` files are appended last. + + An example `--patterns-from` file could look like that:: + + R / + # can be rebuild + - /home/*/.cache + # they're downloads for a reason + - /home/*/Downloads + # susan is a nice person + # include susans home + + /home/susan + # don't backup the other home directories + - /home/*\n\n''') helptext['placeholders'] = textwrap.dedent(''' Repository (or Archive) URLs, --prefix and --remote-path values support these placeholders: @@ -1717,6 +1751,9 @@ class Archiver: help='show version number and exit') subparsers = parser.add_subparsers(title='required arguments', metavar='') + # some empty defaults for all subparsers + common_parser.set_defaults(paths=[], patterns=[]) + serve_epilog = process_epilog(""" This command starts a repository server process. This command is usually not used manually. """) @@ -2114,11 +2151,10 @@ class Archiver: help='only display items with the given status characters') exclude_group = subparser.add_argument_group('Exclusion options') - exclude_group.add_argument('-e', '--exclude', dest='excludes', - type=parse_pattern, action='append', + exclude_group.add_argument('-e', '--exclude', dest='patterns', + type=parse_exclude_pattern, action='append', metavar="PATTERN", help='exclude paths matching PATTERN') - exclude_group.add_argument('--exclude-from', dest='exclude_files', - type=argparse.FileType('r'), action='append', + exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction, metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') exclude_group.add_argument('--exclude-caches', dest='exclude_caches', action='store_true', default=False, @@ -2132,6 +2168,11 @@ class Archiver: action='store_true', default=False, help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise ' 'excluded caches/directories') + exclude_group.add_argument('--pattern', + action=ArgparsePatternAction, + metavar="PATTERN", help='include/exclude paths matching PATTERN') + exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction, + metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line') fs_group = subparser.add_argument_group('Filesystem options') fs_group.add_argument('-x', '--one-file-system', dest='one_file_system', @@ -2183,7 +2224,7 @@ class Archiver: subparser.add_argument('location', metavar='ARCHIVE', type=location_validator(archive=True), help='name of archive to create (must be also a valid directory name)') - subparser.add_argument('paths', metavar='PATH', nargs='+', type=str, + subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to archive') extract_epilog = process_epilog(""" @@ -2213,12 +2254,15 @@ class Archiver: subparser.add_argument('-n', '--dry-run', dest='dry_run', default=False, action='store_true', help='do not actually change any files') - subparser.add_argument('-e', '--exclude', dest='excludes', - type=parse_pattern, action='append', + subparser.add_argument('-e', '--exclude', dest='patterns', + type=parse_exclude_pattern, action='append', metavar="PATTERN", help='exclude paths matching PATTERN') - subparser.add_argument('--exclude-from', dest='exclude_files', - type=argparse.FileType('r'), action='append', + subparser.add_argument('--exclude-from', action=ArgparseExcludeFileAction, metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') + subparser.add_argument('--pattern', action=ArgparsePatternAction, + metavar="PATTERN", help='include/exclude paths matching PATTERN') + subparser.add_argument('--patterns-from', action=ArgparsePatternFileAction, + metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line') subparser.add_argument('--numeric-owner', dest='numeric_owner', action='store_true', default=False, help='only obey numeric user and group identifiers') @@ -2261,12 +2305,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='find differences in archive contents') subparser.set_defaults(func=self.do_diff) - subparser.add_argument('-e', '--exclude', dest='excludes', - type=parse_pattern, action='append', - metavar="PATTERN", help='exclude paths matching PATTERN') - subparser.add_argument('--exclude-from', dest='exclude_files', - type=argparse.FileType('r'), action='append', - metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') subparser.add_argument('--numeric-owner', dest='numeric_owner', action='store_true', default=False, help='only consider numeric user and group identifiers') @@ -2285,6 +2323,30 @@ class Archiver: subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths of items inside the archives to compare; patterns are supported') + exclude_group = subparser.add_argument_group('Exclusion options') + exclude_group.add_argument('-e', '--exclude', dest='patterns', + type=parse_exclude_pattern, action='append', + metavar="PATTERN", help='exclude paths matching PATTERN') + exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction, + metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') + exclude_group.add_argument('--exclude-caches', dest='exclude_caches', + action='store_true', default=False, + help='exclude directories that contain a CACHEDIR.TAG file (' + 'http://www.brynosaurus.com/cachedir/spec.html)') + exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', + metavar='NAME', action='append', type=str, + help='exclude directories that are tagged by containing a filesystem object with ' + 'the given NAME') + exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', + action='store_true', default=False, + help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise ' + 'excluded caches/directories') + exclude_group.add_argument('--pattern', + action=ArgparsePatternAction, + metavar="PATTERN", help='include/exclude paths matching PATTERN') + exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction, + metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line') + rename_epilog = process_epilog(""" This command renames an archive in the repository. @@ -2365,12 +2427,6 @@ class Archiver: subparser.add_argument('--format', '--list-format', dest='format', type=str, help="""specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")""") - subparser.add_argument('-e', '--exclude', dest='excludes', - type=parse_pattern, action='append', - metavar="PATTERN", help='exclude paths matching PATTERN') - subparser.add_argument('--exclude-from', dest='exclude_files', - type=argparse.FileType('r'), action='append', - metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='', type=location_validator(), help='repository/archive to list contents of') @@ -2378,6 +2434,30 @@ class Archiver: help='paths to list; patterns are supported') self.add_archives_filters_args(subparser) + exclude_group = subparser.add_argument_group('Exclusion options') + exclude_group.add_argument('-e', '--exclude', dest='patterns', + type=parse_exclude_pattern, action='append', + metavar="PATTERN", help='exclude paths matching PATTERN') + exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction, + metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') + exclude_group.add_argument('--exclude-caches', dest='exclude_caches', + action='store_true', default=False, + help='exclude directories that contain a CACHEDIR.TAG file (' + 'http://www.brynosaurus.com/cachedir/spec.html)') + exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', + metavar='NAME', action='append', type=str, + help='exclude directories that are tagged by containing a filesystem object with ' + 'the given NAME') + exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', + action='store_true', default=False, + help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise ' + 'excluded caches/directories') + exclude_group.add_argument('--pattern', + action=ArgparsePatternAction, + metavar="PATTERN", help='include/exclude paths matching PATTERN') + exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction, + metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line') + mount_epilog = process_epilog(""" This command mounts an archive as a FUSE filesystem. This can be useful for browsing an archive or restoring individual files. Unless the ``--foreground`` @@ -2718,11 +2798,10 @@ class Archiver: help='print statistics at end') exclude_group = subparser.add_argument_group('Exclusion options') - exclude_group.add_argument('-e', '--exclude', dest='excludes', - type=parse_pattern, action='append', + exclude_group.add_argument('-e', '--exclude', dest='patterns', + type=parse_exclude_pattern, action='append', metavar="PATTERN", help='exclude paths matching PATTERN') - exclude_group.add_argument('--exclude-from', dest='exclude_files', - type=argparse.FileType('r'), action='append', + exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction, metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') exclude_group.add_argument('--exclude-caches', dest='exclude_caches', action='store_true', default=False, @@ -2730,12 +2809,17 @@ class Archiver: 'http://www.brynosaurus.com/cachedir/spec.html)') exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', metavar='NAME', action='append', type=str, - help='exclude directories that are tagged by containing a filesystem object with \ - the given NAME') + help='exclude directories that are tagged by containing a filesystem object with ' + 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', action='store_true', default=False, - help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise \ - excluded caches/directories') + help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise ' + 'excluded caches/directories') + exclude_group.add_argument('--pattern', + action=ArgparsePatternAction, + metavar="PATTERN", help='include/exclude paths matching PATTERN') + exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction, + metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line') archive_group = subparser.add_argument_group('Archive options') archive_group.add_argument('--target', dest='target', metavar='TARGET', default=None, @@ -2998,8 +3082,12 @@ class Archiver: # We can't use argparse for "serve" since we don't want it to show up in "Available commands" if args: args = self.preprocess_args(args) - args = self.parser.parse_args(args or ['-h']) - update_excludes(args) + parser = self.build_parser(self.prog) + args = parser.parse_args(args or ['-h']) + if args.func == self.do_create: + # need at least 1 path but args.paths may also be populated from patterns + if not args.paths: + parser.error('Need at least one PATH argument.') return args def prerun_checks(self, logger): diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 20b0d134..cf0af1e0 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -362,21 +362,52 @@ def parse_timestamp(timestamp): return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc) -def load_excludes(fh): - """Load and parse exclude patterns from file object. Lines empty or starting with '#' after stripping whitespace on - both line ends are ignored. - """ - return [parse_pattern(pattern) for pattern in clean_lines(fh)] +def parse_add_pattern(patternstr, roots, patterns): + """Parse a pattern string and add it to roots or patterns depending on the pattern type.""" + pattern = parse_inclexcl_pattern(patternstr) + if pattern.ptype is RootPath: + roots.append(pattern.pattern) + else: + patterns.append(pattern) -def update_excludes(args): - """Merge exclude patterns from files with those on command line.""" - if hasattr(args, 'exclude_files') and args.exclude_files: - if not hasattr(args, 'excludes') or args.excludes is None: - args.excludes = [] - for file in args.exclude_files: - args.excludes += load_excludes(file) - file.close() +def load_pattern_file(fileobj, roots, patterns): + for patternstr in clean_lines(fileobj): + parse_add_pattern(patternstr, roots, patterns) + + +def load_exclude_file(fileobj, patterns): + for patternstr in clean_lines(fileobj): + patterns.append(parse_exclude_pattern(patternstr)) + + +class ArgparsePatternAction(argparse.Action): + def __init__(self, nargs=1, **kw): + super().__init__(nargs=nargs, **kw) + + def __call__(self, parser, args, values, option_string=None): + parse_add_pattern(values[0], args.paths, args.patterns) + + +class ArgparsePatternFileAction(argparse.Action): + def __init__(self, nargs=1, **kw): + super().__init__(nargs=nargs, **kw) + + def __call__(self, parser, args, values, option_string=None): + """Load and parse patterns from a file. + Lines empty or starting with '#' after stripping whitespace on both line ends are ignored. + """ + filename = values[0] + with open(filename) as f: + self.parse(f, args) + + def parse(self, fobj, args): + load_pattern_file(fobj, args.roots, args.patterns) + + +class ArgparseExcludeFileAction(ArgparsePatternFileAction): + def parse(self, fobj, args): + load_exclude_file(fobj, args.patterns) class PatternMatcher: @@ -395,6 +426,12 @@ class PatternMatcher: """ self._items.extend((i, value) for i in patterns) + def add_inclexcl(self, patterns): + """Add list of patterns (of type InclExclPattern) to internal list. The patterns ptype member is returned from + the match function when one of the given patterns matches. + """ + self._items.extend(patterns) + def match(self, path): for (pattern, value) in self._items: if pattern.match(path): @@ -546,6 +583,9 @@ _PATTERN_STYLES = set([ _PATTERN_STYLE_BY_PREFIX = dict((i.PREFIX, i) for i in _PATTERN_STYLES) +InclExclPattern = namedtuple('InclExclPattern', 'pattern ptype') +RootPath = object() + def parse_pattern(pattern, fallback=FnmatchPattern): """Read pattern from string and return an instance of the appropriate implementation class. @@ -563,6 +603,35 @@ def parse_pattern(pattern, fallback=FnmatchPattern): return cls(pattern) +def parse_exclude_pattern(pattern, fallback=FnmatchPattern): + """Read pattern from string and return an instance of the appropriate implementation class. + """ + epattern = parse_pattern(pattern, fallback) + return InclExclPattern(epattern, False) + + +def parse_inclexcl_pattern(pattern, fallback=ShellPattern): + """Read pattern from string and return a InclExclPattern object.""" + type_prefix_map = { + '-': False, + '+': True, + 'R': RootPath, + 'r': RootPath, + } + try: + ptype = type_prefix_map[pattern[0]] + pattern = pattern[1:].lstrip() + if not pattern: + raise ValueError("Missing pattern!") + except (IndexError, KeyError, ValueError): + raise argparse.ArgumentTypeError("Unable to parse pattern: {}".format(pattern)) + if ptype is RootPath: + pobj = pattern + else: + pobj = parse_pattern(pattern, fallback) + return InclExclPattern(pobj, ptype) + + def timestamp(s): """Convert a --timestamp=s argument to a datetime object""" try: diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 5bfaca12..2b0709be 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -877,6 +877,53 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.mkdir('input/cache3') os.link('input/cache1/%s' % CACHE_TAG_NAME, 'input/cache3/%s' % CACHE_TAG_NAME) + def test_create_without_root(self): + """test create without a root""" + self.cmd('init', self.repository_location) + args = ['create', self.repository_location + '::test'] + if self.FORK_DEFAULT: + self.cmd(*args, exit_code=2) + else: + self.assert_raises(SystemExit, lambda: self.cmd(*args)) + + def test_create_pattern_root(self): + """test create with only a root pattern""" + self.cmd('init', self.repository_location) + self.create_regular_file('file1', size=1024 * 80) + self.create_regular_file('file2', size=1024 * 80) + output = self.cmd('create', '-v', '--list', '--pattern=R input', self.repository_location + '::test') + self.assert_in("A input/file1", output) + self.assert_in("A input/file2", output) + + def test_create_pattern(self): + """test file patterns during create""" + self.cmd('init', self.repository_location) + self.create_regular_file('file1', size=1024 * 80) + self.create_regular_file('file2', size=1024 * 80) + self.create_regular_file('file_important', size=1024 * 80) + output = self.cmd('create', '-v', '--list', + '--pattern=+input/file_important', '--pattern=-input/file*', + self.repository_location + '::test', 'input') + self.assert_in("A input/file_important", output) + self.assert_in("A input/file_important", output) + self.assert_in('x input/file1', output) + self.assert_in('x input/file2', output) + + def test_extract_pattern_opt(self): + self.cmd('init', self.repository_location) + self.create_regular_file('file1', size=1024 * 80) + self.create_regular_file('file2', size=1024 * 80) + self.create_regular_file('file_important', size=1024 * 80) + self.cmd('create', self.repository_location + '::test', 'input') + with changedir('output'): + self.cmd('extract', + '--pattern=+input/file_important', '--pattern=-input/file*', + self.repository_location + '::test') + self.assert_equal(sorted(os.listdir('output/input')), ['file_important']) + + def test_exclude_caches(self): + self.cmd('init', self.repository_location) + def _assert_test_caches(self): with changedir('output'): self.cmd('extract', self.repository_location + '::test') diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 49f32dfd..8dca6a39 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -1,11 +1,12 @@ +import argparse import hashlib -import logging import os import sys from datetime import datetime, timezone, timedelta from time import mktime, strptime, sleep import pytest + import msgpack import msgpack.fallback @@ -21,7 +22,7 @@ from ..helpers import yes, TRUISH, FALSISH, DEFAULTISH from ..helpers import StableDict, bin_to_hex from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams, Chunk from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless -from ..helpers import load_excludes +from ..helpers import load_exclude_file, load_pattern_file from ..helpers import CompressionSpec, CompressionDecider1, CompressionDecider2 from ..helpers import parse_pattern, PatternMatcher, RegexPattern, PathPrefixPattern, FnmatchPattern, ShellPattern from ..helpers import swidth_slice @@ -431,8 +432,13 @@ def test_invalid_unicode_pattern(pattern): (["pp:/"], [" #/wsfoobar", "\tstart/whitespace"]), (["pp:aaabbb"], None), (["pp:/data", "pp: #/", "pp:\tstart", "pp:/whitespace"], ["/more/data", "/home"]), + (["/nomatch", "/more/*"], + ['/data/something00.txt', '/home', ' #/wsfoobar', '\tstart/whitespace', '/whitespace/end\t']), + # the order of exclude patterns shouldn't matter + (["/more/*", "/nomatch"], + ['/data/something00.txt', '/home', ' #/wsfoobar', '\tstart/whitespace', '/whitespace/end\t']), ]) -def test_patterns_from_file(tmpdir, lines, expected): +def test_exclude_patterns_from_file(tmpdir, lines, expected): files = [ '/data/something00.txt', '/more/data', '/home', ' #/wsfoobar', @@ -441,8 +447,10 @@ def test_patterns_from_file(tmpdir, lines, expected): ] def evaluate(filename): + patterns = [] + load_exclude_file(open(filename, "rt"), patterns) matcher = PatternMatcher(fallback=True) - matcher.add(load_excludes(open(filename, "rt")), False) + matcher.add_inclexcl(patterns) return [path for path in files if matcher.match(path)] exclfile = tmpdir.join("exclude.txt") @@ -453,6 +461,104 @@ def test_patterns_from_file(tmpdir, lines, expected): assert evaluate(str(exclfile)) == (files if expected is None else expected) +@pytest.mark.parametrize("lines, expected_roots, expected_numpatterns", [ + # "None" means all files, i.e. none excluded + ([], [], 0), + (["# Comment only"], [], 0), + (["- *"], [], 1), + (["+fm:*/something00.txt", + "-/data"], [], 2), + (["R /"], ["/"], 0), + (["R /", + "# comment"], ["/"], 0), + (["# comment", + "- /data", + "R /home"], ["/home"], 1), +]) +def test_load_patterns_from_file(tmpdir, lines, expected_roots, expected_numpatterns): + def evaluate(filename): + roots = [] + inclexclpatterns = [] + load_pattern_file(open(filename, "rt"), roots, inclexclpatterns) + return roots, len(inclexclpatterns) + patternfile = tmpdir.join("patterns.txt") + + with patternfile.open("wt") as fh: + fh.write("\n".join(lines)) + + roots, numpatterns = evaluate(str(patternfile)) + assert roots == expected_roots + assert numpatterns == expected_numpatterns + + +@pytest.mark.parametrize("lines", [ + (["X /data"]), # illegal pattern type prefix + (["/data"]), # need a pattern type prefix +]) +def test_load_invalid_patterns_from_file(tmpdir, lines): + patternfile = tmpdir.join("patterns.txt") + with patternfile.open("wt") as fh: + fh.write("\n".join(lines)) + filename = str(patternfile) + with pytest.raises(argparse.ArgumentTypeError): + roots = [] + inclexclpatterns = [] + load_pattern_file(open(filename, "rt"), roots, inclexclpatterns) + + +@pytest.mark.parametrize("lines, expected", [ + # "None" means all files, i.e. none excluded + ([], None), + (["# Comment only"], None), + (["- *"], []), + # default match type is sh: for patterns -> * doesn't match a / + (["-*/something0?.txt"], + ['/data', '/data/something00.txt', '/data/subdir/something01.txt', + '/home', '/home/leo', '/home/leo/t', '/home/other']), + (["-fm:*/something00.txt"], + ['/data', '/data/subdir/something01.txt', '/home', '/home/leo', '/home/leo/t', '/home/other']), + (["-fm:*/something0?.txt"], + ["/data", '/home', '/home/leo', '/home/leo/t', '/home/other']), + (["+/*/something0?.txt", + "-/data"], + ["/data/something00.txt", '/home', '/home/leo', '/home/leo/t', '/home/other']), + (["+fm:*/something00.txt", + "-/data"], + ["/data/something00.txt", '/home', '/home/leo', '/home/leo/t', '/home/other']), + # include /home/leo and exclude the rest of /home: + (["+/home/leo", + "-/home/*"], + ['/data', '/data/something00.txt', '/data/subdir/something01.txt', '/home', '/home/leo', '/home/leo/t']), + # wrong order, /home/leo is already excluded by -/home/*: + (["-/home/*", + "+/home/leo"], + ['/data', '/data/something00.txt', '/data/subdir/something01.txt', '/home']), + (["+fm:/home/leo", + "-/home/"], + ['/data', '/data/something00.txt', '/data/subdir/something01.txt', '/home', '/home/leo', '/home/leo/t']), +]) +def test_inclexcl_patterns_from_file(tmpdir, lines, expected): + files = [ + '/data', '/data/something00.txt', '/data/subdir/something01.txt', + '/home', '/home/leo', '/home/leo/t', '/home/other' + ] + + def evaluate(filename): + matcher = PatternMatcher(fallback=True) + roots = [] + inclexclpatterns = [] + load_pattern_file(open(filename, "rt"), roots, inclexclpatterns) + matcher.add_inclexcl(inclexclpatterns) + return [path for path in files if matcher.match(path)] + + patternfile = tmpdir.join("patterns.txt") + + with patternfile.open("wt") as fh: + fh.write("\n".join(lines)) + + assert evaluate(str(patternfile)) == (files if expected is None else expected) + + @pytest.mark.parametrize("pattern, cls", [ ("", FnmatchPattern), From 5e0c2d4b1118b6a21ead346469c9fe1e0a785c68 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 13 Feb 2017 22:32:12 +0100 Subject: [PATCH 0631/1387] new branching model --- docs/development.rst | 66 ++++++++++++++++++++++++++++++++++++-------- docs/support.rst | 1 + 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/docs/development.rst b/docs/development.rst index b567d37a..f88167a5 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -19,18 +19,7 @@ Some guidance for contributors: - discuss about changes on github issue tracker, IRC or mailing list -- choose the branch you base your changesets on wisely: - - - choose x.y-maint for stuff that should go into next x.y.z release - (it usually gets merged into master branch later also), like: - - - bug fixes (code or docs) - - missing *important* (and preferably small) features - - docs rearrangements (so stuff stays in-sync to avoid merge - troubles in future) - - choose master if that does not apply, like for: - - - developing new features +- make your PRs on the ``master`` branch (see `Branching Model`_ for details) - do clean changesets: @@ -56,6 +45,59 @@ Some guidance for contributors: - wait for review by other developers +Branching model +--------------- + +Borg development happens on the ``master`` branch and uses GitHub pull +requests (if you don't have GitHub or don't want to use it you can +send smaller patches via the borgbackup :ref:`mailing_list` to the maintainers). + +Stable releases are maintained on maintenance branches named x.y-maint, eg. +the maintenance branch of the 1.0.x series is 1.0-maint. + +Most PRs should be made against the ``master`` branch. Only if an +issue affects **only** a particular maintenance branch a PR should be +made against it directly. + +While discussing / reviewing a PR it will be decided whether the +change should be applied to maintenance branch(es). Each maintenance +branch has a corresponding *backport/x.y-maint* label, which will then +be applied. + +Changes that are typically considered for backporting: + +- Data loss, corruption and inaccessibility fixes +- Security fixes +- Forward-compatibility improvements +- Documentation corrections + +.. rubric:: Maintainer part + +From time to time a maintainer will backport the changes for a +maintenance branch, typically before a release or if enough changes +were collected: + +1. Notify others that you're doing this to avoid duplicate work. +2. Branch a backporting branch off the maintenance branch. +3. Cherry pick and backport the changes from each labelled PR, remove + the label for each PR you've backported. +4. Make a PR of the backporting branch against the maintenance branch + for backport review. Mention the backported PRs in this PR, eg: + + Includes changes from #2055 #2057 #2381 + + This way GitHub will automatically show in these PRs where they + were backported. + +.. rubric:: Historic model + +Previously (until release 1.0.10) Borg used a `"merge upwards" +`_ model where +most minor changes and fixes where committed to a maintenance branch +(eg. 1.0-maint), and the maintenance branch(es) were regularly merged +back into the main development branch. This became more and more +troublesome due to merges growing more conflict-heavy and error-prone. + Code and issues --------------- diff --git a/docs/support.rst b/docs/support.rst index 5ee34de9..293264f5 100644 --- a/docs/support.rst +++ b/docs/support.rst @@ -28,6 +28,7 @@ nickname you get by typing "/nick mydesirednickname"): http://webchat.freenode.net/?randomnick=1&channels=%23borgbackup&uio=MTY9dHJ1ZSY5PXRydWUa8 +.. _mailing_list: Mailing list ------------ From 73990b878f053bb8800b9e23de28e60489dfc41a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 14 Feb 2017 18:58:34 +0100 Subject: [PATCH 0632/1387] create: handle BackupOSError on a per-path level in one spot --- src/borg/archiver.py | 165 +++++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 84 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f86fa726..4898731f 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -29,7 +29,7 @@ import borg from . import __version__ from . import helpers from .archive import Archive, ArchiveChecker, ArchiveRecreater, Statistics, is_special -from .archive import BackupOSError +from .archive import BackupOSError, backup_io from .cache import Cache from .constants import * # NOQA from .crc32 import crc32 @@ -396,101 +396,98 @@ class Archiver: def _process(self, archive, cache, matcher, exclude_caches, exclude_if_present, keep_exclude_tags, skip_inodes, path, restrict_dev, read_special=False, dry_run=False, st=None): + """ + Process *path* recursively according to the various parameters. + + *st* (if given) is a *os.stat_result* object for *path*. + + This should only raise on critical errors. Per-item errors must be handled within this method. + """ if not matcher.match(path): self.print_file_status('x', path) return - if st is None: - try: - st = os.lstat(path) - except OSError as e: - self.print_warning('%s: stat: %s', path, e) - return - if (st.st_ino, st.st_dev) in skip_inodes: - return - # if restrict_dev is given, we do not want to recurse into a new filesystem, - # but we WILL save the mountpoint directory (or more precise: the root - # directory of the mounted filesystem that shadows the mountpoint dir). - recurse = restrict_dev is None or st.st_dev == restrict_dev - status = None - # Ignore if nodump flag is set try: - if get_flags(path, st) & stat.UF_NODUMP: - self.print_file_status('x', path) + if st is None: + with backup_io('stat'): + st = os.lstat(path) + if (st.st_ino, st.st_dev) in skip_inodes: return - except OSError as e: - self.print_warning('%s: flags: %s', path, e) - return - if stat.S_ISREG(st.st_mode): - if not dry_run: - try: - status = archive.process_file(path, st, cache, self.ignore_inode) - except BackupOSError as e: - status = 'E' - self.print_warning('%s: %s', path, e) - elif stat.S_ISDIR(st.st_mode): - if recurse: - tag_paths = dir_is_tagged(path, exclude_caches, exclude_if_present) - if tag_paths: - if keep_exclude_tags and not dry_run: - archive.process_dir(path, st) - for tag_path in tag_paths: - self._process(archive, cache, matcher, exclude_caches, exclude_if_present, - keep_exclude_tags, skip_inodes, tag_path, restrict_dev, - read_special=read_special, dry_run=dry_run) + # if restrict_dev is given, we do not want to recurse into a new filesystem, + # but we WILL save the mountpoint directory (or more precise: the root + # directory of the mounted filesystem that shadows the mountpoint dir). + recurse = restrict_dev is None or st.st_dev == restrict_dev + status = None + # Ignore if nodump flag is set + with backup_io('flags'): + if get_flags(path, st) & stat.UF_NODUMP: + self.print_file_status('x', path) return - if not dry_run: - status = archive.process_dir(path, st) - if recurse: - try: - entries = helpers.scandir_inorder(path) - except OSError as e: - status = 'E' - self.print_warning('%s: scandir: %s', path, e) - else: + if stat.S_ISREG(st.st_mode): + if not dry_run: + status = archive.process_file(path, st, cache, self.ignore_inode) + elif stat.S_ISDIR(st.st_mode): + if recurse: + tag_paths = dir_is_tagged(path, exclude_caches, exclude_if_present) + if tag_paths: + if keep_exclude_tags and not dry_run: + archive.process_dir(path, st) + for tag_path in tag_paths: + self._process(archive, cache, matcher, exclude_caches, exclude_if_present, + keep_exclude_tags, skip_inodes, tag_path, restrict_dev, + read_special=read_special, dry_run=dry_run) + return + if not dry_run: + status = archive.process_dir(path, st) + if recurse: + with backup_io('scandir'): + entries = helpers.scandir_inorder(path) for dirent in entries: normpath = os.path.normpath(dirent.path) self._process(archive, cache, matcher, exclude_caches, exclude_if_present, keep_exclude_tags, skip_inodes, normpath, restrict_dev, read_special=read_special, dry_run=dry_run) - elif stat.S_ISLNK(st.st_mode): - if not dry_run: - if not read_special: - status = archive.process_symlink(path, st) - else: - try: - st_target = os.stat(path) - except OSError: - special = False - else: - special = is_special(st_target.st_mode) - if special: - status = archive.process_file(path, st_target, cache) - else: + elif stat.S_ISLNK(st.st_mode): + if not dry_run: + if not read_special: status = archive.process_symlink(path, st) - elif stat.S_ISFIFO(st.st_mode): - if not dry_run: - if not read_special: - status = archive.process_fifo(path, st) - else: - status = archive.process_file(path, st, cache) - elif stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode): - if not dry_run: - if not read_special: - status = archive.process_dev(path, st) - else: - status = archive.process_file(path, st, cache) - elif stat.S_ISSOCK(st.st_mode): - # Ignore unix sockets - return - elif stat.S_ISDOOR(st.st_mode): - # Ignore Solaris doors - return - elif stat.S_ISPORT(st.st_mode): - # Ignore Solaris event ports - return - else: - self.print_warning('Unknown file type: %s', path) - return + else: + try: + st_target = os.stat(path) + except OSError: + special = False + else: + special = is_special(st_target.st_mode) + if special: + status = archive.process_file(path, st_target, cache) + else: + status = archive.process_symlink(path, st) + elif stat.S_ISFIFO(st.st_mode): + if not dry_run: + if not read_special: + status = archive.process_fifo(path, st) + else: + status = archive.process_file(path, st, cache) + elif stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode): + if not dry_run: + if not read_special: + status = archive.process_dev(path, st) + else: + status = archive.process_file(path, st, cache) + elif stat.S_ISSOCK(st.st_mode): + # Ignore unix sockets + return + elif stat.S_ISDOOR(st.st_mode): + # Ignore Solaris doors + return + elif stat.S_ISPORT(st.st_mode): + # Ignore Solaris event ports + return + else: + self.print_warning('Unknown file type: %s', path) + return + except BackupOSError as e: + self.print_warning('%s: %s', path, e) + status = 'E' # Status output if status is None: if not dry_run: From 788b608aa9fb6126dfa386b56760a0a44ce40333 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 14 Feb 2017 23:01:52 +0100 Subject: [PATCH 0633/1387] setup.py build_usage/build_man fixes --- setup.py | 4 ++-- src/borg/archiver.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index d432bb16..a8da0bf0 100644 --- a/setup.py +++ b/setup.py @@ -220,7 +220,7 @@ class build_usage(Command): os.mkdir('docs/usage') # allows us to build docs without the C modules fully loaded during help generation from borg.archiver import Archiver - parser = Archiver(prog='borg').parser + parser = Archiver(prog='borg').build_parser() self.generate_level("", parser, Archiver) @@ -369,7 +369,7 @@ class build_man(Command): os.makedirs('docs/man', exist_ok=True) # allows us to build docs without the C modules fully loaded during help generation from borg.archiver import Archiver - parser = Archiver(prog='borg').parser + parser = Archiver(prog='borg').build_parser() self.generate_level('', parser, Archiver) self.build_topic_pages(Archiver) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 4898731f..5de24d26 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1691,7 +1691,7 @@ class Archiver: print(warning, file=sys.stderr) return args - def build_parser(self, prog=None): + def build_parser(self): def process_epilog(epilog): epilog = textwrap.dedent(epilog).splitlines() try: @@ -1702,7 +1702,7 @@ class Archiver: epilog = [line for line in epilog if not line.startswith('.. man')] return '\n'.join(epilog) - common_parser = argparse.ArgumentParser(add_help=False, prog=prog) + common_parser = argparse.ArgumentParser(add_help=False, prog=self.prog) common_group = common_parser.add_argument_group('Common options') common_group.add_argument('-h', '--help', action='help', help='show this help message and exit') @@ -1743,7 +1743,7 @@ class Archiver: action='store_true', default=False, help='treat part files like normal files (e.g. to list/extract them)') - parser = argparse.ArgumentParser(prog=prog, description='Borg - Deduplicated Backups') + parser = argparse.ArgumentParser(prog=self.prog, description='Borg - Deduplicated Backups') parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__, help='show version number and exit') subparsers = parser.add_subparsers(title='required arguments', metavar='') @@ -3079,7 +3079,7 @@ class Archiver: # We can't use argparse for "serve" since we don't want it to show up in "Available commands" if args: args = self.preprocess_args(args) - parser = self.build_parser(self.prog) + parser = self.build_parser() args = parser.parse_args(args or ['-h']) if args.func == self.do_create: # need at least 1 path but args.paths may also be populated from patterns From 4446577dd5f189fe60f66e59304deb5bba7c83ab Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 14 Feb 2017 23:11:21 +0100 Subject: [PATCH 0634/1387] docs: less spacing between options --- docs/borg_theme/css/borg.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 3fa45332..6c82f9b1 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -24,3 +24,7 @@ #usage dt code { font-weight: normal; } + +#usage dl dl dd { + margin-bottom: 0.5em; +} From 9e0ea92a9da5f043535e5cb187bd36ede62629cd Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 16 Feb 2017 16:06:15 +0100 Subject: [PATCH 0635/1387] docs: FAQ by categories as proposed by @anarcat in #1802 Usage & Limitations Security Common issues Miscellaneous (only three items, two fork-related) Note: This does not change any links to FAQ items. --- docs/faq.rst | 133 ++++++++++++++++++++++++++++----------------------- 1 file changed, 72 insertions(+), 61 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index c41b1a95..b3eb8f4e 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -5,6 +5,9 @@ Frequently asked questions ========================== +Usage & Limitations +################### + Can I backup VM disk images? ---------------------------- @@ -105,7 +108,6 @@ Are there other known limitations? An easy workaround is to create multiple archives with less items each. See also the :ref:`archive_limitation` and :issue:`1452`. - Why is my backup bigger than with attic? Why doesn't |project_name| do compression by default? ---------------------------------------------------------------------------------------------- @@ -120,6 +122,70 @@ decision about whether you want to use compression, which algorithm and which level you want to use. This is why compression defaults to none. +If a backup stops mid-way, does the already-backed-up data stay there? +---------------------------------------------------------------------- + +Yes, |project_name| supports resuming backups. + +During a backup a special checkpoint archive named ``.checkpoint`` +is saved every checkpoint interval (the default value for this is 30 +minutes) containing all the data backed-up until that point. + +This checkpoint archive is a valid archive, +but it is only a partial backup (not all files that you wanted to backup are +contained in it). Having it in the repo until a successful, full backup is +completed is useful because it references all the transmitted chunks up +to the checkpoint. This means that in case of an interruption, you only need to +retransfer the data since the last checkpoint. + +If a backup was interrupted, you do not need to do any special considerations, +just invoke ``borg create`` as you always do. You may use the same archive name +as in previous attempt or a different one (e.g. if you always include the current +datetime), it does not matter. + +|project_name| always does full single-pass backups, so it will start again +from the beginning - but it will be much faster, because some of the data was +already stored into the repo (and is still referenced by the checkpoint +archive), so it does not need to get transmitted and stored again. + +Once your backup has finished successfully, you can delete all +``.checkpoint`` archives. If you run ``borg prune``, it will +also care for deleting unneeded checkpoints. + +Note: the checkpointing mechanism creates hidden, partial files in an archive, +so that checkpoints even work while a big file is being processed. +They are named ``.borg_part_`` and all operations usually ignore +these files, but you can make them considered by giving the option +``--consider-part-files``. You usually only need that option if you are +really desperate (e.g. if you have no completed backup of that file and you'ld +rather get a partial file extracted than nothing). You do **not** want to give +that option under any normal circumstances. + +Can |project_name| add redundancy to the backup data to deal with hardware malfunction? +--------------------------------------------------------------------------------------- + +No, it can't. While that at first sounds like a good idea to defend against +some defect HDD sectors or SSD flash blocks, dealing with this in a +reliable way needs a lot of low-level storage layout information and +control which we do not have (and also can't get, even if we wanted). + +So, if you need that, consider RAID or a filesystem that offers redundant +storage or just make backups to different locations / different hardware. + +See also :issue:`225`. + +Can |project_name| verify data integrity of a backup archive? +------------------------------------------------------------- + +Yes, if you want to detect accidental data damage (like bit rot), use the +``check`` operation. It will notice corruption using CRCs and hashes. +If you want to be able to detect malicious tampering also, use an encrypted +repo. It will then be able to check using CRCs and HMACs. + + +Security +######## + How can I specify the encryption passphrase programmatically? ------------------------------------------------------------- @@ -210,6 +276,9 @@ Send a private email to the :ref:`security-contact` if you think you have discovered a security issue. Please disclose security issues responsibly. +Common issues +############# + Why do I get "connection closed by remote" after a while? --------------------------------------------------------- @@ -269,45 +338,6 @@ This has some pros and cons, though: The long term plan to improve this is called "borgception", see :issue:`474`. -If a backup stops mid-way, does the already-backed-up data stay there? ----------------------------------------------------------------------- - -Yes, |project_name| supports resuming backups. - -During a backup a special checkpoint archive named ``.checkpoint`` -is saved every checkpoint interval (the default value for this is 30 -minutes) containing all the data backed-up until that point. - -This checkpoint archive is a valid archive, -but it is only a partial backup (not all files that you wanted to backup are -contained in it). Having it in the repo until a successful, full backup is -completed is useful because it references all the transmitted chunks up -to the checkpoint. This means that in case of an interruption, you only need to -retransfer the data since the last checkpoint. - -If a backup was interrupted, you do not need to do any special considerations, -just invoke ``borg create`` as you always do. You may use the same archive name -as in previous attempt or a different one (e.g. if you always include the current -datetime), it does not matter. - -|project_name| always does full single-pass backups, so it will start again -from the beginning - but it will be much faster, because some of the data was -already stored into the repo (and is still referenced by the checkpoint -archive), so it does not need to get transmitted and stored again. - -Once your backup has finished successfully, you can delete all -``.checkpoint`` archives. If you run ``borg prune``, it will -also care for deleting unneeded checkpoints. - -Note: the checkpointing mechanism creates hidden, partial files in an archive, -so that checkpoints even work while a big file is being processed. -They are named ``.borg_part_`` and all operations usually ignore -these files, but you can make them considered by giving the option -``--consider-part-files``. You usually only need that option if you are -really desperate (e.g. if you have no completed backup of that file and you'ld -rather get a partial file extracted than nothing). You do **not** want to give -that option under any normal circumstances. - How can I backup huge file(s) over a unstable connection? --------------------------------------------------------- @@ -338,27 +368,6 @@ If you run into that, try this: the parent directory (or even everything) - mount the repo using FUSE and use some file manager -Can |project_name| add redundancy to the backup data to deal with hardware malfunction? ---------------------------------------------------------------------------------------- - -No, it can't. While that at first sounds like a good idea to defend against -some defect HDD sectors or SSD flash blocks, dealing with this in a -reliable way needs a lot of low-level storage layout information and -control which we do not have (and also can't get, even if we wanted). - -So, if you need that, consider RAID or a filesystem that offers redundant -storage or just make backups to different locations / different hardware. - -See also :issue:`225`. - -Can |project_name| verify data integrity of a backup archive? -------------------------------------------------------------- - -Yes, if you want to detect accidental data damage (like bit rot), use the -``check`` operation. It will notice corruption using CRCs and hashes. -If you want to be able to detect malicious tampering also, use an encrypted -repo. It will then be able to check using CRCs and HMACs. - .. _a_status_oddity: I am seeing 'A' (added) status for a unchanged file!? @@ -469,6 +478,8 @@ maybe open an issue in their issue tracker. Do not file an issue in the If you can reproduce the issue with the proven filesystem, please file an issue in the |project_name| issue tracker about that. +Miscellaneous +############# Requirements for the borg single-file binary, esp. (g)libc? ----------------------------------------------------------- From 3d79d441a52f8440a32f35afcf7d8b69577b49c4 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 16 Feb 2017 16:49:26 +0100 Subject: [PATCH 0636/1387] faq: update Which file types, attributes, etc. are *not* preserved? ext4 immutable has been supported for a while. --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index b3eb8f4e..5c8cd775 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -96,7 +96,7 @@ Which file types, attributes, etc. are *not* preserved? backed up as (deduplicated and compressed) runs of zero bytes. Archive extraction has optional support to extract all-zero chunks as holes in a sparse file. - * filesystem specific attributes, like ext4 immutable bit, see :issue:`618`. + * Some filesystem specific attributes, like btrfs NOCOW, see :ref:`platforms`. Are there other known limitations? ---------------------------------- From caeff71a6cb92b2225237771d25133bf119d7ccb Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 16 Feb 2017 14:25:28 +0100 Subject: [PATCH 0637/1387] init: mandatory --encryption arg --- src/borg/archiver.py | 3 +-- src/borg/key.py | 4 +++- src/borg/testsuite/archiver.py | 42 ++++++++++++++++------------------ 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 5de24d26..9dbbd202 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1850,9 +1850,8 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False), help='repository to create') - subparser.add_argument('-e', '--encryption', dest='encryption', + subparser.add_argument('-e', '--encryption', dest='encryption', required=True, choices=('none', 'keyfile', 'repokey', 'keyfile-blake2', 'repokey-blake2', 'authenticated'), - default=None, help='select encryption key mode') subparser.add_argument('-a', '--append-only', dest='append_only', action='store_true', help='create an append-only mode repository') diff --git a/src/borg/key.py b/src/borg/key.py index e5d736ea..4358c644 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -98,8 +98,10 @@ def key_creator(repository, args): return Blake2RepoKey.create(repository, args) elif args.encryption == 'authenticated': return AuthenticatedKey.create(repository, args) - else: + elif args.encryption == 'none': return PlaintextKey.create(repository, args) + else: + raise ValueError('Invalid encryption mode "%s"' % args.encryption) def key_factory(repository, manifest_data): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 2b0709be..adf85b7d 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -79,7 +79,13 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw): archiver = Archiver() archiver.prerun_checks = lambda *args: None archiver.exit_code = EXIT_SUCCESS - args = archiver.parse_args(list(args)) + try: + args = archiver.parse_args(list(args)) + # argparse parsing may raise SystemExit when the command line is bad or + # actions that abort early (eg. --help) where given. Catch this and return + # the error code as-if we invoked a Borg binary. + except SystemExit as e: + return e.code, output.getvalue() ret = archiver.run(args) return ret, output.getvalue() finally: @@ -879,16 +885,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_without_root(self): """test create without a root""" - self.cmd('init', self.repository_location) - args = ['create', self.repository_location + '::test'] - if self.FORK_DEFAULT: - self.cmd(*args, exit_code=2) - else: - self.assert_raises(SystemExit, lambda: self.cmd(*args)) + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test', exit_code=2) def test_create_pattern_root(self): """test create with only a root pattern""" - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) output = self.cmd('create', '-v', '--list', '--pattern=R input', self.repository_location + '::test') @@ -897,7 +899,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_pattern(self): """test file patterns during create""" - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('file_important', size=1024 * 80) @@ -910,7 +912,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('x input/file2', output) def test_extract_pattern_opt(self): - self.cmd('init', self.repository_location) + self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('file_important', size=1024 * 80) @@ -921,9 +923,6 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.repository_location + '::test') self.assert_equal(sorted(os.listdir('output/input')), ['file_important']) - def test_exclude_caches(self): - self.cmd('init', self.repository_location) - def _assert_test_caches(self): with changedir('output'): self.cmd('extract', self.repository_location + '::test') @@ -1516,12 +1515,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('break-lock', self.repository_location) def test_usage(self): - if self.FORK_DEFAULT: - self.cmd(exit_code=0) - self.cmd('-h', exit_code=0) - else: - self.assert_raises(SystemExit, lambda: self.cmd()) - self.assert_raises(SystemExit, lambda: self.cmd('-h')) + self.cmd() + self.cmd('-h') def test_help(self): assert 'Borg' in self.cmd('help') @@ -1777,6 +1772,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', '--encryption=repokey', self.repository_location, exit_code=1) assert not os.path.exists(self.repository_location) + def test_init_requires_encryption_option(self): + self.cmd('init', self.repository_location, exit_code=2) + def check_cache(self): # First run a regular borg check self.cmd('check', self.repository_location) @@ -2476,10 +2474,10 @@ class RemoteArchiverTestCase(ArchiverTestCase): path_prefix = os.path.dirname(self.repository_path) # restrict to repo directory's parent directory: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', path_prefix]): - self.cmd('init', self.repository_location + '_2') + self.cmd('init', '--encryption=repokey', self.repository_location + '_2') # restrict to repo directory's parent directory and another directory: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]): - self.cmd('init', self.repository_location + '_3') + self.cmd('init', '--encryption=repokey', self.repository_location + '_3') @unittest.skip('only works locally') def test_debug_put_get_delete_obj(self): @@ -2682,7 +2680,7 @@ def test_get_args(): assert args.restrict_to_paths == ['/p1', '/p2'] # trying to cheat - try to execute different subcommand args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ], - 'borg init /') + 'borg init --encryption=repokey /') assert args.func == archiver.do_serve From 8e8d9d3f48510a1db982bb019202745369ec990f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 14 Feb 2017 21:40:20 +0100 Subject: [PATCH 0638/1387] docs: add edited "Cryptography in Borg" and "Remote RPC protocol security" sections The former section is a bit older (Nov 2016) and has been the piece responsible for finding CVE-2016-10099, since while writing it I wondered how the manifest was authenticated to actually *be* the manifest. Well. There it is ;) It has been edited to final form only recently and should now be ready for review. The latter section is new. --- docs/internals.rst | 4 + docs/security.rst | 279 +++++++++++++++++++++++++++++++++++++++++++++ docs/usage.rst | 1 + 3 files changed, 284 insertions(+) create mode 100644 docs/security.rst diff --git a/docs/internals.rst b/docs/internals.rst index 138761b2..d8d482a3 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -5,6 +5,10 @@ Internals ========= +.. toctree:: + + security + This page documents the internal data structures and storage mechanisms of |project_name|. It is partly based on `mailing list discussion about internals`_ and also on static code analysis. diff --git a/docs/security.rst b/docs/security.rst new file mode 100644 index 00000000..f84b321c --- /dev/null +++ b/docs/security.rst @@ -0,0 +1,279 @@ + +.. somewhat surprisingly the "bash" highlighter gives nice results with + the pseudo-code notation used in the "Encryption" section. + +.. highlight:: bash + +======== +Security +======== + +Cryptography in Borg +==================== + +Attack model +------------ + +The attack model of Borg is that the environment of the client process +(e.g. ``borg create``) is trusted and the repository (server) is not. The +attacker has any and all access to the repository, including interactive +manipulation (man-in-the-middle) for remote repositories. + +Furthermore the client environment is assumed to be persistent across +attacks (practically this means that the security database cannot be +deleted between attacks). + +Under these circumstances Borg guarantees that the attacker cannot + +1. modify the data of any archive without the client detecting the change +2. rename, remove or add an archive without the client detecting the change +3. recover plain-text data +4. recover definite (heuristics based on access patterns are possible) + structural information such as the object graph (which archives + refer to what chunks) + +The attacker can always impose a denial of service per definition (he could +forbid connections to the repository, or delete it entirely). + +Structural Authentication +------------------------- + +Borg is fundamentally based on an object graph structure (see :ref:`internals`), +where the root object is called the manifest. + +Borg follows the `Horton principle`_, which states that +not only the message must be authenticated, but also its meaning (often +expressed through context), because every object used is referenced by a +parent object through its object ID up to the manifest. The object ID in +Borg is a MAC of the object's plaintext, therefore this ensures that +an attacker cannot change the context of an object without forging the MAC. + +In other words, the object ID itself only authenticates the plaintext of the +object and not its context or meaning. The latter is established by a different +object referring to an object ID, thereby assigning a particular meaning to +an object. For example, an archive item contains a list of object IDs that +represent packed file metadata. On their own it's not clear that these objects +would represent what they do, but by the archive item referring to them +in a particular part of its own data structure assigns this meaning. + +This results in a directed acyclic graph of authentication from the manifest +to the data chunks of individual files. + +.. rubric:: Authenticating the manifest + +Since the manifest has a fixed ID (000...000) the aforementioned authentication +does not apply to it, indeed, cannot apply to it; it is impossible to authenticate +the root node of a DAG through its edges, since the root node has no incoming edges. + +With the scheme as described so far an attacker could easily replace the manifest, +therefore Borg includes a tertiary authentication mechanism (TAM) that is applied +to the manifest since version 1.0.9 (see :ref:`tam_vuln`). + +TAM works by deriving a separate key through HKDF_ from the other encryption and +authentication keys and calculating the HMAC of the metadata to authenticate [#]_:: + + # RANDOM(n) returns n random bytes + salt = RANDOM(64) + + ikm = id_key || enc_key || enc_hmac_key + # *context* depends on the operation, for manifest authentication it is + # the ASCII string "borg-metadata-authentication-manifest". + tam_key = HKDF-SHA-512(ikm, salt, context) + + # *data* is a dict-like structure + data[hmac] = zeroes + packed = pack(data) + data[hmac] = HMAC(tam_key, packed) + packed_authenticated = pack(data) + +Since an attacker cannot gain access to this key and also cannot make the +client authenticate arbitrary data using this mechanism, the attacker is unable +to forge the authentication. + +This effectively 'anchors' the manifest to the key, which is controlled by the +client, thereby anchoring the entire DAG, making it impossible for an attacker +to add, remove or modify any part of the DAG without Borg being able to detect +the tampering. + +Note that when using BORG_PASSPHRASE the attacker cannot swap the *entire* +repository against a new repository with e.g. repokey mode and no passphrase, +because Borg will abort access when BORG_PASSPRHASE is incorrect. + +However, interactively a user might not notice this kind of attack +immediately, if she assumes that the reason for the absent passphrase +prompt is a set BORG_PASSPHRASE. See issue :issue:`2169` for details. + +.. [#] The reason why the authentication tag is stored in the packed + data itself is that older Borg versions can still read the + manifest this way, while a changed layout would have broken + compatibility. + +Encryption +---------- + +Encryption is currently based on the Encrypt-then-MAC construction, +which is generally seen as the most robust way to create an authenticated +encryption scheme from encryption and message authentication primitives. + +Every operation (encryption, MAC / authentication, chunk ID derivation) +uses independent, random keys generated by `os.urandom`_ [#]_. + +Borg does not support unauthenticated encryption -- only authenticated encryption +schemes are supported. No unauthenticated encryption schemes will be added +in the future. + +Depending on the chosen mode (see :ref:`borg_init`) different primitives are used: + +- The actual encryption is currently always AES-256 in CTR mode. The + counter is added in plaintext, since it is needed for decryption, + and is also tracked locally on the client to avoid counter reuse. + +- The authentication primitive is either HMAC-SHA-256 or BLAKE2b-256 + in a keyed mode. HMAC-SHA-256 uses 256 bit keys, while BLAKE2b-256 + uses 512 bit keys. + + The latter is secure not only because BLAKE2b itself is not + susceptible to `length extension`_, but also since it truncates the + hash output from 512 bits to 256 bits, which would make the + construction safe even if BLAKE2b were broken regarding length + extension or similar attacks. + +- The primitive used for authentication is always the same primitive + that is used for deriving the chunk ID, but they are always + used with independent keys. + +Encryption:: + + id = AUTHENTICATOR(id_key, data) + compressed = compress(data) + + iv = reserve_iv() + encrypted = AES-256-CTR(enc_key, 8-null-bytes || iv, compressed) + authenticated = type-byte || AUTHENTICATOR(enc_hmac_key, encrypted) || iv || encrypted + + +Decryption:: + + # Given: input *authenticated* data, possibly a *chunk-id* to assert + type-byte, mac, iv, encrypted = SPLIT(authenticated) + + ASSERT(type-byte is correct) + ASSERT( CONSTANT-TIME-COMPARISON( mac, AUTHENTICATOR(enc_hmac_key, encrypted) ) ) + + decrypted = AES-256-CTR(enc_key, 8-null-bytes || iv, encrypted) + decompressed = decompress(decrypted) + + ASSERT( CONSTANT-TIME-COMPARISON( chunk-id, AUTHENTICATOR(id_key, decompressed) ) ) + +.. [#] Using the :ref:`borg key migrate-to-repokey ` + command a user can convert repositories created using Attic in "passphrase" + mode to "repokey" mode. In this case the keys were directly derived from + the user's passphrase at some point using PBKDF2. + + Borg does not support "passphrase" mode otherwise any more. + +Offline key security +-------------------- + +Borg cannot secure the key material while it is running, because the keys +are needed in plain to decrypt/encrypt repository objects. + +For offline storage of the encryption keys they are encrypted with a +user-chosen passphrase. + +A 256 bit key encryption key (KEK) is derived from the passphrase +using PBKDF2-HMAC-SHA256 with a random 256 bit salt which is then used +to Encrypt-then-MAC a packed representation of the keys with +AES-256-CTR with a constant initialization vector of 0 (this is the +same construction used for Encryption_ with HMAC-SHA-256). + +The resulting MAC is stored alongside the ciphertext, which is +converted to base64 in its entirety. + +This base64 blob (commonly referred to as *keyblob*) is then stored in +the key file or in the repository config (keyfile and repokey modes +respectively). + +This scheme, and specifically the use of a constant IV with the CTR +mode, is secure because an identical passphrase will result in a +different derived KEK for every encryption due to the salt. + +Implementations used +-------------------- + +We do not implement cryptographic primitives ourselves, but rely +on widely used libraries providing them: + +- AES-CTR and HMAC-SHA-256 from OpenSSL 1.0 / 1.1 are used, + which is also linked into the static binaries we provide. + We think this is not an additional risk, since we don't ever + use OpenSSL's networking, TLS or X.509 code, but only their + primitives implemented in libcrypto. +- SHA-256 and SHA-512 from Python's hashlib_ standard library module are used +- HMAC, PBKDF2 and a constant-time comparison from Python's hmac_ standard + library module is used. +- BLAKE2b is either provided by the system's libb2, an official implementation, + or a bundled copy of the BLAKE2 reference implementation (written in C). + +Implemented cryptographic constructions are: + +- Encrypt-then-MAC based on AES-256-CTR and either HMAC-SHA-256 + or keyed BLAKE2b256 as described above under Encryption_. +- HKDF_-SHA-512 + +.. _Horton principle: https://en.wikipedia.org/wiki/Horton_Principle +.. _HKDF: https://tools.ietf.org/html/rfc5869 +.. _length extension: https://en.wikipedia.org/wiki/Length_extension_attack +.. _hashlib: https://docs.python.org/3/library/hashlib.html +.. _hmac: https://docs.python.org/3/library/hmac.html +.. _os.urandom: https://docs.python.org/3/library/os.html#os.urandom + +Remote RPC protocol security +============================ + +.. note:: This section could be further expanded / detailed. + +The RPC protocol is fundamentally based on msgpack'd messages exchanged +over an encrypted SSH channel (the system's SSH client is used for this +by piping data from/to it). + +This means that the authorization and transport security properties +are inherited from SSH and the configuration of the SSH client +and the SSH server. Therefore the remainder of this section +will focus on the security of the RPC protocol within Borg. + +The assumed worst-case a server can inflict to a client is a +denial of repository service. + +The situation were a server can create a general DoS on the client +should be avoided, but might be possible by e.g. forcing the client to +allocate large amounts of memory to decode large messages (or messages +that merely indicate a large amount of data follows). See issue +:issue:`2139` for details. + +We believe that other kinds of attacks, especially critical vulnerabilities +like remote code execution are inhibited by the design of the protocol: + +1. The server cannot send requests to the client on its own accord, + it only can send responses. This avoids "unexpected inversion of control" + issues. +2. msgpack serialization does not allow embedding or referencing code that + is automatically executed. Incoming messages are unpacked by the msgpack + unpacker into native Python data structures (like tuples and dictionaries), + which are then passed to the rest of the program. + + Additional verification of the correct form of the responses could be implemented. +3. Remote errors are presented in two forms: + + 1. A simple plain-text *stderr* channel. A prefix string indicates the kind of message + (e.g. WARNING, INFO, ERROR), which is used to suppress it according to the + log level selected in the client. + + A server can send arbitrary log messages, which may confuse a user. However, + log messages are only processed when server requests are in progress, therefore + the server cannot interfere / confuse with security critical dialogue like + the password prompt. + 2. Server-side exceptions passed over the main data channel. These follow the + general pattern of server-sent responses and are sent instead of response data + for a request. + diff --git a/docs/usage.rst b/docs/usage.rst index 2ad55d02..d11aff60 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -427,6 +427,7 @@ Examples converting borg 0.xx to borg current no key file found for repository +.. _borg_key_migrate-to-repokey: Upgrading a passphrase encrypted attic repo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From b05893e723905a3413faf5a190f6db8d4a9ee8f3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 17 Feb 2017 05:00:37 +0100 Subject: [PATCH 0639/1387] borg rpc: use limited msgpack.Unpacker, fixes #2139 we do not trust the remote, so we are careful unpacking its responses. the remote could return manipulated msgpack data that announces e.g. a huge array or map or string. the local would then need to allocate huge amounts of RAM in expectation of that data (no matter whether really that much is coming or not). by using limits in the Unpacker, a ValueError will be raised if unexpected amounts of data shall get unpacked. memory DoS will be avoided. --- src/borg/archive.py | 4 ++-- src/borg/archiver.py | 4 ++-- src/borg/remote.py | 28 ++++++++++++++++++++++++---- src/borg/repository.py | 2 ++ 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index f499f923..c18829ed 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -39,7 +39,7 @@ from .item import Item, ArchiveItem from .key import key_factory from .platform import acl_get, acl_set, set_flags, get_flags, swidth from .remote import cache_if_remote -from .repository import Repository +from .repository import Repository, LIST_SCAN_LIMIT has_lchmod = hasattr(os, 'lchmod') @@ -1060,7 +1060,7 @@ class ArchiveChecker: self.chunks = ChunkIndex(capacity) marker = None while True: - result = self.repository.list(limit=10000, marker=marker) + result = self.repository.list(limit=LIST_SCAN_LIMIT, marker=marker) if not result: break marker = result[-1] diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 9dbbd202..37157680 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -57,7 +57,7 @@ from .key import key_creator, tam_required_file, tam_required, RepoKey, Passphra from .keymanager import KeyManager from .platform import get_flags, umount, get_process_id from .remote import RepositoryServer, RemoteRepository, cache_if_remote -from .repository import Repository +from .repository import Repository, LIST_SCAN_LIMIT from .selftest import selftest from .upgrader import AtticRepositoryUpgrader, BorgRepositoryUpgrader @@ -1305,7 +1305,7 @@ class Archiver: marker = None i = 0 while True: - result = repository.list(limit=10000, marker=marker) + result = repository.list(limit=LIST_SCAN_LIMIT, marker=marker) if not result: break marker = result[-1] diff --git a/src/borg/remote.py b/src/borg/remote.py index 2567e0ea..b1338212 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -23,7 +23,7 @@ from .helpers import sysinfo from .helpers import bin_to_hex from .helpers import replace_placeholders from .helpers import yes -from .repository import Repository +from .repository import Repository, MAX_OBJECT_SIZE, LIST_SCAN_LIMIT from .version import parse_version, format_version from .logger import create_logger @@ -57,6 +57,27 @@ def os_write(fd, data): return amount +def get_limited_unpacker(kind): + """return a limited Unpacker because we should not trust msgpack data received from remote""" + args = dict(use_list=False, # return tuples, not lists + max_bin_len=0, # not used + max_ext_len=0, # not used + max_buffer_size=3 * max(BUFSIZE, MAX_OBJECT_SIZE), + max_str_len=MAX_OBJECT_SIZE, # a chunk or other repo object + ) + if kind == 'server': + args.update(dict(max_array_len=100, # misc. cmd tuples + max_map_len=100, # misc. cmd dicts + )) + elif kind == 'client': + args.update(dict(max_array_len=LIST_SCAN_LIMIT, # result list from repo.list() / .scan() + max_map_len=100, # misc. result dicts + )) + else: + raise ValueError('kind must be "server" or "client"') + return msgpack.Unpacker(**args) + + class ConnectionClosed(Error): """Connection closed by remote host""" @@ -185,7 +206,7 @@ class RepositoryServer: # pragma: no cover # Make stderr blocking fl = fcntl.fcntl(stderr_fd, fcntl.F_GETFL) fcntl.fcntl(stderr_fd, fcntl.F_SETFL, fl & ~os.O_NONBLOCK) - unpacker = msgpack.Unpacker(use_list=False) + unpacker = get_limited_unpacker('server') while True: r, w, es = select.select([stdin_fd], [], [], 10) if r: @@ -487,8 +508,7 @@ class RemoteRepository: self.ignore_responses = set() self.responses = {} self.ratelimit = SleepingBandwidthLimiter(args.remote_ratelimit * 1024 if args and args.remote_ratelimit else 0) - - self.unpacker = msgpack.Unpacker(use_list=False) + self.unpacker = get_limited_unpacker('client') self.server_version = parse_version('1.0.8') # fallback version if server is too old to send version information self.p = None testing = location.host == '__testsuite__' diff --git a/src/borg/repository.py b/src/borg/repository.py index 4183473e..23587355 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -33,6 +33,8 @@ TAG_PUT = 0 TAG_DELETE = 1 TAG_COMMIT = 2 +LIST_SCAN_LIMIT = 10000 # repo.list() / .scan() result count limit the borg client uses + FreeSpace = partial(defaultdict, int) From 5aa74abedffcc6862e28228bf0b7af56fb2f4e69 Mon Sep 17 00:00:00 2001 From: Abogical Date: Fri, 17 Feb 2017 14:28:39 +0200 Subject: [PATCH 0640/1387] Add dsize and dcsize keys These keys shows the amount of deduplicated size and compressed size of each file in the archive. --- src/borg/helpers.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index d6c71d05..a96049fb 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1425,12 +1425,14 @@ class ItemFormatter(BaseFormatter): 'source': 'link target for links (identical to linktarget)', 'extra': 'prepends {source} with " -> " for soft links and " link to " for hard links', 'csize': 'compressed size', + 'dsize': 'deduplicated size', + 'dcsize': 'deduplicated compressed size', 'num_chunks': 'number of chunks in this file', 'unique_chunks': 'number of unique chunks in this file', } KEY_GROUPS = ( ('type', 'mode', 'uid', 'gid', 'user', 'group', 'path', 'bpath', 'source', 'linktarget', 'flags'), - ('size', 'csize', 'num_chunks', 'unique_chunks'), + ('size', 'csize', 'dsize', 'dcsize', 'num_chunks', 'unique_chunks'), ('mtime', 'ctime', 'atime', 'isomtime', 'isoctime', 'isoatime'), tuple(sorted(hashlib.algorithms_guaranteed)), ('archiveid', 'archivename', 'extra'), @@ -1479,6 +1481,8 @@ class ItemFormatter(BaseFormatter): self.call_keys = { 'size': self.calculate_size, 'csize': self.calculate_csize, + 'dsize': self.calculate_dsize, + 'dcsize': self.calculate_dcsize, 'num_chunks': self.calculate_num_chunks, 'unique_chunks': self.calculate_unique_chunks, 'isomtime': partial(self.format_time, 'mtime'), @@ -1540,6 +1544,14 @@ class ItemFormatter(BaseFormatter): def calculate_csize(self, item): return sum(c.csize for c in item.get('chunks', [])) + def calculate_dsize(self, item): + chunk_index = self.archive.cache.chunks + return sum(c.size for c in item.get('chunks', []) if chunk_index[c.id].refcount == 1) + + def calculate_dcsize(self, item): + chunk_index = self.archive.cache.chunks + return sum(c.csize for c in item.get('chunks', []) if chunk_index[c.id].refcount == 1) + def hash_item(self, hash_function, item): if 'chunks' not in item: return "" From 59571115a17c54b4724384466c4d04eeb9793d06 Mon Sep 17 00:00:00 2001 From: Abogical Date: Fri, 17 Feb 2017 15:26:14 +0200 Subject: [PATCH 0641/1387] Add tests for dsize and dcsize --- src/borg/testsuite/archiver.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index d720af46..03e647c2 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1298,9 +1298,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', self.repository_location) test_archive = self.repository_location + '::test' self.cmd('create', '-C', 'lz4', test_archive, 'input') - output = self.cmd('list', '--format', '{size} {csize} {path}{NL}', test_archive) - size, csize, path = output.split("\n")[1].split(" ") + output = self.cmd('list', '--format', '{size} {csize} {dsize} {dcsize} {path}{NL}', test_archive) + size, csize, dsize, dcsize, path = output.split("\n")[1].split(" ") assert int(csize) < int(size) + assert int(dcsize) < int(dsize) + assert int(dsize) <= int(size) + assert int(dcsize) <= int(csize) def _get_sizes(self, compression, compressible, size=10000): if compressible: From 6ed0746934a95b0cfd8504fdfe1354169598d674 Mon Sep 17 00:00:00 2001 From: Abogical Date: Fri, 17 Feb 2017 17:26:02 +0200 Subject: [PATCH 0642/1387] Count non-unique chunks deduplicated sizes --- src/borg/helpers.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index a96049fb..502ab1e6 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -19,7 +19,7 @@ import time import unicodedata import uuid from binascii import hexlify -from collections import namedtuple, deque, abc +from collections import namedtuple, deque, abc, Counter from datetime import datetime, timezone, timedelta from fnmatch import translate from functools import wraps, partial, lru_cache @@ -1546,11 +1546,15 @@ class ItemFormatter(BaseFormatter): def calculate_dsize(self, item): chunk_index = self.archive.cache.chunks - return sum(c.size for c in item.get('chunks', []) if chunk_index[c.id].refcount == 1) + chunks = item.get('chunks', []) + chunks_counter = Counter(c.id for c in chunks) + return sum(c.size for c in chunks if chunk_index[c.id].refcount == chunks_counter[c.id]) def calculate_dcsize(self, item): chunk_index = self.archive.cache.chunks - return sum(c.csize for c in item.get('chunks', []) if chunk_index[c.id].refcount == 1) + chunks = item.get('chunks', []) + chunks_counter = Counter(c.id for c in chunks) + return sum(c.csize for c in chunks if chunk_index[c.id].refcount == chunks_counter[c.id]) def hash_item(self, hash_function, item): if 'chunks' not in item: From 31f3ddf50303f603c9ab38354cd9b102bd401942 Mon Sep 17 00:00:00 2001 From: Abogical Date: Fri, 17 Feb 2017 19:12:01 +0200 Subject: [PATCH 0643/1387] Join the hall of fame --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index bc7a8c7c..9f469cd5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -10,6 +10,7 @@ Borg authors ("The Borg Collective") - Marian Beermann - Daniel Reichelt - Lauri Niskanen +- Abdel-Rahman A. (Abogical) Borg is a fork of Attic. From 6a25b6bdfac5d55f86be5cc64ec951a0cd2851a8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 Feb 2017 07:15:53 +0100 Subject: [PATCH 0644/1387] update docs about limited msgpack Unpacker for RPC code --- docs/security.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/security.rst b/docs/security.rst index f84b321c..5688744e 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -248,8 +248,8 @@ denial of repository service. The situation were a server can create a general DoS on the client should be avoided, but might be possible by e.g. forcing the client to allocate large amounts of memory to decode large messages (or messages -that merely indicate a large amount of data follows). See issue -:issue:`2139` for details. +that merely indicate a large amount of data follows). The RPC protocol +code uses a limited msgpack Unpacker to prohibit this. We believe that other kinds of attacks, especially critical vulnerabilities like remote code execution are inhibited by the design of the protocol: From cd3cbee962f9b6d761e1123ce293337a68d32f15 Mon Sep 17 00:00:00 2001 From: Abogical Date: Sat, 18 Feb 2017 01:56:18 +0200 Subject: [PATCH 0645/1387] Refactor unique chunks summing --- src/borg/helpers.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 502ab1e6..d45ae887 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1481,8 +1481,8 @@ class ItemFormatter(BaseFormatter): self.call_keys = { 'size': self.calculate_size, 'csize': self.calculate_csize, - 'dsize': self.calculate_dsize, - 'dcsize': self.calculate_dcsize, + 'dsize': partial(self.sum_unique_chunks_metadata, lambda chunk: chunk.size), + 'dcsize': partial(self.sum_unique_chunks_metadata, lambda chunk: chunk.csize), 'num_chunks': self.calculate_num_chunks, 'unique_chunks': self.calculate_unique_chunks, 'isomtime': partial(self.format_time, 'mtime'), @@ -1531,6 +1531,20 @@ class ItemFormatter(BaseFormatter): item_data[key] = self.call_keys[key](item) return item_data + def sum_unique_chunks_metadata(self, metadata_func, item): + """ + sum unique chunks metadata, a unique chunk is a chunk which is referenced globally as often as it is in the + item + + item: The item to sum its unique chunks' metadata + metadata_func: A function that takes a parameter of type ChunkIndexEntry and returns a number, used to return + the metadata needed from the chunk + """ + chunk_index = self.archive.cache.chunks + chunks = item.get('chunks', []) + chunks_counter = Counter(c.id for c in chunks) + return sum(metadata_func(c) for c in chunks if chunk_index[c.id].refcount == chunks_counter[c.id]) + def calculate_num_chunks(self, item): return len(item.get('chunks', [])) @@ -1544,18 +1558,6 @@ class ItemFormatter(BaseFormatter): def calculate_csize(self, item): return sum(c.csize for c in item.get('chunks', [])) - def calculate_dsize(self, item): - chunk_index = self.archive.cache.chunks - chunks = item.get('chunks', []) - chunks_counter = Counter(c.id for c in chunks) - return sum(c.size for c in chunks if chunk_index[c.id].refcount == chunks_counter[c.id]) - - def calculate_dcsize(self, item): - chunk_index = self.archive.cache.chunks - chunks = item.get('chunks', []) - chunks_counter = Counter(c.id for c in chunks) - return sum(c.csize for c in chunks if chunk_index[c.id].refcount == chunks_counter[c.id]) - def hash_item(self, hash_function, item): if 'chunks' not in item: return "" From 38e4817b48e4a0c212934331974478dc72fea779 Mon Sep 17 00:00:00 2001 From: Abogical Date: Sat, 18 Feb 2017 01:58:24 +0200 Subject: [PATCH 0646/1387] Correct calculation of unique chunks --- src/borg/helpers.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index d45ae887..550d7bc1 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1484,7 +1484,7 @@ class ItemFormatter(BaseFormatter): 'dsize': partial(self.sum_unique_chunks_metadata, lambda chunk: chunk.size), 'dcsize': partial(self.sum_unique_chunks_metadata, lambda chunk: chunk.csize), 'num_chunks': self.calculate_num_chunks, - 'unique_chunks': self.calculate_unique_chunks, + 'unique_chunks': partial(self.sum_unique_chunks_metadata, lambda chunk: 1), 'isomtime': partial(self.format_time, 'mtime'), 'isoctime': partial(self.format_time, 'ctime'), 'isoatime': partial(self.format_time, 'atime'), @@ -1548,10 +1548,6 @@ class ItemFormatter(BaseFormatter): def calculate_num_chunks(self, item): return len(item.get('chunks', [])) - def calculate_unique_chunks(self, item): - chunk_index = self.archive.cache.chunks - return sum(1 for c in item.get('chunks', []) if chunk_index[c.id].refcount == 1) - def calculate_size(self, item): return sum(c.size for c in item.get('chunks', [])) From b82f6488753f8dfed15b36e8b164161c9d8ac163 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 19 Feb 2017 00:49:36 +0100 Subject: [PATCH 0647/1387] archive check: detect and fix missing all-zero replacement chunks, fixes #2180 --- src/borg/archive.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index f499f923..8ba5b211 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1246,6 +1246,13 @@ class ArchiveChecker: Missing file chunks will be replaced with new chunks of the same length containing all zeros. If a previously missing file chunk re-appears, the replacement chunk is replaced by the correct one. """ + def replacement_chunk(size): + data = bytes(size) + chunk_id = self.key.id_hash(data) + cdata = self.key.encrypt(Chunk(data)) + csize = len(cdata) + return chunk_id, size, csize, cdata + offset = 0 chunk_list = [] chunks_replaced = False @@ -1261,16 +1268,20 @@ class ArchiveChecker: logger.error('{}: New missing file chunk detected (Byte {}-{}). ' 'Replacing with all-zero chunk.'.format(item.path, offset, offset + size)) self.error_found = chunks_replaced = True - data = bytes(size) - chunk_id = self.key.id_hash(data) - cdata = self.key.encrypt(Chunk(data)) - csize = len(cdata) + chunk_id, size, csize, cdata = replacement_chunk(size) add_reference(chunk_id, size, csize, cdata) else: logger.info('{}: Previously missing file chunk is still missing (Byte {}-{}). It has a ' 'all-zero replacement chunk already.'.format(item.path, offset, offset + size)) chunk_id, size, csize = chunk_current - add_reference(chunk_id, size, csize) + if chunk_id in self.chunks: + add_reference(chunk_id, size, csize) + else: + logger.warning('{}: Missing all-zero replacement chunk detected (Byte {}-{}). ' + 'Generating new replacement chunk.'.format(item.path, offset, offset + size)) + self.error_found = chunks_replaced = True + chunk_id, size, csize, cdata = replacement_chunk(size) + add_reference(chunk_id, size, csize, cdata) else: if chunk_current == chunk_healthy: # normal case, all fine. From 4e08bbcf0b2b05a7f2d236936a74e7efdf3665ae Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 19 Feb 2017 05:12:00 +0100 Subject: [PATCH 0648/1387] document BORG_HOSTNAME_IS_UNIQUE, fixes #2087 --- docs/usage_general.rst.inc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/usage_general.rst.inc b/docs/usage_general.rst.inc index 7cbe7cae..fc96f5b2 100644 --- a/docs/usage_general.rst.inc +++ b/docs/usage_general.rst.inc @@ -139,6 +139,9 @@ General: Main usecase for this is to fully automate ``borg change-passphrase``. BORG_DISPLAY_PASSPHRASE When set, use the value to answer the "display the passphrase for verification" question when defining a new passphrase for encrypted repositories. + BORG_HOSTNAME_IS_UNIQUE=yes + Use this to assert that your hostname is unique. + Borg will then automatically remove locks that it could determine to be stale. BORG_LOGGING_CONF When set, use the given filename as INI_-style logging configuration. BORG_RSH From 8d7dfe739fb800c234b9a8cf370b4cade7d4c9c0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 20 Feb 2017 07:38:55 +0100 Subject: [PATCH 0649/1387] fix ChunkIndex.__contains__ assertion for big-endian archs also: add some missing assertion messages severity: - no issue on little-endian platforms (== most, including x86/x64) - harmless even on big-endian as long as refcount is below 0xfffbffff, which is very likely always the case in practice anyway. --- src/borg/hashindex.pyx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index a10d403a..d696ec3d 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -232,7 +232,7 @@ cdef class ChunkIndex(IndexBase): if not data: raise KeyError(key) cdef uint32_t refcount = _le32toh(data[0]) - assert refcount <= _MAX_VALUE + assert refcount <= _MAX_VALUE, "invalid reference count" return ChunkIndexEntry(refcount, _le32toh(data[1]), _le32toh(data[2])) def __setitem__(self, key, value): @@ -250,7 +250,7 @@ cdef class ChunkIndex(IndexBase): assert len(key) == self.key_size data = hashindex_get(self.index, key) if data != NULL: - assert data[0] <= _MAX_VALUE + assert _le32toh(data[0]) <= _MAX_VALUE, "invalid reference count" return data != NULL def incref(self, key): @@ -328,8 +328,8 @@ cdef class ChunkIndex(IndexBase): if values: refcount1 = _le32toh(values[0]) refcount2 = _le32toh(data[0]) - assert refcount1 <= _MAX_VALUE - assert refcount2 <= _MAX_VALUE + assert refcount1 <= _MAX_VALUE, "invalid reference count" + assert refcount2 <= _MAX_VALUE, "invalid reference count" result64 = refcount1 + refcount2 values[0] = _htole32(min(result64, _MAX_VALUE)) values[1] = data[1] From 63f17087c887e5a3319c739af9724490400a0e18 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 17 Feb 2017 19:29:03 +0100 Subject: [PATCH 0650/1387] docs: edited internals section a bit --- docs/internals.rst | 196 ++++++++++++++++++++++++++++----------------- docs/security.rst | 4 + 2 files changed, 128 insertions(+), 72 deletions(-) diff --git a/docs/internals.rst b/docs/internals.rst index d8d482a3..067d9ada 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -13,17 +13,28 @@ This page documents the internal data structures and storage mechanisms of |project_name|. It is partly based on `mailing list discussion about internals`_ and also on static code analysis. +Borg is uses a low-level, key-value store, the Repository_, and implements +a more complex data structure on top of it, which is made up of the manifest_, +`archives `_, `items `_ and `data chunks `_. -Repository and Archives ------------------------ +Each repository can hold multiple `archives `_, which represent +individual backups that contain a full archive of the files specified +when the backup was performed. -|project_name| stores its data in a `Repository`. Each repository can -hold multiple `Archives`, which represent individual backups that -contain a full archive of the files specified when the backup was -performed. Deduplication is performed across multiple backups, both on -data and metadata, using `Chunks` created by the chunker using the Buzhash_ +Deduplication is performed globally across all data in the repository +(multiple backups and even multiple hosts), both on data and +metadata, using `chunks `_ created by the chunker using the Buzhash_ algorithm. +Repository +---------- + +.. Some parts of this description were taken from the Repository docstring + +|project_name| stores its data in a `Repository`, which is a filesystem-based +transactional key-value store. Thus the repository does not know about +the concept of archives or items. + Each repository has the following file structure: README @@ -44,35 +55,13 @@ index.%d lock.roster and lock.exclusive/* used by the locking system to manage shared and exclusive locks - -Lock files ----------- - -|project_name| uses locks to get (exclusive or shared) access to the cache and -the repository. - -The locking system is based on creating a directory `lock.exclusive` (for -exclusive locks). Inside the lock directory, there is a file indicating -hostname, process id and thread id of the lock holder. - -There is also a json file `lock.roster` that keeps a directory of all shared -and exclusive lockers. - -If the process can create the `lock.exclusive` directory for a resource, it has -the lock for it. If creation fails (because the directory has already been -created by some other process), lock acquisition fails. - -The cache lock is usually in `~/.cache/borg/REPOID/lock.*`. -The repository lock is in `repository/lock.*`. - -In case you run into troubles with the locks, you can use the ``borg break-lock`` -command after you first have made sure that no |project_name| process is -running on any machine that accesses this resource. Be very careful, the cache -or repository might get damaged if multiple processes use it at the same time. - +Transactionality is achieved by using a log (aka journal) to record changes. The log is a series of numbered files +called segments_. Each segment is a series of log entries. The segment number together with the offset of each +entry relative to its segment start establishes an ordering of the log entries. This is the "definition" of +time for the purposes of the log. Config file ------------ +~~~~~~~~~~~ Each repository has a ``config`` file which which is a ``INI``-style file and looks like this:: @@ -88,61 +77,93 @@ identifier for repositories. It will not change if you move the repository around so you can make a local transfer then decide to move the repository to another (even remote) location at a later time. - Keys ----- -The key to address the key/value store is usually computed like this: +~~~~ -key = id = id_hash(unencrypted_data) +Repository keys are byte-strings of fixed length (32 bytes), they +don't have a particular meaning (except for the Manifest_). -The id_hash function is: +Normally the keys are computed like this:: -* sha256 (no encryption keys available) -* hmac-sha256 (encryption keys available) + key = id = id_hash(unencrypted_data) +The id_hash function depends on the :ref:`encryption mode `. -Segments and archives ---------------------- +Segments +~~~~~~~~ A |project_name| repository is a filesystem based transactional key/value store. It makes extensive use of msgpack_ to store data and, unless otherwise noted, data is stored in msgpack_ encoded files. Objects referenced by a key are stored inline in files (`segments`) of approx. -5MB size in numbered subdirectories of ``repo/data``. +500 MB size in numbered subdirectories of ``repo/data``. -They contain: +A segment starts with a magic number (``BORG_SEG`` as an eight byte ASCII string), +followed by a number of log entries. Each log entry consists of: -* header size -* crc -* size -* tag -* key -* data +* size of the entry +* CRC32 of the entire entry (for a PUT this includes the data) +* entry tag: PUT, DELETE or COMMIT +* PUT and DELETE follow this with the 32 byte key +* PUT follow the key with the data -Segments are built locally, and then uploaded. Those files are -strictly append-only and modified only once. +Those files are strictly append-only and modified only once. -Tag is either ``PUT``, ``DELETE``, or ``COMMIT``. A segment file is -basically a transaction log where each repository operation is -appended to the file. So if an object is written to the repository a -``PUT`` tag is written to the file followed by the object id and -data. If an object is deleted a ``DELETE`` tag is appended -followed by the object id. A ``COMMIT`` tag is written when a -repository transaction is committed. When a repository is opened any -``PUT`` or ``DELETE`` operations not followed by a ``COMMIT`` tag are -discarded since they are part of a partial/uncommitted transaction. +Tag is either ``PUT``, ``DELETE``, or ``COMMIT``. +When an object is written to the repository a ``PUT`` entry is written +to the file containing the object id and data. If an object is deleted +a ``DELETE`` entry is appended with the object id. + +A ``COMMIT`` tag is written when a repository transaction is +committed. + +When a repository is opened any ``PUT`` or ``DELETE`` operations not +followed by a ``COMMIT`` tag are discarded since they are part of a +partial/uncommitted transaction. + +Compaction +~~~~~~~~~~ + +For a given key only the last entry regarding the key, which is called current (all other entries are called +superseded), is relevant: If there is no entry or the last entry is a DELETE then the key does not exist. +Otherwise the last PUT defines the value of the key. + +By superseding a PUT (with either another PUT or a DELETE) the log entry becomes obsolete. A segment containing +such obsolete entries is called sparse, while a segment containing no such entries is called compact. + +Since writing a ``DELETE`` tag does not actually delete any data and +thus does not free disk space any log-based data store will need a +compaction strategy. + +Borg tracks which segments are sparse and does a forward compaction +when a commit is issued (unless the :ref:`append_only_mode` is +active). + +Compaction processes sparse segments from oldest to newest; sparse segments +which don't contain enough deleted data to justify compaction are skipped. This +avoids doing e.g. 500 MB of writing current data to a new segment when only +a couple kB were deleted in a segment. + +Segments that are compacted are read in entirety. Current entries are written to +a new segment, while superseded entries are omitted. After each segment an intermediary +commit is written to the new segment, data is synced and the old segment is deleted -- +freeing disk space. + +(The actual algorithm is more complex to avoid various consistency issues, refer to +the ``borg.repository`` module for more comments and documentation on these issues.) + +.. _manifest: The manifest ------------ The manifest is an object with an all-zero key that references all the -archives. -It contains: +archives. It contains: -* version -* list of archive infos +* Manifest version +* A list of archive infos * timestamp * config @@ -153,10 +174,12 @@ Each archive info contains: * time It is the last object stored, in the last segment, and is replaced -each time. +each time an archive is added or deleted. -The Archive ------------ +.. _archive: + +Archives +-------- The archive metadata does not contain the file items directly. Only references to other objects that contain that data. An archive is an @@ -199,8 +222,10 @@ IntegrityError will be raised. A workaround is to create multiple archives with less items each, see also :issue:`1452`. -The Item --------- +.. _item: + +Items +----- Each item represents a file, directory or other fs item and is stored as an ``item`` dictionary that contains: @@ -252,7 +277,6 @@ what files you have based on a specific set of chunk sizes). For some more general usage hints see also ``--chunker-params``. - Indexes / Caches ---------------- @@ -428,6 +452,8 @@ b) with ``create --chunker-params 19,23,21,4095`` (default): Encryption ---------- +.. seealso:: The :ref:`borgcrypto` section for an in-depth review. + AES_-256 is used in CTR mode (so no need for padding). A 64bit initialization vector is used, a `HMAC-SHA256`_ is computed on the encrypted chunk with a random 64bit nonce and both are stored in the chunk. @@ -453,6 +479,7 @@ is stored into the keyfile or as repokey). The passphrase is passed through the ``BORG_PASSPHRASE`` environment variable or prompted for interactive usage. +.. _key_files: Key files --------- @@ -550,3 +577,28 @@ Compression is applied after deduplication, thus using different compression methods in one repo does not influence deduplication. See ``borg create --help`` about how to specify the compression level and its default. + +Lock files +---------- + +|project_name| uses locks to get (exclusive or shared) access to the cache and +the repository. + +The locking system is based on creating a directory `lock.exclusive` (for +exclusive locks). Inside the lock directory, there is a file indicating +hostname, process id and thread id of the lock holder. + +There is also a json file `lock.roster` that keeps a directory of all shared +and exclusive lockers. + +If the process can create the `lock.exclusive` directory for a resource, it has +the lock for it. If creation fails (because the directory has already been +created by some other process), lock acquisition fails. + +The cache lock is usually in `~/.cache/borg/REPOID/lock.*`. +The repository lock is in `repository/lock.*`. + +In case you run into troubles with the locks, you can use the ``borg break-lock`` +command after you first have made sure that no |project_name| process is +running on any machine that accesses this resource. Be very careful, the cache +or repository might get damaged if multiple processes use it at the same time. diff --git a/docs/security.rst b/docs/security.rst index 5688744e..7b54b6a8 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -8,6 +8,8 @@ Security ======== +.. _borgcrypto: + Cryptography in Borg ==================== @@ -198,6 +200,8 @@ This scheme, and specifically the use of a constant IV with the CTR mode, is secure because an identical passphrase will result in a different derived KEK for every encryption due to the salt. +Refer to the :ref:`key_files` section for details on the format. + Implementations used -------------------- From e5bbba573a0f181c1347ad22dc21a938a79408db Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 17 Feb 2017 23:31:47 +0100 Subject: [PATCH 0651/1387] docs: make internals.rst an index page Subsections: - Security - Data structures and file formats --- docs/internals.rst | 600 +---------------------------- docs/internals/data-structures.rst | 587 ++++++++++++++++++++++++++++ docs/{ => internals}/security.rst | 0 3 files changed, 601 insertions(+), 586 deletions(-) create mode 100644 docs/internals/data-structures.rst rename docs/{ => internals}/security.rst (100%) diff --git a/docs/internals.rst b/docs/internals.rst index 067d9ada..7cfb055c 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -1,5 +1,4 @@ .. include:: global.rst.inc -.. highlight:: none .. _internals: Internals @@ -7,598 +6,27 @@ Internals .. toctree:: - security + internals/security + internals/data-structures This page documents the internal data structures and storage mechanisms of |project_name|. It is partly based on `mailing list discussion about internals`_ and also on static code analysis. -Borg is uses a low-level, key-value store, the Repository_, and implements -a more complex data structure on top of it, which is made up of the manifest_, -`archives `_, `items `_ and `data chunks `_. +Borg uses a low-level, key-value store, the :ref:`repository`, and +implements a more complex data structure on top of it, which is made +up of the :ref:`manifest `, :ref:`archives `, +:ref:`items ` and data :ref:`chunks`. -Each repository can hold multiple `archives `_, which represent -individual backups that contain a full archive of the files specified -when the backup was performed. +Each repository can hold multiple :ref:`archives `, which +represent individual backups that contain a full archive of the files +specified when the backup was performed. Deduplication is performed globally across all data in the repository -(multiple backups and even multiple hosts), both on data and -metadata, using `chunks `_ created by the chunker using the Buzhash_ +(multiple backups and even multiple hosts), both on data and metadata, +using :ref:`chunks` created by the chunker using the Buzhash_ algorithm. -Repository ----------- - -.. Some parts of this description were taken from the Repository docstring - -|project_name| stores its data in a `Repository`, which is a filesystem-based -transactional key-value store. Thus the repository does not know about -the concept of archives or items. - -Each repository has the following file structure: - -README - simple text file telling that this is a |project_name| repository - -config - repository configuration - -data/ - directory where the actual data is stored - -hints.%d - hints for repository compaction - -index.%d - repository index - -lock.roster and lock.exclusive/* - used by the locking system to manage shared and exclusive locks - -Transactionality is achieved by using a log (aka journal) to record changes. The log is a series of numbered files -called segments_. Each segment is a series of log entries. The segment number together with the offset of each -entry relative to its segment start establishes an ordering of the log entries. This is the "definition" of -time for the purposes of the log. - -Config file -~~~~~~~~~~~ - -Each repository has a ``config`` file which which is a ``INI``-style file -and looks like this:: - - [repository] - version = 1 - segments_per_dir = 10000 - max_segment_size = 5242880 - id = 57d6c1d52ce76a836b532b0e42e677dec6af9fca3673db511279358828a21ed6 - -This is where the ``repository.id`` is stored. It is a unique -identifier for repositories. It will not change if you move the -repository around so you can make a local transfer then decide to move -the repository to another (even remote) location at a later time. - -Keys -~~~~ - -Repository keys are byte-strings of fixed length (32 bytes), they -don't have a particular meaning (except for the Manifest_). - -Normally the keys are computed like this:: - - key = id = id_hash(unencrypted_data) - -The id_hash function depends on the :ref:`encryption mode `. - -Segments -~~~~~~~~ - -A |project_name| repository is a filesystem based transactional key/value -store. It makes extensive use of msgpack_ to store data and, unless -otherwise noted, data is stored in msgpack_ encoded files. - -Objects referenced by a key are stored inline in files (`segments`) of approx. -500 MB size in numbered subdirectories of ``repo/data``. - -A segment starts with a magic number (``BORG_SEG`` as an eight byte ASCII string), -followed by a number of log entries. Each log entry consists of: - -* size of the entry -* CRC32 of the entire entry (for a PUT this includes the data) -* entry tag: PUT, DELETE or COMMIT -* PUT and DELETE follow this with the 32 byte key -* PUT follow the key with the data - -Those files are strictly append-only and modified only once. - -Tag is either ``PUT``, ``DELETE``, or ``COMMIT``. - -When an object is written to the repository a ``PUT`` entry is written -to the file containing the object id and data. If an object is deleted -a ``DELETE`` entry is appended with the object id. - -A ``COMMIT`` tag is written when a repository transaction is -committed. - -When a repository is opened any ``PUT`` or ``DELETE`` operations not -followed by a ``COMMIT`` tag are discarded since they are part of a -partial/uncommitted transaction. - -Compaction -~~~~~~~~~~ - -For a given key only the last entry regarding the key, which is called current (all other entries are called -superseded), is relevant: If there is no entry or the last entry is a DELETE then the key does not exist. -Otherwise the last PUT defines the value of the key. - -By superseding a PUT (with either another PUT or a DELETE) the log entry becomes obsolete. A segment containing -such obsolete entries is called sparse, while a segment containing no such entries is called compact. - -Since writing a ``DELETE`` tag does not actually delete any data and -thus does not free disk space any log-based data store will need a -compaction strategy. - -Borg tracks which segments are sparse and does a forward compaction -when a commit is issued (unless the :ref:`append_only_mode` is -active). - -Compaction processes sparse segments from oldest to newest; sparse segments -which don't contain enough deleted data to justify compaction are skipped. This -avoids doing e.g. 500 MB of writing current data to a new segment when only -a couple kB were deleted in a segment. - -Segments that are compacted are read in entirety. Current entries are written to -a new segment, while superseded entries are omitted. After each segment an intermediary -commit is written to the new segment, data is synced and the old segment is deleted -- -freeing disk space. - -(The actual algorithm is more complex to avoid various consistency issues, refer to -the ``borg.repository`` module for more comments and documentation on these issues.) - -.. _manifest: - -The manifest ------------- - -The manifest is an object with an all-zero key that references all the -archives. It contains: - -* Manifest version -* A list of archive infos -* timestamp -* config - -Each archive info contains: - -* name -* id -* time - -It is the last object stored, in the last segment, and is replaced -each time an archive is added or deleted. - -.. _archive: - -Archives --------- - -The archive metadata does not contain the file items directly. Only -references to other objects that contain that data. An archive is an -object that contains: - -* version -* name -* list of chunks containing item metadata (size: count * ~40B) -* cmdline -* hostname -* username -* time - -.. _archive_limitation: - -Note about archive limitations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The archive is currently stored as a single object in the repository -and thus limited in size to MAX_OBJECT_SIZE (20MiB). - -As one chunk list entry is ~40B, that means we can reference ~500.000 item -metadata stream chunks per archive. - -Each item metadata stream chunk is ~128kiB (see hardcoded ITEMS_CHUNKER_PARAMS). - -So that means the whole item metadata stream is limited to ~64GiB chunks. -If compression is used, the amount of storable metadata is bigger - by the -compression factor. - -If the medium size of an item entry is 100B (small size file, no ACLs/xattrs), -that means a limit of ~640 million files/directories per archive. - -If the medium size of an item entry is 2kB (~100MB size files or more -ACLs/xattrs), the limit will be ~32 million files/directories per archive. - -If one tries to create an archive object bigger than MAX_OBJECT_SIZE, a fatal -IntegrityError will be raised. - -A workaround is to create multiple archives with less items each, see -also :issue:`1452`. - -.. _item: - -Items ------ - -Each item represents a file, directory or other fs item and is stored as an -``item`` dictionary that contains: - -* path -* list of data chunks (size: count * ~40B) -* user -* group -* uid -* gid -* mode (item type + permissions) -* source (for links) -* rdev (for devices) -* mtime, atime, ctime in nanoseconds -* xattrs -* acl -* bsdfiles - -All items are serialized using msgpack and the resulting byte stream -is fed into the same chunker algorithm as used for regular file data -and turned into deduplicated chunks. The reference to these chunks is then added -to the archive metadata. To achieve a finer granularity on this metadata -stream, we use different chunker params for this chunker, which result in -smaller chunks. - -A chunk is stored as an object as well, of course. - -.. _chunker_details: - -Chunks ------- - -The |project_name| chunker uses a rolling hash computed by the Buzhash_ algorithm. -It triggers (chunks) when the last HASH_MASK_BITS bits of the hash are zero, -producing chunks of 2^HASH_MASK_BITS Bytes on average. - -``borg create --chunker-params CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE`` -can be used to tune the chunker parameters, the default is: - -- CHUNK_MIN_EXP = 19 (minimum chunk size = 2^19 B = 512 kiB) -- CHUNK_MAX_EXP = 23 (maximum chunk size = 2^23 B = 8 MiB) -- HASH_MASK_BITS = 21 (statistical medium chunk size ~= 2^21 B = 2 MiB) -- HASH_WINDOW_SIZE = 4095 [B] (`0xFFF`) - -The buzhash table is altered by XORing it with a seed randomly generated once -for the archive, and stored encrypted in the keyfile. This is to prevent chunk -size based fingerprinting attacks on your encrypted repo contents (to guess -what files you have based on a specific set of chunk sizes). - -For some more general usage hints see also ``--chunker-params``. - -Indexes / Caches ----------------- - -The **files cache** is stored in ``cache/files`` and is used at backup time to -quickly determine whether a given file is unchanged and we have all its chunks. - -The files cache is a key -> value mapping and contains: - -* key: - - - full, absolute file path id_hash -* value: - - - file inode number - - file size - - file mtime_ns - - list of file content chunk id hashes - - age (0 [newest], 1, 2, 3, ..., BORG_FILES_CACHE_TTL - 1) - -To determine whether a file has not changed, cached values are looked up via -the key in the mapping and compared to the current file attribute values. - -If the file's size, mtime_ns and inode number is still the same, it is -considered to not have changed. In that case, we check that all file content -chunks are (still) present in the repository (we check that via the chunks -cache). - -If everything is matching and all chunks are present, the file is not read / -chunked / hashed again (but still a file metadata item is written to the -archive, made from fresh file metadata read from the filesystem). This is -what makes borg so fast when processing unchanged files. - -If there is a mismatch or a chunk is missing, the file is read / chunked / -hashed. Chunks already present in repo won't be transferred to repo again. - -The inode number is stored and compared to make sure we distinguish between -different files, as a single path may not be unique across different -archives in different setups. - -Not all filesystems have stable inode numbers. If that is the case, borg can -be told to ignore the inode number in the check via --ignore-inode. - -The age value is used for cache management. If a file is "seen" in a backup -run, its age is reset to 0, otherwise its age is incremented by one. -If a file was not seen in BORG_FILES_CACHE_TTL backups, its cache entry is -removed. See also: :ref:`always_chunking` and :ref:`a_status_oddity` - -The files cache is a python dictionary, storing python objects, which -generates a lot of overhead. - -Borg can also work without using the files cache (saves memory if you have a -lot of files or not much RAM free), then all files are assumed to have changed. -This is usually much slower than with files cache. - -The **chunks cache** is stored in ``cache/chunks`` and is used to determine -whether we already have a specific chunk, to count references to it and also -for statistics. - -The chunks cache is a key -> value mapping and contains: - -* key: - - - chunk id_hash -* value: - - - reference count - - size - - encrypted/compressed size - -The chunks cache is a hashindex, a hash table implemented in C and tuned for -memory efficiency. - -The **repository index** is stored in ``repo/index.%d`` and is used to -determine a chunk's location in the repository. - -The repo index is a key -> value mapping and contains: - -* key: - - - chunk id_hash -* value: - - - segment (that contains the chunk) - - offset (where the chunk is located in the segment) - -The repo index is a hashindex, a hash table implemented in C and tuned for -memory efficiency. - - -Hints are stored in a file (``repo/hints.%d``). - -It contains: - -* version -* list of segments -* compact - -hints and index can be recreated if damaged or lost using ``check --repair``. - -The chunks cache and the repository index are stored as hash tables, with -only one slot per bucket, but that spreads the collisions to the following -buckets. As a consequence the hash is just a start position for a linear -search, and if the element is not in the table the index is linearly crossed -until an empty bucket is found. - -When the hash table is filled to 75%, its size is grown. When it's -emptied to 25%, its size is shrinked. So operations on it have a variable -complexity between constant and linear with low factor, and memory overhead -varies between 33% and 300%. - -.. _cache-memory-usage: - -Indexes / Caches memory usage ------------------------------ - -Here is the estimated memory usage of |project_name| - it's complicated: - - chunk_count ~= total_file_size / 2 ^ HASH_MASK_BITS - - repo_index_usage = chunk_count * 40 - - chunks_cache_usage = chunk_count * 44 - - files_cache_usage = total_file_count * 240 + chunk_count * 80 - - mem_usage ~= repo_index_usage + chunks_cache_usage + files_cache_usage - = chunk_count * 164 + total_file_count * 240 - -Due to the hashtables, the best/usual/worst cases for memory allocation can -be estimated like that: - - mem_allocation = mem_usage / load_factor # l_f = 0.25 .. 0.75 - - mem_allocation_peak = mem_allocation * (1 + growth_factor) # g_f = 1.1 .. 2 - - -All units are Bytes. - -It is assuming every chunk is referenced exactly once (if you have a lot of -duplicate chunks, you will have less chunks than estimated above). - -It is also assuming that typical chunk size is 2^HASH_MASK_BITS (if you have -a lot of files smaller than this statistical medium chunk size, you will have -more chunks than estimated above, because 1 file is at least 1 chunk). - -If a remote repository is used the repo index will be allocated on the remote side. - -The chunks cache, files cache and the repo index are all implemented as hash -tables. A hash table must have a significant amount of unused entries to be -fast - the so-called load factor gives the used/unused elements ratio. - -When a hash table gets full (load factor getting too high), it needs to be -grown (allocate new, bigger hash table, copy all elements over to it, free old -hash table) - this will lead to short-time peaks in memory usage each time this -happens. Usually does not happen for all hashtables at the same time, though. -For small hash tables, we start with a growth factor of 2, which comes down to -~1.1x for big hash tables. - -E.g. backing up a total count of 1 Mi (IEC binary prefix i.e. 2^20) files with a total size of 1TiB. - -a) with ``create --chunker-params 10,23,16,4095`` (custom, like borg < 1.0 or attic): - - mem_usage = 2.8GiB - -b) with ``create --chunker-params 19,23,21,4095`` (default): - - mem_usage = 0.31GiB - -.. note:: There is also the ``--no-files-cache`` option to switch off the files cache. - You'll save some memory, but it will need to read / chunk all the files as - it can not skip unmodified files then. - -Encryption ----------- - -.. seealso:: The :ref:`borgcrypto` section for an in-depth review. - -AES_-256 is used in CTR mode (so no need for padding). A 64bit initialization -vector is used, a `HMAC-SHA256`_ is computed on the encrypted chunk with a -random 64bit nonce and both are stored in the chunk. -The header of each chunk is: ``TYPE(1)`` + ``HMAC(32)`` + ``NONCE(8)`` + ``CIPHERTEXT``. -Encryption and HMAC use two different keys. - -In AES CTR mode you can think of the IV as the start value for the counter. -The counter itself is incremented by one after each 16 byte block. -The IV/counter is not required to be random but it must NEVER be reused. -So to accomplish this |project_name| initializes the encryption counter to be -higher than any previously used counter value before encrypting new data. - -To reduce payload size, only 8 bytes of the 16 bytes nonce is saved in the -payload, the first 8 bytes are always zeros. This does not affect security but -limits the maximum repository capacity to only 295 exabytes (2**64 * 16 bytes). - -Encryption keys (and other secrets) are kept either in a key file on the client -('keyfile' mode) or in the repository config on the server ('repokey' mode). -In both cases, the secrets are generated from random and then encrypted by a -key derived from your passphrase (this happens on the client before the key -is stored into the keyfile or as repokey). - -The passphrase is passed through the ``BORG_PASSPHRASE`` environment variable -or prompted for interactive usage. - -.. _key_files: - -Key files ---------- - -When initialized with the ``init -e keyfile`` command, |project_name| -needs an associated file in ``$HOME/.config/borg/keys`` to read and write -the repository. The format is based on msgpack_, base64 encoding and -PBKDF2_ SHA256 hashing, which is then encoded again in a msgpack_. - -The internal data structure is as follows: - -version - currently always an integer, 1 - -repository_id - the ``id`` field in the ``config`` ``INI`` file of the repository. - -enc_key - the key used to encrypt data with AES (256 bits) - -enc_hmac_key - the key used to HMAC the encrypted data (256 bits) - -id_key - the key used to HMAC the plaintext chunk data to compute the chunk's id - -chunk_seed - the seed for the buzhash chunking table (signed 32 bit integer) - -Those fields are processed using msgpack_. The utf-8 encoded passphrase -is processed with PBKDF2_ (SHA256_, 100000 iterations, random 256 bit salt) -to give us a derived key. The derived key is 256 bits long. -A `HMAC-SHA256`_ checksum of the above fields is generated with the derived -key, then the derived key is also used to encrypt the above pack of fields. -Then the result is stored in a another msgpack_ formatted as follows: - -version - currently always an integer, 1 - -salt - random 256 bits salt used to process the passphrase - -iterations - number of iterations used to process the passphrase (currently 100000) - -algorithm - the hashing algorithm used to process the passphrase and do the HMAC - checksum (currently the string ``sha256``) - -hash - the HMAC of the encrypted derived key - -data - the derived key, encrypted with AES over a PBKDF2_ SHA256 key - described above - -The resulting msgpack_ is then encoded using base64 and written to the -key file, wrapped using the standard ``textwrap`` module with a header. -The header is a single line with a MAGIC string, a space and a hexadecimal -representation of the repository id. - - -Compression ------------ - -|project_name| supports the following compression methods: - -- none (no compression, pass through data 1:1) -- lz4 (low compression, but super fast) -- zlib (level 0-9, level 0 is no compression [but still adding zlib overhead], - level 1 is low, level 9 is high compression) -- lzma (level 0-9, level 0 is low, level 9 is high compression). - -Speed: none > lz4 > zlib > lzma -Compression: lzma > zlib > lz4 > none - -Be careful, higher zlib and especially lzma compression levels might take a -lot of resources (CPU and memory). - -The overall speed of course also depends on the speed of your target storage. -If that is slow, using a higher compression level might yield better overall -performance. You need to experiment a bit. Maybe just watch your CPU load, if -that is relatively low, increase compression until 1 core is 70-100% loaded. - -Even if your target storage is rather fast, you might see interesting effects: -while doing no compression at all (none) is a operation that takes no time, it -likely will need to store more data to the storage compared to using lz4. -The time needed to transfer and store the additional data might be much more -than if you had used lz4 (which is super fast, but still might compress your -data about 2:1). This is assuming your data is compressible (if you backup -already compressed data, trying to compress them at backup time is usually -pointless). - -Compression is applied after deduplication, thus using different compression -methods in one repo does not influence deduplication. - -See ``borg create --help`` about how to specify the compression level and its default. - -Lock files ----------- - -|project_name| uses locks to get (exclusive or shared) access to the cache and -the repository. - -The locking system is based on creating a directory `lock.exclusive` (for -exclusive locks). Inside the lock directory, there is a file indicating -hostname, process id and thread id of the lock holder. - -There is also a json file `lock.roster` that keeps a directory of all shared -and exclusive lockers. - -If the process can create the `lock.exclusive` directory for a resource, it has -the lock for it. If creation fails (because the directory has already been -created by some other process), lock acquisition fails. - -The cache lock is usually in `~/.cache/borg/REPOID/lock.*`. -The repository lock is in `repository/lock.*`. - -In case you run into troubles with the locks, you can use the ``borg break-lock`` -command after you first have made sure that no |project_name| process is -running on any machine that accesses this resource. Be very careful, the cache -or repository might get damaged if multiple processes use it at the same time. +To actually perform the repository-wide deduplication, a hash of each +chunk is checked against the :ref:`cache`, which is a hash-table of +all chunks that already exist. diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst new file mode 100644 index 00000000..51b34a0b --- /dev/null +++ b/docs/internals/data-structures.rst @@ -0,0 +1,587 @@ +.. include:: ../global.rst.inc +.. highlight:: none + +Data structures and file formats +================================ + +.. _repository: + +Repository +---------- + +.. Some parts of this description were taken from the Repository docstring + +|project_name| stores its data in a `Repository`, which is a filesystem-based +transactional key-value store. Thus the repository does not know about +the concept of archives or items. + +Each repository has the following file structure: + +README + simple text file telling that this is a |project_name| repository + +config + repository configuration + +data/ + directory where the actual data is stored + +hints.%d + hints for repository compaction + +index.%d + repository index + +lock.roster and lock.exclusive/* + used by the locking system to manage shared and exclusive locks + +Transactionality is achieved by using a log (aka journal) to record changes. The log is a series of numbered files +called segments_. Each segment is a series of log entries. The segment number together with the offset of each +entry relative to its segment start establishes an ordering of the log entries. This is the "definition" of +time for the purposes of the log. + +Config file +~~~~~~~~~~~ + +Each repository has a ``config`` file which which is a ``INI``-style file +and looks like this:: + + [repository] + version = 1 + segments_per_dir = 10000 + max_segment_size = 5242880 + id = 57d6c1d52ce76a836b532b0e42e677dec6af9fca3673db511279358828a21ed6 + +This is where the ``repository.id`` is stored. It is a unique +identifier for repositories. It will not change if you move the +repository around so you can make a local transfer then decide to move +the repository to another (even remote) location at a later time. + +Keys +~~~~ + +Repository keys are byte-strings of fixed length (32 bytes), they +don't have a particular meaning (except for the Manifest_). + +Normally the keys are computed like this:: + + key = id = id_hash(unencrypted_data) + +The id_hash function depends on the :ref:`encryption mode `. + +Segments +~~~~~~~~ + +A |project_name| repository is a filesystem based transactional key/value +store. It makes extensive use of msgpack_ to store data and, unless +otherwise noted, data is stored in msgpack_ encoded files. + +Objects referenced by a key are stored inline in files (`segments`) of approx. +500 MB size in numbered subdirectories of ``repo/data``. + +A segment starts with a magic number (``BORG_SEG`` as an eight byte ASCII string), +followed by a number of log entries. Each log entry consists of: + +* size of the entry +* CRC32 of the entire entry (for a PUT this includes the data) +* entry tag: PUT, DELETE or COMMIT +* PUT and DELETE follow this with the 32 byte key +* PUT follow the key with the data + +Those files are strictly append-only and modified only once. + +Tag is either ``PUT``, ``DELETE``, or ``COMMIT``. + +When an object is written to the repository a ``PUT`` entry is written +to the file containing the object id and data. If an object is deleted +a ``DELETE`` entry is appended with the object id. + +A ``COMMIT`` tag is written when a repository transaction is +committed. + +When a repository is opened any ``PUT`` or ``DELETE`` operations not +followed by a ``COMMIT`` tag are discarded since they are part of a +partial/uncommitted transaction. + +Compaction +~~~~~~~~~~ + +For a given key only the last entry regarding the key, which is called current (all other entries are called +superseded), is relevant: If there is no entry or the last entry is a DELETE then the key does not exist. +Otherwise the last PUT defines the value of the key. + +By superseding a PUT (with either another PUT or a DELETE) the log entry becomes obsolete. A segment containing +such obsolete entries is called sparse, while a segment containing no such entries is called compact. + +Since writing a ``DELETE`` tag does not actually delete any data and +thus does not free disk space any log-based data store will need a +compaction strategy. + +Borg tracks which segments are sparse and does a forward compaction +when a commit is issued (unless the :ref:`append_only_mode` is +active). + +Compaction processes sparse segments from oldest to newest; sparse segments +which don't contain enough deleted data to justify compaction are skipped. This +avoids doing e.g. 500 MB of writing current data to a new segment when only +a couple kB were deleted in a segment. + +Segments that are compacted are read in entirety. Current entries are written to +a new segment, while superseded entries are omitted. After each segment an intermediary +commit is written to the new segment, data is synced and the old segment is deleted -- +freeing disk space. + +(The actual algorithm is more complex to avoid various consistency issues, refer to +the ``borg.repository`` module for more comments and documentation on these issues.) + +.. _manifest: + +The manifest +------------ + +The manifest is an object with an all-zero key that references all the +archives. It contains: + +* Manifest version +* A list of archive infos +* timestamp +* config + +Each archive info contains: + +* name +* id +* time + +It is the last object stored, in the last segment, and is replaced +each time an archive is added or deleted. + +.. _archive: + +Archives +-------- + +The archive metadata does not contain the file items directly. Only +references to other objects that contain that data. An archive is an +object that contains: + +* version +* name +* list of chunks containing item metadata (size: count * ~40B) +* cmdline +* hostname +* username +* time + +.. _archive_limitation: + +Note about archive limitations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The archive is currently stored as a single object in the repository +and thus limited in size to MAX_OBJECT_SIZE (20MiB). + +As one chunk list entry is ~40B, that means we can reference ~500.000 item +metadata stream chunks per archive. + +Each item metadata stream chunk is ~128kiB (see hardcoded ITEMS_CHUNKER_PARAMS). + +So that means the whole item metadata stream is limited to ~64GiB chunks. +If compression is used, the amount of storable metadata is bigger - by the +compression factor. + +If the medium size of an item entry is 100B (small size file, no ACLs/xattrs), +that means a limit of ~640 million files/directories per archive. + +If the medium size of an item entry is 2kB (~100MB size files or more +ACLs/xattrs), the limit will be ~32 million files/directories per archive. + +If one tries to create an archive object bigger than MAX_OBJECT_SIZE, a fatal +IntegrityError will be raised. + +A workaround is to create multiple archives with less items each, see +also :issue:`1452`. + +.. _item: + +Items +----- + +Each item represents a file, directory or other fs item and is stored as an +``item`` dictionary that contains: + +* path +* list of data chunks (size: count * ~40B) +* user +* group +* uid +* gid +* mode (item type + permissions) +* source (for links) +* rdev (for devices) +* mtime, atime, ctime in nanoseconds +* xattrs +* acl +* bsdfiles + +All items are serialized using msgpack and the resulting byte stream +is fed into the same chunker algorithm as used for regular file data +and turned into deduplicated chunks. The reference to these chunks is then added +to the archive metadata. To achieve a finer granularity on this metadata +stream, we use different chunker params for this chunker, which result in +smaller chunks. + +A chunk is stored as an object as well, of course. + +.. _chunks: +.. _chunker_details: + +Chunks +------ + +The |project_name| chunker uses a rolling hash computed by the Buzhash_ algorithm. +It triggers (chunks) when the last HASH_MASK_BITS bits of the hash are zero, +producing chunks of 2^HASH_MASK_BITS Bytes on average. + +``borg create --chunker-params CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE`` +can be used to tune the chunker parameters, the default is: + +- CHUNK_MIN_EXP = 19 (minimum chunk size = 2^19 B = 512 kiB) +- CHUNK_MAX_EXP = 23 (maximum chunk size = 2^23 B = 8 MiB) +- HASH_MASK_BITS = 21 (statistical medium chunk size ~= 2^21 B = 2 MiB) +- HASH_WINDOW_SIZE = 4095 [B] (`0xFFF`) + +The buzhash table is altered by XORing it with a seed randomly generated once +for the archive, and stored encrypted in the keyfile. This is to prevent chunk +size based fingerprinting attacks on your encrypted repo contents (to guess +what files you have based on a specific set of chunk sizes). + +For some more general usage hints see also ``--chunker-params``. + +.. _cache: + +Indexes / Caches +---------------- + +The **files cache** is stored in ``cache/files`` and is used at backup time to +quickly determine whether a given file is unchanged and we have all its chunks. + +The files cache is a key -> value mapping and contains: + +* key: + + - full, absolute file path id_hash +* value: + + - file inode number + - file size + - file mtime_ns + - list of file content chunk id hashes + - age (0 [newest], 1, 2, 3, ..., BORG_FILES_CACHE_TTL - 1) + +To determine whether a file has not changed, cached values are looked up via +the key in the mapping and compared to the current file attribute values. + +If the file's size, mtime_ns and inode number is still the same, it is +considered to not have changed. In that case, we check that all file content +chunks are (still) present in the repository (we check that via the chunks +cache). + +If everything is matching and all chunks are present, the file is not read / +chunked / hashed again (but still a file metadata item is written to the +archive, made from fresh file metadata read from the filesystem). This is +what makes borg so fast when processing unchanged files. + +If there is a mismatch or a chunk is missing, the file is read / chunked / +hashed. Chunks already present in repo won't be transferred to repo again. + +The inode number is stored and compared to make sure we distinguish between +different files, as a single path may not be unique across different +archives in different setups. + +Not all filesystems have stable inode numbers. If that is the case, borg can +be told to ignore the inode number in the check via --ignore-inode. + +The age value is used for cache management. If a file is "seen" in a backup +run, its age is reset to 0, otherwise its age is incremented by one. +If a file was not seen in BORG_FILES_CACHE_TTL backups, its cache entry is +removed. See also: :ref:`always_chunking` and :ref:`a_status_oddity` + +The files cache is a python dictionary, storing python objects, which +generates a lot of overhead. + +Borg can also work without using the files cache (saves memory if you have a +lot of files or not much RAM free), then all files are assumed to have changed. +This is usually much slower than with files cache. + +The **chunks cache** is stored in ``cache/chunks`` and is used to determine +whether we already have a specific chunk, to count references to it and also +for statistics. + +The chunks cache is a key -> value mapping and contains: + +* key: + + - chunk id_hash +* value: + + - reference count + - size + - encrypted/compressed size + +The chunks cache is a hashindex, a hash table implemented in C and tuned for +memory efficiency. + +The **repository index** is stored in ``repo/index.%d`` and is used to +determine a chunk's location in the repository. + +The repo index is a key -> value mapping and contains: + +* key: + + - chunk id_hash +* value: + + - segment (that contains the chunk) + - offset (where the chunk is located in the segment) + +The repo index is a hashindex, a hash table implemented in C and tuned for +memory efficiency. + + +Hints are stored in a file (``repo/hints.%d``). + +It contains: + +* version +* list of segments +* compact + +hints and index can be recreated if damaged or lost using ``check --repair``. + +The chunks cache and the repository index are stored as hash tables, with +only one slot per bucket, but that spreads the collisions to the following +buckets. As a consequence the hash is just a start position for a linear +search, and if the element is not in the table the index is linearly crossed +until an empty bucket is found. + +When the hash table is filled to 75%, its size is grown. When it's +emptied to 25%, its size is shrinked. So operations on it have a variable +complexity between constant and linear with low factor, and memory overhead +varies between 33% and 300%. + +.. _cache-memory-usage: + +Indexes / Caches memory usage +----------------------------- + +Here is the estimated memory usage of |project_name| - it's complicated: + + chunk_count ~= total_file_size / 2 ^ HASH_MASK_BITS + + repo_index_usage = chunk_count * 40 + + chunks_cache_usage = chunk_count * 44 + + files_cache_usage = total_file_count * 240 + chunk_count * 80 + + mem_usage ~= repo_index_usage + chunks_cache_usage + files_cache_usage + = chunk_count * 164 + total_file_count * 240 + +Due to the hashtables, the best/usual/worst cases for memory allocation can +be estimated like that: + + mem_allocation = mem_usage / load_factor # l_f = 0.25 .. 0.75 + + mem_allocation_peak = mem_allocation * (1 + growth_factor) # g_f = 1.1 .. 2 + + +All units are Bytes. + +It is assuming every chunk is referenced exactly once (if you have a lot of +duplicate chunks, you will have less chunks than estimated above). + +It is also assuming that typical chunk size is 2^HASH_MASK_BITS (if you have +a lot of files smaller than this statistical medium chunk size, you will have +more chunks than estimated above, because 1 file is at least 1 chunk). + +If a remote repository is used the repo index will be allocated on the remote side. + +The chunks cache, files cache and the repo index are all implemented as hash +tables. A hash table must have a significant amount of unused entries to be +fast - the so-called load factor gives the used/unused elements ratio. + +When a hash table gets full (load factor getting too high), it needs to be +grown (allocate new, bigger hash table, copy all elements over to it, free old +hash table) - this will lead to short-time peaks in memory usage each time this +happens. Usually does not happen for all hashtables at the same time, though. +For small hash tables, we start with a growth factor of 2, which comes down to +~1.1x for big hash tables. + +E.g. backing up a total count of 1 Mi (IEC binary prefix i.e. 2^20) files with a total size of 1TiB. + +a) with ``create --chunker-params 10,23,16,4095`` (custom, like borg < 1.0 or attic): + + mem_usage = 2.8GiB + +b) with ``create --chunker-params 19,23,21,4095`` (default): + + mem_usage = 0.31GiB + +.. note:: There is also the ``--no-files-cache`` option to switch off the files cache. + You'll save some memory, but it will need to read / chunk all the files as + it can not skip unmodified files then. + +Encryption +---------- + +.. seealso:: The :ref:`borgcrypto` section for an in-depth review. + +AES_-256 is used in CTR mode (so no need for padding). A 64bit initialization +vector is used, a `HMAC-SHA256`_ is computed on the encrypted chunk with a +random 64bit nonce and both are stored in the chunk. +The header of each chunk is: ``TYPE(1)`` + ``HMAC(32)`` + ``NONCE(8)`` + ``CIPHERTEXT``. +Encryption and HMAC use two different keys. + +In AES CTR mode you can think of the IV as the start value for the counter. +The counter itself is incremented by one after each 16 byte block. +The IV/counter is not required to be random but it must NEVER be reused. +So to accomplish this |project_name| initializes the encryption counter to be +higher than any previously used counter value before encrypting new data. + +To reduce payload size, only 8 bytes of the 16 bytes nonce is saved in the +payload, the first 8 bytes are always zeros. This does not affect security but +limits the maximum repository capacity to only 295 exabytes (2**64 * 16 bytes). + +Encryption keys (and other secrets) are kept either in a key file on the client +('keyfile' mode) or in the repository config on the server ('repokey' mode). +In both cases, the secrets are generated from random and then encrypted by a +key derived from your passphrase (this happens on the client before the key +is stored into the keyfile or as repokey). + +The passphrase is passed through the ``BORG_PASSPHRASE`` environment variable +or prompted for interactive usage. + +.. _key_files: + +Key files +--------- + +When initialized with the ``init -e keyfile`` command, |project_name| +needs an associated file in ``$HOME/.config/borg/keys`` to read and write +the repository. The format is based on msgpack_, base64 encoding and +PBKDF2_ SHA256 hashing, which is then encoded again in a msgpack_. + +The internal data structure is as follows: + +version + currently always an integer, 1 + +repository_id + the ``id`` field in the ``config`` ``INI`` file of the repository. + +enc_key + the key used to encrypt data with AES (256 bits) + +enc_hmac_key + the key used to HMAC the encrypted data (256 bits) + +id_key + the key used to HMAC the plaintext chunk data to compute the chunk's id + +chunk_seed + the seed for the buzhash chunking table (signed 32 bit integer) + +Those fields are processed using msgpack_. The utf-8 encoded passphrase +is processed with PBKDF2_ (SHA256_, 100000 iterations, random 256 bit salt) +to give us a derived key. The derived key is 256 bits long. +A `HMAC-SHA256`_ checksum of the above fields is generated with the derived +key, then the derived key is also used to encrypt the above pack of fields. +Then the result is stored in a another msgpack_ formatted as follows: + +version + currently always an integer, 1 + +salt + random 256 bits salt used to process the passphrase + +iterations + number of iterations used to process the passphrase (currently 100000) + +algorithm + the hashing algorithm used to process the passphrase and do the HMAC + checksum (currently the string ``sha256``) + +hash + the HMAC of the encrypted derived key + +data + the derived key, encrypted with AES over a PBKDF2_ SHA256 key + described above + +The resulting msgpack_ is then encoded using base64 and written to the +key file, wrapped using the standard ``textwrap`` module with a header. +The header is a single line with a MAGIC string, a space and a hexadecimal +representation of the repository id. + + +Compression +----------- + +|project_name| supports the following compression methods: + +- none (no compression, pass through data 1:1) +- lz4 (low compression, but super fast) +- zlib (level 0-9, level 0 is no compression [but still adding zlib overhead], + level 1 is low, level 9 is high compression) +- lzma (level 0-9, level 0 is low, level 9 is high compression). + +Speed: none > lz4 > zlib > lzma +Compression: lzma > zlib > lz4 > none + +Be careful, higher zlib and especially lzma compression levels might take a +lot of resources (CPU and memory). + +The overall speed of course also depends on the speed of your target storage. +If that is slow, using a higher compression level might yield better overall +performance. You need to experiment a bit. Maybe just watch your CPU load, if +that is relatively low, increase compression until 1 core is 70-100% loaded. + +Even if your target storage is rather fast, you might see interesting effects: +while doing no compression at all (none) is a operation that takes no time, it +likely will need to store more data to the storage compared to using lz4. +The time needed to transfer and store the additional data might be much more +than if you had used lz4 (which is super fast, but still might compress your +data about 2:1). This is assuming your data is compressible (if you backup +already compressed data, trying to compress them at backup time is usually +pointless). + +Compression is applied after deduplication, thus using different compression +methods in one repo does not influence deduplication. + +See ``borg create --help`` about how to specify the compression level and its default. + +Lock files +---------- + +|project_name| uses locks to get (exclusive or shared) access to the cache and +the repository. + +The locking system is based on creating a directory `lock.exclusive` (for +exclusive locks). Inside the lock directory, there is a file indicating +hostname, process id and thread id of the lock holder. + +There is also a json file `lock.roster` that keeps a directory of all shared +and exclusive lockers. + +If the process can create the `lock.exclusive` directory for a resource, it has +the lock for it. If creation fails (because the directory has already been +created by some other process), lock acquisition fails. + +The cache lock is usually in `~/.cache/borg/REPOID/lock.*`. +The repository lock is in `repository/lock.*`. + +In case you run into troubles with the locks, you can use the ``borg break-lock`` +command after you first have made sure that no |project_name| process is +running on any machine that accesses this resource. Be very careful, the cache +or repository might get damaged if multiple processes use it at the same time. diff --git a/docs/security.rst b/docs/internals/security.rst similarity index 100% rename from docs/security.rst rename to docs/internals/security.rst From 6b21d6308607f7cbae8935efc539b156482a136a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 17 Feb 2017 23:34:42 +0100 Subject: [PATCH 0652/1387] docs: datas: enc: correct factual error -- no nonce involved there. --- docs/internals/data-structures.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 51b34a0b..8a40fb72 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -437,9 +437,9 @@ Encryption .. seealso:: The :ref:`borgcrypto` section for an in-depth review. -AES_-256 is used in CTR mode (so no need for padding). A 64bit initialization -vector is used, a `HMAC-SHA256`_ is computed on the encrypted chunk with a -random 64bit nonce and both are stored in the chunk. +AES_-256 is used in CTR mode (so no need for padding). A 64 bit initialization +vector is used, a `HMAC-SHA256`_ is computed on the encrypted chunk +and both are stored in the chunk. The header of each chunk is: ``TYPE(1)`` + ``HMAC(32)`` + ``NONCE(8)`` + ``CIPHERTEXT``. Encryption and HMAC use two different keys. From c03a7ad8444d0d536bb37fe2fdf88f3721b954db Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 18 Feb 2017 00:01:16 +0100 Subject: [PATCH 0653/1387] docs: datas: enc: 1.1.x mas different MACs --- docs/internals/data-structures.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 8a40fb72..f139a02a 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -438,12 +438,12 @@ Encryption .. seealso:: The :ref:`borgcrypto` section for an in-depth review. AES_-256 is used in CTR mode (so no need for padding). A 64 bit initialization -vector is used, a `HMAC-SHA256`_ is computed on the encrypted chunk +vector is used, a MAC is computed on the encrypted chunk and both are stored in the chunk. -The header of each chunk is: ``TYPE(1)`` + ``HMAC(32)`` + ``NONCE(8)`` + ``CIPHERTEXT``. -Encryption and HMAC use two different keys. +The header of each chunk is: ``TYPE(1)`` + ``MAC(32)`` + ``NONCE(8)`` + ``CIPHERTEXT``. +Encryption and MAC use two different keys. -In AES CTR mode you can think of the IV as the start value for the counter. +In AES-CTR mode you can think of the IV as the start value for the counter. The counter itself is incremented by one after each 16 byte block. The IV/counter is not required to be random but it must NEVER be reused. So to accomplish this |project_name| initializes the encryption counter to be From ecdf6ba25fba40753c7dc5b80aed553a94b5b562 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 18 Feb 2017 00:01:53 +0100 Subject: [PATCH 0654/1387] docs: key enc: correct / clarify some stuff, link to internals/security --- docs/internals/data-structures.rst | 23 ++++++++++++++--------- docs/internals/security.rst | 29 +++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index f139a02a..4d4d5ea9 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -467,11 +467,16 @@ or prompted for interactive usage. Key files --------- +.. seealso:: The :ref:`key_encryption` section for an in-depth review of the key encryption. + When initialized with the ``init -e keyfile`` command, |project_name| needs an associated file in ``$HOME/.config/borg/keys`` to read and write the repository. The format is based on msgpack_, base64 encoding and PBKDF2_ SHA256 hashing, which is then encoded again in a msgpack_. +The same data structure is also used in the "repokey" modes, which store +it in the repository in the configuration file. + The internal data structure is as follows: version @@ -492,12 +497,14 @@ id_key chunk_seed the seed for the buzhash chunking table (signed 32 bit integer) -Those fields are processed using msgpack_. The utf-8 encoded passphrase +These fields are packed using msgpack_. The utf-8 encoded passphrase is processed with PBKDF2_ (SHA256_, 100000 iterations, random 256 bit salt) -to give us a derived key. The derived key is 256 bits long. -A `HMAC-SHA256`_ checksum of the above fields is generated with the derived -key, then the derived key is also used to encrypt the above pack of fields. -Then the result is stored in a another msgpack_ formatted as follows: +to derive a 256 bit key encryption key (KEK). + +A `HMAC-SHA256`_ checksum of the packed fields is generated with the KEK, +then the KEK is also used to encrypt the same packed fields using AES-CTR. + +The result is stored in a another msgpack_ formatted as follows: version currently always an integer, 1 @@ -513,18 +520,16 @@ algorithm checksum (currently the string ``sha256``) hash - the HMAC of the encrypted derived key + HMAC-SHA256 of the *plaintext* of the packed fields. data - the derived key, encrypted with AES over a PBKDF2_ SHA256 key - described above + The encrypted, packed fields. The resulting msgpack_ is then encoded using base64 and written to the key file, wrapped using the standard ``textwrap`` module with a header. The header is a single line with a MAGIC string, a space and a hexadecimal representation of the repository id. - Compression ----------- diff --git a/docs/internals/security.rst b/docs/internals/security.rst index 7b54b6a8..5d36cb60 100644 --- a/docs/internals/security.rst +++ b/docs/internals/security.rst @@ -174,6 +174,8 @@ Decryption:: Borg does not support "passphrase" mode otherwise any more. +.. _key_encryption: + Offline key security -------------------- @@ -185,12 +187,11 @@ user-chosen passphrase. A 256 bit key encryption key (KEK) is derived from the passphrase using PBKDF2-HMAC-SHA256 with a random 256 bit salt which is then used -to Encrypt-then-MAC a packed representation of the keys with -AES-256-CTR with a constant initialization vector of 0 (this is the -same construction used for Encryption_ with HMAC-SHA-256). - -The resulting MAC is stored alongside the ciphertext, which is -converted to base64 in its entirety. +to Encrypt-*and*-MAC (unlike the Encrypt-*then*-MAC approach used +otherwise) a packed representation of the keys with AES-256-CTR with a +constant initialization vector of 0. A HMAC-SHA256 of the plaintext is +generated using the same KEK and is stored alongside the ciphertext, +which is converted to base64 in its entirety. This base64 blob (commonly referred to as *keyblob*) is then stored in the key file or in the repository config (keyfile and repokey modes @@ -198,9 +199,19 @@ respectively). This scheme, and specifically the use of a constant IV with the CTR mode, is secure because an identical passphrase will result in a -different derived KEK for every encryption due to the salt. +different derived KEK for every key encryption due to the salt. -Refer to the :ref:`key_files` section for details on the format. +The use of Encrypt-and-MAC instead of Encrypt-then-MAC is seen as +uncritical (but not ideal) here, since it is combined with AES-CTR mode, +which is not vulnerable to padding attacks. + + +.. seealso:: + + Refer to the :ref:`key_files` section for details on the format. + + Refer to issue :issue:`747` for suggested improvements of the encryption + scheme and password-based key derivation. Implementations used -------------------- @@ -223,6 +234,8 @@ Implemented cryptographic constructions are: - Encrypt-then-MAC based on AES-256-CTR and either HMAC-SHA-256 or keyed BLAKE2b256 as described above under Encryption_. +- Encrypt-and-MAC based on AES-256-CTR and HMAC-SHA-256 + as described above under `Offline key security`_. - HKDF_-SHA-512 .. _Horton principle: https://en.wikipedia.org/wiki/Horton_Principle From 16525258586483a9bae7610837ef9d8ce6979cf2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 22 Feb 2017 15:28:27 +0100 Subject: [PATCH 0655/1387] docs: clarify metadata kind, manifest ops --- docs/internals.rst | 10 +++++----- docs/internals/data-structures.rst | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/internals.rst b/docs/internals.rst index 7cfb055c..cfbc679e 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -23,10 +23,10 @@ represent individual backups that contain a full archive of the files specified when the backup was performed. Deduplication is performed globally across all data in the repository -(multiple backups and even multiple hosts), both on data and metadata, -using :ref:`chunks` created by the chunker using the Buzhash_ -algorithm. +(multiple backups and even multiple hosts), both on data and file +metadata, using :ref:`chunks` created by the chunker using the +Buzhash_ algorithm. To actually perform the repository-wide deduplication, a hash of each -chunk is checked against the :ref:`cache`, which is a hash-table of -all chunks that already exist. +chunk is checked against the :ref:`chunks cache `, which is a +hash-table of all chunks that already exist. diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 4d4d5ea9..a76f14e1 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -154,7 +154,7 @@ Each archive info contains: * time It is the last object stored, in the last segment, and is replaced -each time an archive is added or deleted. +each time an archive is added, modified or deleted. .. _archive: From 19d50cff767e733b0cd3680468e9f78667d3d1d1 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 22 Feb 2017 15:53:15 +0100 Subject: [PATCH 0656/1387] docs: internals: move toctree to after the introduction text this only changes the location on the page, nothing about how the TOC is arranged. --- docs/internals.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/internals.rst b/docs/internals.rst index cfbc679e..658f10ba 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -4,11 +4,6 @@ Internals ========= -.. toctree:: - - internals/security - internals/data-structures - This page documents the internal data structures and storage mechanisms of |project_name|. It is partly based on `mailing list discussion about internals`_ and also on static code analysis. @@ -30,3 +25,9 @@ Buzhash_ algorithm. To actually perform the repository-wide deduplication, a hash of each chunk is checked against the :ref:`chunks cache `, which is a hash-table of all chunks that already exist. + +.. toctree:: + :caption: Contents + + internals/security + internals/data-structures From 20a5282a4cd283249ecd77cb35223bc3a04fb154 Mon Sep 17 00:00:00 2001 From: Dan Christensen Date: Tue, 21 Feb 2017 22:48:42 -0500 Subject: [PATCH 0657/1387] In api decorator, pass wait argument to RemoteRepository.call; fixes #2185 --- src/borg/remote.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index b1338212..e8f19ab3 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -425,12 +425,16 @@ def api(*, since, **kwargs_decorator): def do_rpc(self, *args, **kwargs): sig = inspect.signature(f) bound_args = sig.bind(self, *args, **kwargs) - named = {} + named = {} # Arguments for the remote process + extra = {} # Arguments for the local process for name, param in sig.parameters.items(): if name == 'self': continue if name in bound_args.arguments: - named[name] = bound_args.arguments[name] + if name == 'wait': + extra[name] = bound_args.arguments[name] + else: + named[name] = bound_args.arguments[name] else: if param.default is not param.empty: named[name] = param.default @@ -447,7 +451,7 @@ def api(*, since, **kwargs_decorator): raise self.RPCServerOutdated("{0} {1}={2!s}".format(f.__name__, name, named[name]), format_version(restriction['since'])) - return self.call(f.__name__, named) + return self.call(f.__name__, named, **extra) return do_rpc return decorator From 69f7810658956d7abfa9b0c609f89af4727647de Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 22 Feb 2017 16:53:03 +0100 Subject: [PATCH 0658/1387] info: show utilization of maximum archive size See #1452 This is 100 % accurate. Also increases maximum data size by ~41 bytes. Not 100 % side-effect free; if you manage to exactly land in that area then older Borg would not read it. OTOH it gives us a nice round number there. --- src/borg/archive.py | 7 +++++-- src/borg/archiver.py | 1 + src/borg/constants.py | 2 ++ src/borg/repository.py | 6 ++++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 3ebbca9d..07d62e16 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -350,10 +350,13 @@ Archive fingerprint: {0.fpr} Time (start): {start} Time (end): {end} Duration: {0.duration} -Number of files: {0.stats.nfiles}'''.format( +Number of files: {0.stats.nfiles} +Utilization of max. archive size: {csize_max:.0%} +'''.format( self, start=format_time(to_localtime(self.start.replace(tzinfo=timezone.utc))), - end=format_time(to_localtime(self.end.replace(tzinfo=timezone.utc)))) + end=format_time(to_localtime(self.end.replace(tzinfo=timezone.utc))), + csize_max=self.cache.chunks[self.id].csize / MAX_DATA_SIZE) def __repr__(self): return 'Archive(%r)' % self.name diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 37157680..041749a1 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -987,6 +987,7 @@ class Archiver: print('Duration: %s' % archive.duration_from_meta) print('Number of files: %d' % stats.nfiles) print('Command line: %s' % format_cmdline(archive.metadata.cmdline)) + print('Utilization of max. archive size: %d%%' % (100 * cache.chunks[archive.id].csize / MAX_DATA_SIZE)) print(DASHES) print(STATS_HEADER) print(str(stats)) diff --git a/src/borg/constants.py b/src/borg/constants.py index 27ad8c29..18e0bd5b 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -27,6 +27,8 @@ CACHE_TAG_CONTENTS = b'Signature: 8a477f597d28d172789f06886806bc55' # bytes. That's why it's 500 MiB instead of 512 MiB. DEFAULT_MAX_SEGMENT_SIZE = 500 * 1024 * 1024 +MAX_DATA_SIZE = 20 * 1024 * 1024 + # A few hundred files per directory to go easy on filesystems which don't like too many files per dir (NTFS) DEFAULT_SEGMENTS_PER_DIR = 500 diff --git a/src/borg/repository.py b/src/borg/repository.py index 23587355..2999d855 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -26,7 +26,6 @@ from .crc32 import crc32 logger = create_logger(__name__) -MAX_OBJECT_SIZE = 20 * 1024 * 1024 MAGIC = b'BORG_SEG' MAGIC_LEN = len(MAGIC) TAG_PUT = 0 @@ -1204,4 +1203,7 @@ class LoggedIO: return self.segment - 1 # close_segment() increments it -MAX_DATA_SIZE = MAX_OBJECT_SIZE - LoggedIO.put_header_fmt.size +# MAX_OBJECT_SIZE = 20 MiB (MAX_DATA_SIZE) + 41 bytes for a Repository PUT header, which consists of +# a 1 byte tag ID, 4 byte CRC, 4 byte size and 32 bytes for the ID. +MAX_OBJECT_SIZE = MAX_DATA_SIZE + LoggedIO.put_header_fmt.size +assert MAX_OBJECT_SIZE == 20971561 From b0e4f13fba9e415f6d70620ebb539b6c74f85abb Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 23 Feb 2017 00:34:40 +0100 Subject: [PATCH 0659/1387] set MAX_DATA_SIZE = 20971479 bytes in solid stone --- src/borg/constants.py | 4 +++- src/borg/repository.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/borg/constants.py b/src/borg/constants.py index 18e0bd5b..610486d0 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -27,7 +27,9 @@ CACHE_TAG_CONTENTS = b'Signature: 8a477f597d28d172789f06886806bc55' # bytes. That's why it's 500 MiB instead of 512 MiB. DEFAULT_MAX_SEGMENT_SIZE = 500 * 1024 * 1024 -MAX_DATA_SIZE = 20 * 1024 * 1024 +# 20 MiB minus 41 bytes for a Repository header (because the "size" field in the Repository includes +# the header, and the total size was set to 20 MiB). +MAX_DATA_SIZE = 20971479 # A few hundred files per directory to go easy on filesystems which don't like too many files per dir (NTFS) DEFAULT_SEGMENTS_PER_DIR = 500 diff --git a/src/borg/repository.py b/src/borg/repository.py index 2999d855..81935f4a 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -1203,7 +1203,7 @@ class LoggedIO: return self.segment - 1 # close_segment() increments it -# MAX_OBJECT_SIZE = 20 MiB (MAX_DATA_SIZE) + 41 bytes for a Repository PUT header, which consists of +# MAX_OBJECT_SIZE = <20 MiB (MAX_DATA_SIZE) + 41 bytes for a Repository PUT header, which consists of # a 1 byte tag ID, 4 byte CRC, 4 byte size and 32 bytes for the ID. MAX_OBJECT_SIZE = MAX_DATA_SIZE + LoggedIO.put_header_fmt.size -assert MAX_OBJECT_SIZE == 20971561 +assert MAX_OBJECT_SIZE == 20971520 == 20 * 1024 * 1024 From cc26bdf8102641b7ac8d4fee376a9b06c5994125 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 23 Feb 2017 11:41:06 +0100 Subject: [PATCH 0660/1387] info: add --json option --- src/borg/archiver.py | 58 ++++++++++++++++++++++++++-------- src/borg/cache.py | 18 +++++++---- src/borg/testsuite/archiver.py | 10 ++++++ 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 041749a1..f45ef2ab 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -65,6 +65,10 @@ from .upgrader import AtticRepositoryUpgrader, BorgRepositoryUpgrader STATS_HEADER = " Original size Compressed size Deduplicated size" +def print_as_json(obj): + print(json.dumps(obj, sort_keys=True, indent=4)) + + def argument(args, str_or_bool): """If bool is passed, return it. If str is passed, retrieve named attribute from args.""" if isinstance(str_or_bool, str): @@ -960,7 +964,7 @@ class Archiver: if any((args.location.archive, args.first, args.last, args.prefix)): return self._info_archives(args, repository, manifest, key, cache) else: - return self._info_repository(repository, key, cache) + return self._info_repository(args, repository, key, cache) def _info_archives(self, args, repository, manifest, key, cache): def format_cmdline(cmdline): @@ -998,20 +1002,44 @@ class Archiver: print() return self.exit_code - def _info_repository(self, repository, key, cache): - print('Repository ID: %s' % bin_to_hex(repository.id)) - if key.NAME == 'plaintext': - encrypted = 'No' + def _info_repository(self, args, repository, key, cache): + if args.json: + encryption = { + 'mode': key.NAME, + } + if key.NAME.startswith('key file'): + encryption['keyfile'] = key.find_key() else: - encrypted = 'Yes (%s)' % key.NAME - print('Encrypted: %s' % encrypted) - if key.NAME.startswith('key file'): - print('Key file: %s' % key.find_key()) - print('Cache: %s' % cache.path) - print('Security dir: %s' % cache.security_manager.dir) - print(DASHES) - print(STATS_HEADER) - print(str(cache)) + encryption = 'Encrypted: ' + if key.NAME == 'plaintext': + encryption += 'No' + else: + encryption += 'Yes (%s)' % key.NAME + if key.NAME.startswith('key file'): + encryption += '\nKey file: %s' % key.find_key() + + info = { + 'id': bin_to_hex(repository.id), + 'location': repository._location.canonical_path(), + 'cache': cache.path, + 'security_dir': cache.security_manager.dir, + 'encryption': encryption, + } + + if args.json: + info['cache-stats'] = cache.stats() + print_as_json(info) + else: + print(textwrap.dedent(""" + Repository ID: {id} + Location: {location} + {encryption} + Cache: {cache} + Security dir: {security_dir} + """).strip().format_map(info)) + print(DASHES) + print(STATS_HEADER) + print(str(cache)) return self.exit_code @with_repository(exclusive=True) @@ -2542,6 +2570,8 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='', type=location_validator(), help='archive or repository to display information about') + subparser.add_argument('--json', action='store_true', + help='format output as JSON') self.add_archives_filters_args(subparser) break_lock_epilog = process_epilog(""" diff --git a/src/borg/cache.py b/src/borg/cache.py index 21efcbc9..f1a8ef85 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -219,18 +219,22 @@ All archives: {0.total_size:>20s} {0.total_csize:>20s} {0.unique_csize:>20s} Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" return fmt.format(self.format_tuple()) - def format_tuple(self): + Summary = namedtuple('Summary', ['total_size', 'total_csize', 'unique_size', 'unique_csize', 'total_unique_chunks', + 'total_chunks']) + + def stats(self): # XXX: this should really be moved down to `hashindex.pyx` - Summary = namedtuple('Summary', ['total_size', 'total_csize', 'unique_size', 'unique_csize', 'total_unique_chunks', 'total_chunks']) - stats = Summary(*self.chunks.summarize())._asdict() + stats = self.Summary(*self.chunks.summarize())._asdict() + return stats + + def format_tuple(self): + stats = self.stats() for field in ['total_size', 'total_csize', 'unique_csize']: stats[field] = format_file_size(stats[field]) - return Summary(**stats) + return self.Summary(**stats) def chunks_stored_size(self): - Summary = namedtuple('Summary', ['total_size', 'total_csize', 'unique_size', 'unique_csize', 'total_unique_chunks', 'total_chunks']) - stats = Summary(*self.chunks.summarize()) - return stats.unique_csize + return self.stats()['unique_csize'] def create(self): """Create a new empty cache at `self.path` diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index cce0c92f..5e6dad87 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1112,6 +1112,16 @@ class ArchiverTestCase(ArchiverTestCaseBase): info_archive = self.cmd('info', '--first', '1', self.repository_location) assert 'Archive name: test\n' in info_archive + def test_info_json(self): + self.create_regular_file('file1', size=1024 * 80) + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + info_repo = json.loads(self.cmd('info', '--json', self.repository_location)) + assert len(info_repo['id']) == 64 + assert info_repo['encryption']['mode'] == 'repokey' + assert 'keyfile' not in info_repo['encryption'] + assert 'cache-stats' in info_repo + def test_comment(self): self.create_regular_file('file1', size=1024 * 80) self.cmd('init', '--encryption=repokey', self.repository_location) From 7cbade2f8c117fcab59b9dbbb0152e28ea6abd11 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 23 Feb 2017 11:54:57 +0100 Subject: [PATCH 0661/1387] create: add --json option --- src/borg/archive.py | 21 +++++++++++++++++++++ src/borg/archiver.py | 25 +++++++++++++++++-------- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 07d62e16..2073080a 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -68,6 +68,14 @@ class Statistics: return "<{cls} object at {hash:#x} ({self.osize}, {self.csize}, {self.usize})>".format( cls=type(self).__name__, hash=id(self), self=self) + def as_dict(self): + return { + 'original_size': self.osize, + 'compressed_size': self.csize, + 'deduplicated_size': self.usize, + 'nfiles': self.nfiles, + } + @property def osize_fmt(self): return format_file_size(self.osize) @@ -343,6 +351,19 @@ class Archive: def duration_from_meta(self): return format_timedelta(self.ts_end - self.ts) + def info(self): + return { + 'name': self.name, + 'id': self.fpr, + 'start': format_time(to_localtime(self.start.replace(tzinfo=timezone.utc))), + 'end': format_time(to_localtime(self.end.replace(tzinfo=timezone.utc))), + 'duration': (self.end - self.start).total_seconds(), + 'nfiles': self.stats.nfiles, + 'limits': { + 'max_archive_size': self.cache.chunks[self.id].csize / MAX_DATA_SIZE, + }, + } + def __str__(self): return '''\ Archive name: {0.name} diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f45ef2ab..76ea8992 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -369,13 +369,20 @@ class Archiver: if args.progress: archive.stats.show_progress(final=True) if args.stats: - log_multi(DASHES, - str(archive), - DASHES, - STATS_HEADER, - str(archive.stats), - str(cache), - DASHES, logger=logging.getLogger('borg.output.stats')) + if args.json: + print_as_json({ + 'cache_stats': cache.stats(), + 'stats': archive.stats.as_dict(), + 'archive': archive.info(), + }) + else: + log_multi(DASHES, + str(archive), + DASHES, + STATS_HEADER, + str(archive.stats), + str(cache), + DASHES, logger=logging.getLogger('borg.output.stats')) self.output_filter = args.output_filter self.output_list = args.output_list @@ -1027,7 +1034,7 @@ class Archiver: } if args.json: - info['cache-stats'] = cache.stats() + info['cache_stats'] = cache.stats() print_as_json(info) else: print(textwrap.dedent(""" @@ -2174,6 +2181,8 @@ class Archiver: help='output verbose list of items (files, dirs, ...)') subparser.add_argument('--filter', dest='output_filter', metavar='STATUSCHARS', help='only display items with the given status characters') + subparser.add_argument('--json', action='store_true', + help='output stats as JSON') exclude_group = subparser.add_argument_group('Exclusion options') exclude_group.add_argument('-e', '--exclude', dest='patterns', From 2ab5d0f2139884ea873f5e7324213ed10c53a908 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 23 Feb 2017 12:09:09 +0100 Subject: [PATCH 0662/1387] use custom JSON encoder for repr'ing Borg objects consistently --- src/borg/archiver.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 76ea8992..0f959456 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -65,8 +65,25 @@ from .upgrader import AtticRepositoryUpgrader, BorgRepositoryUpgrader STATS_HEADER = " Original size Compressed size Deduplicated size" +class BorgJsonEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, Repository) or isinstance(o, RemoteRepository): + return { + 'id': bin_to_hex(o.id), + 'location': o._location.canonical_path(), + } + if isinstance(o, Archive): + return o.info() + if isinstance(o, Cache): + return { + 'path': o.path, + 'stats': o.stats(), + } + return super().default(o) + + def print_as_json(obj): - print(json.dumps(obj, sort_keys=True, indent=4)) + print(json.dumps(obj, sort_keys=True, indent=4, cls=BorgJsonEncoder)) def argument(args, str_or_bool): @@ -371,9 +388,10 @@ class Archiver: if args.stats: if args.json: print_as_json({ - 'cache_stats': cache.stats(), + 'repository': repository, + 'cache': cache, 'stats': archive.stats.as_dict(), - 'archive': archive.info(), + 'archive': archive, }) else: log_multi(DASHES, @@ -1026,9 +1044,8 @@ class Archiver: encryption += '\nKey file: %s' % key.find_key() info = { - 'id': bin_to_hex(repository.id), - 'location': repository._location.canonical_path(), - 'cache': cache.path, + 'repository': repository, + 'cache': cache, 'security_dir': cache.security_manager.dir, 'encryption': encryption, } @@ -1041,9 +1058,12 @@ class Archiver: Repository ID: {id} Location: {location} {encryption} - Cache: {cache} + Cache: {cache.path} Security dir: {security_dir} - """).strip().format_map(info)) + """).strip().format( + id=bin_to_hex(repository.id), + location=repository._location.canonical_path(), + **info)) print(DASHES) print(STATS_HEADER) print(str(cache)) From 6180f5055c3f4c1b60bbf1c85f59afaa8d8f5a27 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 23 Feb 2017 12:28:01 +0100 Subject: [PATCH 0663/1387] info: --json for archives --- src/borg/archive.py | 30 ++++++++++++++++++++++------ src/borg/archiver.py | 47 +++++++++++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 2073080a..7b87c0bd 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -290,7 +290,8 @@ class Archive: self.end = end self.consider_part_files = consider_part_files self.pipeline = DownloadPipeline(self.repository, self.key) - if create: + self.create = create + if self.create: self.file_compression_logger = create_logger('borg.debug.file-compression') self.items_buffer = CacheChunkBuffer(self.cache, self.key, self.stats) self.chunker = Chunker(self.key.chunk_seed, *chunker_params) @@ -352,17 +353,34 @@ class Archive: return format_timedelta(self.ts_end - self.ts) def info(self): - return { + if self.create: + stats = self.stats + start = self.start.replace(tzinfo=timezone.utc) + end = self.end.replace(tzinfo=timezone.utc) + else: + stats = self.calc_stats(self.cache) + start = self.ts + end = self.ts_end + info = { 'name': self.name, 'id': self.fpr, - 'start': format_time(to_localtime(self.start.replace(tzinfo=timezone.utc))), - 'end': format_time(to_localtime(self.end.replace(tzinfo=timezone.utc))), - 'duration': (self.end - self.start).total_seconds(), - 'nfiles': self.stats.nfiles, + 'start': format_time(to_localtime(start)), + 'end': format_time(to_localtime(end)), + 'duration': (end - start).total_seconds(), + 'stats': stats.as_dict(), 'limits': { 'max_archive_size': self.cache.chunks[self.id].csize / MAX_DATA_SIZE, }, } + if self.create: + info['command_line'] = sys.argv + else: + info.update({ + 'command_line': self.metadata.cmdline, + 'hostname': self.metadata.hostname, + 'username': self.metadata.username, + }) + return info def __str__(self): return '''\ diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 0f959456..214b5746 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -390,7 +390,6 @@ class Archiver: print_as_json({ 'repository': repository, 'cache': cache, - 'stats': archive.stats.as_dict(), 'archive': archive, }) else: @@ -1002,29 +1001,41 @@ class Archiver: if not archive_names: return self.exit_code + output_data = [] + for i, archive_name in enumerate(archive_names, 1): archive = Archive(repository, key, manifest, archive_name, cache=cache, consider_part_files=args.consider_part_files) - stats = archive.calc_stats(cache) - print('Archive name: %s' % archive.name) - print('Archive fingerprint: %s' % archive.fpr) - print('Comment: %s' % archive.metadata.get('comment', '')) - print('Hostname: %s' % archive.metadata.hostname) - print('Username: %s' % archive.metadata.username) - print('Time (start): %s' % format_time(to_localtime(archive.ts))) - print('Time (end): %s' % format_time(to_localtime(archive.ts_end))) - print('Duration: %s' % archive.duration_from_meta) - print('Number of files: %d' % stats.nfiles) - print('Command line: %s' % format_cmdline(archive.metadata.cmdline)) - print('Utilization of max. archive size: %d%%' % (100 * cache.chunks[archive.id].csize / MAX_DATA_SIZE)) - print(DASHES) - print(STATS_HEADER) - print(str(stats)) - print(str(cache)) + if args.json: + output_data.append(archive.info()) + else: + stats = archive.calc_stats(cache) + print('Archive name: %s' % archive.name) + print('Archive fingerprint: %s' % archive.fpr) + print('Comment: %s' % archive.metadata.get('comment', '')) + print('Hostname: %s' % archive.metadata.hostname) + print('Username: %s' % archive.metadata.username) + print('Time (start): %s' % format_time(to_localtime(archive.ts))) + print('Time (end): %s' % format_time(to_localtime(archive.ts_end))) + print('Duration: %s' % archive.duration_from_meta) + print('Number of files: %d' % stats.nfiles) + print('Command line: %s' % format_cmdline(archive.metadata.cmdline)) + print('Utilization of max. archive size: %d%%' % (100 * cache.chunks[archive.id].csize / MAX_DATA_SIZE)) + print(DASHES) + print(STATS_HEADER) + print(str(stats)) + print(str(cache)) if self.exit_code: break - if len(archive_names) - i: + if not args.json and len(archive_names) - i: print() + + if args.json: + print_as_json({ + 'repository': repository, + 'cache': cache, + 'archives': output_data, + }) return self.exit_code def _info_repository(self, args, repository, key, cache): From 25781f53d4c4d76a4070003f96eb6d9906337578 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 23 Feb 2017 12:32:26 +0100 Subject: [PATCH 0664/1387] list: --json for archive listing --- src/borg/archiver.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 214b5746..e380917b 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -977,8 +977,19 @@ class Archiver: format = "{archive:<36} {time} [{id}]{NL}" formatter = ArchiveFormatter(format) + output_data = [] + for archive_info in manifest.archives.list_considering(args): - write(safe_encode(formatter.format_item(archive_info))) + if args.json: + output_data.append(formatter.get_item_data(archive_info)) + else: + write(safe_encode(formatter.format_item(archive_info))) + + if args.json: + print_as_json({ + 'repository': manifest.repository, + 'archives': output_data, + }) return self.exit_code @@ -2492,6 +2503,8 @@ class Archiver: subparser.add_argument('--format', '--list-format', dest='format', type=str, help="""specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")""") + subparser.add_argument('--json', action='store_true', + help='format output as JSON') subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='', type=location_validator(), help='repository/archive to list contents of') From 1f8c0929bf5013d40c27e9cba84de9c8915c022b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 23 Feb 2017 12:51:57 +0100 Subject: [PATCH 0665/1387] list: --json for archive contents listing --- src/borg/archiver.py | 8 ++++++-- src/borg/helpers.py | 42 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e380917b..f0e6d7e0 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -962,10 +962,12 @@ class Archiver: format = "{path}{NL}" else: format = "{mode} {user:6} {group:6} {size:8} {isomtime} {path}{extra}{NL}" - formatter = ItemFormatter(archive, format) + formatter = ItemFormatter(archive, format, json=args.json) + write(safe_encode(formatter.begin())) for item in archive.iter_items(lambda item: matcher.match(item.path)): write(safe_encode(formatter.format_item(item))) + write(safe_encode(formatter.end())) return self.exit_code def _list_repository(self, args, manifest, write): @@ -2504,7 +2506,9 @@ class Archiver: help="""specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")""") subparser.add_argument('--json', action='store_true', - help='format output as JSON') + help='format output as JSON. The form of --format is ignored, but keys used in it ' + 'are added to the JSON output. Some keys are always present. Note: JSON can only ' + 'represent text. A "bpath" key is therefore not available.') subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='', type=location_validator(), help='repository/archive to list contents of') diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 89b557e5..9964cb44 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -5,6 +5,7 @@ import grp import hashlib import logging import io +import json import os import os.path import platform @@ -1620,8 +1621,9 @@ class ItemFormatter(BaseFormatter): assert not keys, str(keys) return "\n".join(help) - def __init__(self, archive, format): + def __init__(self, archive, format, *, json=False): self.archive = archive + self.json = json static_keys = { 'archivename': archive.name, 'archiveid': archive.fpr, @@ -1646,7 +1648,34 @@ class ItemFormatter(BaseFormatter): for hash_function in hashlib.algorithms_guaranteed: self.add_key(hash_function, partial(self.hash_item, hash_function)) self.used_call_keys = set(self.call_keys) & self.format_keys - self.item_data = static_keys + if self.json: + self.item_data = {} + self.format_item = self.format_item_json + self.first = True + else: + self.item_data = static_keys + + def begin(self): + from borg.archiver import BorgJsonEncoder + if not self.json: + return '' + return textwrap.dedent(""" + {{ + "repository": {repository}, + "files": [ + """).strip().format(repository=BorgJsonEncoder().encode(self.archive.repository)) + + def end(self): + if not self.json: + return '' + return "]}" + + def format_item_json(self, item): + if self.first: + self.first = False + return json.dumps(self.get_item_data(item)) + else: + return ',' + json.dumps(self.get_item_data(item)) def add_key(self, key, callable_with_item): self.call_keys[key] = callable_with_item @@ -1673,12 +1702,15 @@ class ItemFormatter(BaseFormatter): item_data['uid'] = item.uid item_data['gid'] = item.gid item_data['path'] = remove_surrogates(item.path) - item_data['bpath'] = item.path + if self.json: + item_data['healthy'] = 'chunks_healthy' not in item + else: + item_data['bpath'] = item.path + item_data['extra'] = extra + item_data['health'] = 'broken' if 'chunks_healthy' in item else 'healthy' item_data['source'] = source item_data['linktarget'] = source - item_data['extra'] = extra item_data['flags'] = item.get('bsdflags') - item_data['health'] = 'broken' if 'chunks_healthy' in item else 'healthy' for key in self.used_call_keys: item_data[key] = self.call_keys[key](item) return item_data From 56563a4392600f096e5966574b44f9132cb43823 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 23 Feb 2017 14:27:32 +0100 Subject: [PATCH 0666/1387] move JSON generation and utilities to helpers --- src/borg/archiver.py | 70 +++++++++------------------------- src/borg/helpers.py | 64 +++++++++++++++++++++++++++---- src/borg/testsuite/archiver.py | 65 ++++++++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 61 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f0e6d7e0..39504dc0 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -52,6 +52,7 @@ from .helpers import parse_pattern, PatternMatcher, PathPrefixPattern from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm from .helpers import ErrorIgnoringTextIOWrapper from .helpers import ProgressIndicatorPercent +from .helpers import BorgJsonEncoder, basic_json_data, json_print from .item import Item from .key import key_creator, tam_required_file, tam_required, RepoKey, PassphraseKey from .keymanager import KeyManager @@ -65,27 +66,6 @@ from .upgrader import AtticRepositoryUpgrader, BorgRepositoryUpgrader STATS_HEADER = " Original size Compressed size Deduplicated size" -class BorgJsonEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, Repository) or isinstance(o, RemoteRepository): - return { - 'id': bin_to_hex(o.id), - 'location': o._location.canonical_path(), - } - if isinstance(o, Archive): - return o.info() - if isinstance(o, Cache): - return { - 'path': o.path, - 'stats': o.stats(), - } - return super().default(o) - - -def print_as_json(obj): - print(json.dumps(obj, sort_keys=True, indent=4, cls=BorgJsonEncoder)) - - def argument(args, str_or_bool): """If bool is passed, return it. If str is passed, retrieve named attribute from args.""" if isinstance(str_or_bool, str): @@ -385,13 +365,12 @@ class Archiver: archive.save(comment=args.comment, timestamp=args.timestamp) if args.progress: archive.stats.show_progress(final=True) + args.stats |= args.json if args.stats: if args.json: - print_as_json({ - 'repository': repository, - 'cache': cache, + json_print(basic_json_data(manifest, cache=cache, extra={ 'archive': archive, - }) + })) else: log_multi(DASHES, str(archive), @@ -988,10 +967,9 @@ class Archiver: write(safe_encode(formatter.format_item(archive_info))) if args.json: - print_as_json({ - 'repository': manifest.repository, - 'archives': output_data, - }) + json_print(basic_json_data(manifest, extra={ + 'archives': output_data + })) return self.exit_code @@ -1001,7 +979,7 @@ class Archiver: if any((args.location.archive, args.first, args.last, args.prefix)): return self._info_archives(args, repository, manifest, key, cache) else: - return self._info_repository(args, repository, key, cache) + return self._info_repository(args, repository, manifest, key, cache) def _info_archives(self, args, repository, manifest, key, cache): def format_cmdline(cmdline): @@ -1044,20 +1022,18 @@ class Archiver: print() if args.json: - print_as_json({ - 'repository': repository, - 'cache': cache, + json_print(basic_json_data(manifest, cache=cache, extra={ 'archives': output_data, - }) + })) return self.exit_code - def _info_repository(self, args, repository, key, cache): + def _info_repository(self, args, repository, manifest, key, cache): + info = basic_json_data(manifest, cache=cache, extra={ + 'security_dir': cache.security_manager.dir, + }) + if args.json: - encryption = { - 'mode': key.NAME, - } - if key.NAME.startswith('key file'): - encryption['keyfile'] = key.find_key() + json_print(info) else: encryption = 'Encrypted: ' if key.NAME == 'plaintext': @@ -1066,18 +1042,8 @@ class Archiver: encryption += 'Yes (%s)' % key.NAME if key.NAME.startswith('key file'): encryption += '\nKey file: %s' % key.find_key() + info['encryption'] = encryption - info = { - 'repository': repository, - 'cache': cache, - 'security_dir': cache.security_manager.dir, - 'encryption': encryption, - } - - if args.json: - info['cache_stats'] = cache.stats() - print_as_json(info) - else: print(textwrap.dedent(""" Repository ID: {id} Location: {location} @@ -2226,7 +2192,7 @@ class Archiver: subparser.add_argument('--filter', dest='output_filter', metavar='STATUSCHARS', help='only display items with the given status characters') subparser.add_argument('--json', action='store_true', - help='output stats as JSON') + help='output stats as JSON (implies --stats)') exclude_group = subparser.add_argument_group('Exclusion options') exclude_group.add_argument('-e', '--exclude', dest='patterns', diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 9964cb44..39bbf01a 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -207,6 +207,10 @@ class Manifest: def id_str(self): return bin_to_hex(self.id) + @property + def last_timestamp(self): + return datetime.strptime(self.timestamp, "%Y-%m-%dT%H:%M:%S.%f") + @classmethod def load(cls, repository, key=None, force_tam_not_required=False): from .item import ManifestItem @@ -251,7 +255,7 @@ class Manifest: if self.timestamp is None: self.timestamp = datetime.utcnow().isoformat() else: - prev_ts = datetime.strptime(self.timestamp, "%Y-%m-%dT%H:%M:%S.%f") + prev_ts = self.last_timestamp incremented = (prev_ts + timedelta(microseconds=1)).isoformat() self.timestamp = max(incremented, datetime.utcnow().isoformat()) manifest = ManifestItem( @@ -1656,14 +1660,13 @@ class ItemFormatter(BaseFormatter): self.item_data = static_keys def begin(self): - from borg.archiver import BorgJsonEncoder if not self.json: return '' - return textwrap.dedent(""" - {{ - "repository": {repository}, - "files": [ - """).strip().format(repository=BorgJsonEncoder().encode(self.archive.repository)) + begin = json_dump(basic_json_data(self.archive.manifest)) + begin, _, _ = begin.rpartition('\n}') # remove last closing brace, we want to extend the object + begin += ',\n' + begin += ' "files": [\n' + return begin def end(self): if not self.json: @@ -2090,3 +2093,50 @@ def swidth_slice(string, max_width): if reverse: result.reverse() return ''.join(result) + + +class BorgJsonEncoder(json.JSONEncoder): + def default(self, o): + from .repository import Repository + from .remote import RemoteRepository + from .archive import Archive + from .cache import Cache + if isinstance(o, Repository) or isinstance(o, RemoteRepository): + return { + 'id': bin_to_hex(o.id), + 'location': o._location.canonical_path(), + } + if isinstance(o, Archive): + return o.info() + if isinstance(o, Cache): + return { + 'path': o.path, + 'stats': o.stats(), + } + return super().default(o) + + +def basic_json_data(manifest, *, cache=None, extra=None): + key = manifest.key + data = extra or {} + data.update({ + 'repository': BorgJsonEncoder().default(manifest.repository), + 'encryption': { + 'mode': key.NAME, + }, + }) + data['repository']['last_modified'] = format_time(to_localtime(manifest.last_timestamp.replace(tzinfo=timezone.utc))) + if key.NAME.startswith('key file'): + data['encryption']['keyfile'] = key.find_key() + if cache: + data['cache'] = cache + return data + + +def json_dump(obj): + """Dump using BorgJSONEncoder.""" + return json.dumps(obj, sort_keys=True, indent=4, cls=BorgJsonEncoder) + + +def json_print(obj): + print(json_dump(obj)) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 5e6dad87..ca0f4102 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1117,10 +1117,27 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') info_repo = json.loads(self.cmd('info', '--json', self.repository_location)) - assert len(info_repo['id']) == 64 + repository = info_repo['repository'] + assert len(repository['id']) == 64 + assert 'last_modified' in repository assert info_repo['encryption']['mode'] == 'repokey' assert 'keyfile' not in info_repo['encryption'] - assert 'cache-stats' in info_repo + cache = info_repo['cache'] + stats = cache['stats'] + assert all(isinstance(o, int) for o in stats.values()) + assert all(key in stats for key in ('total_chunks', 'total_csize', 'total_size', 'total_unique_chunks', 'unique_csize', 'unique_size')) + + info_archive = json.loads(self.cmd('info', '--json', self.repository_location + '::test')) + assert info_repo['repository'] == info_archive['repository'] + assert info_repo['cache'] == info_archive['cache'] + archives = info_archive['archives'] + assert len(archives) == 1 + archive = archives[0] + assert archive['name'] == 'test' + assert isinstance(archive['command_line'], list) + assert isinstance(archive['duration'], float) + assert len(archive['id']) == 64 + assert 'stats' in archive def test_comment(self): self.create_regular_file('file1', size=1024 * 80) @@ -1273,6 +1290,23 @@ class ArchiverTestCase(ArchiverTestCaseBase): if has_lchflags: self.assert_in("x input/file3", output) + def test_create_json(self): + self.create_regular_file('file1', size=1024 * 80) + self.cmd('init', '--encryption=repokey', self.repository_location) + create_info = json.loads(self.cmd('create', '--json', self.repository_location + '::test', 'input')) + # The usual keys + assert 'encryption' in create_info + assert 'repository' in create_info + assert 'cache' in create_info + assert 'last_modified' in create_info['repository'] + + archive = create_info['archive'] + assert archive['name'] == 'test' + assert isinstance(archive['command_line'], list) + assert isinstance(archive['duration'], float) + assert len(archive['id']) == 64 + assert 'stats' in archive + def test_create_topical(self): now = time.time() self.create_regular_file('file1', size=1024 * 80) @@ -1457,6 +1491,33 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert int(dsize) <= int(size) assert int(dcsize) <= int(csize) + def test_list_json(self): + self.create_regular_file('file1', size=1024 * 80) + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + list_repo = json.loads(self.cmd('list', '--json', self.repository_location)) + repository = list_repo['repository'] + assert len(repository['id']) == 64 + assert 'last_modified' in repository + assert list_repo['encryption']['mode'] == 'repokey' + assert 'keyfile' not in list_repo['encryption'] + + list_archive = json.loads(self.cmd('list', '--json', self.repository_location + '::test')) + assert list_repo['repository'] == list_archive['repository'] + files = list_archive['files'] + assert len(files) == 2 + file1 = files[1] + assert file1['path'] == 'input/file1' + assert file1['size'] == 81920 + + list_archive = json.loads(self.cmd('list', '--json', '--format={sha256}', self.repository_location + '::test')) + assert list_repo['repository'] == list_archive['repository'] + files = list_archive['files'] + assert len(files) == 2 + file1 = files[1] + assert file1['path'] == 'input/file1' + assert file1['sha256'] == 'b2915eb69f260d8d3c25249195f2c8f4f716ea82ec760ae929732c0262442b2b' + def _get_sizes(self, compression, compressible, size=10000): if compressible: contents = b'X' * size From 8cdf192511b46d58bf298a2efc084a5ec1e9c12a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 23 Feb 2017 15:17:17 +0100 Subject: [PATCH 0667/1387] list: add "name" key for consistency with info cmd --- src/borg/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 39bbf01a..792e3457 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1558,6 +1558,7 @@ class ArchiveFormatter(BaseFormatter): def get_item_data(self, archive): return { + 'name': remove_surrogates(archive.name), 'barchive': archive.name, 'archive': remove_surrogates(archive.name), 'id': bin_to_hex(archive.id), @@ -1566,7 +1567,7 @@ class ArchiveFormatter(BaseFormatter): @staticmethod def keys_help(): - return " - archive: archive name interpreted as text (might be missing non-text characters, see barchive)\n" \ + return " - archive, name: archive name interpreted as text (might be missing non-text characters, see barchive)\n" \ " - barchive: verbatim archive name, can contain any character except NUL\n" \ " - time: time of creation of the archive\n" \ " - id: internal ID of the archive" From f3c7e7cd3676e3ce4ec76f81acc33e32d67bf763 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 23 Feb 2017 16:50:52 +0100 Subject: [PATCH 0668/1387] RemoteRepository: account rx/tx bytes --- src/borg/remote.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index e8f19ab3..05ba6c8c 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -507,6 +507,8 @@ class RemoteRepository: self.location = self._location = location self.preload_ids = [] self.msgid = 0 + self.rx_bytes = 0 + self.tx_bytes = 0 self.to_send = b'' self.chunkid_to_msgids = {} self.ignore_responses = set() @@ -607,6 +609,8 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. # in any case, we want to cleanly close the repo, even if the # rollback can not succeed (e.g. because the connection was # already closed) and raised another exception: + logger.debug('RemoteRepository: %d bytes sent, %d bytes received, %d messages sent', + self.tx_bytes, self.rx_bytes, self.msgid) self.close() @property @@ -728,6 +732,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. data = os.read(fd, BUFSIZE) if not data: raise ConnectionClosed() + self.rx_bytes += len(data) self.unpacker.feed(data) for unpacked in self.unpacker: if isinstance(unpacked, dict): @@ -752,6 +757,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. data = os.read(fd, 32768) if not data: raise ConnectionClosed() + self.rx_bytes += len(data) data = data.decode('utf-8') for line in data.splitlines(keepends=True): handle_remote_line(line) @@ -785,7 +791,9 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. if self.to_send: try: - self.to_send = self.to_send[self.ratelimit.write(self.stdin_fd, self.to_send):] + written = self.ratelimit.write(self.stdin_fd, self.to_send) + self.tx_bytes += written + self.to_send = self.to_send[written:] except OSError as e: # io.write might raise EAGAIN even though select indicates # that the fd should be writable From a52b54dc3c864d0fbb0d21707ac93f8fe421fdb8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 14 Feb 2017 06:35:54 +0100 Subject: [PATCH 0669/1387] archived file items: add size metadata if an item has a chunk list, pre-compute the total size and store it into "size" metadata entry. this speeds up access to item size (e.g. for regular files) and could also be used to verify the validity of the chunks list. note about hardlinks: size is only stored for hardlink masters (only they have an own chunk list) --- src/borg/archive.py | 3 +++ src/borg/archiver.py | 11 ++++++++--- src/borg/constants.py | 2 +- src/borg/fuse.py | 1 + src/borg/helpers.py | 5 ++++- src/borg/item.pyx | 9 ++++++++- 6 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 07d62e16..8727b418 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -777,6 +777,7 @@ Utilization of max. archive size: {csize_max:.0%} length = len(item.chunks) # the item should only have the *additional* chunks we processed after the last partial item: item.chunks = item.chunks[from_chunk:] + item.size = sum(chunk.size for chunk in item.chunks) item.path += '.borg_part_%d' % number item.part = number number += 1 @@ -825,6 +826,7 @@ Utilization of max. archive size: {csize_max:.0%} ) fd = sys.stdin.buffer # binary self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd))) + item.size = sum(chunk.size for chunk in item.chunks) self.stats.nfiles += 1 self.add_item(item) return 'i' # stdin @@ -885,6 +887,7 @@ Utilization of max. archive size: {csize_max:.0%} cache.memorize_file(path_hash, st, [c.id for c in item.chunks]) status = status or 'M' # regular file, modified (if not 'A' already) item.update(self.stat_attrs(st, path)) + item.size = sum(chunk.size for chunk in item.chunks) if is_special_file: # we processed a special file like a regular file. reflect that in mode, # so it can be extracted / accessed in FUSE mount like a regular file: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 041749a1..d526006f 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -600,10 +600,15 @@ class Archiver: def sum_chunk_size(item, consider_ids=None): if item.get('deleted'): - return None + size = None else: - return sum(c.size for c in item.chunks - if consider_ids is None or c.id in consider_ids) + if consider_ids is not None: # consider only specific chunks + size = sum(chunk.size for chunk in item.chunks if chunk.id in consider_ids) + else: # consider all chunks + size = item.get('size') + if size is None: + size = sum(chunk.size for chunk in item.chunks) + return size def get_owner(item): if args.numeric_owner: diff --git a/src/borg/constants.py b/src/borg/constants.py index 610486d0..f7cb11c9 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -1,6 +1,6 @@ # this set must be kept complete, otherwise the RobustUnpacker might malfunction: ITEM_KEYS = frozenset(['path', 'source', 'rdev', 'chunks', 'chunks_healthy', 'hardlink_master', - 'mode', 'user', 'group', 'uid', 'gid', 'mtime', 'atime', 'ctime', + 'mode', 'user', 'group', 'uid', 'gid', 'mtime', 'atime', 'ctime', 'size', 'xattrs', 'bsdflags', 'acl_nfs4', 'acl_access', 'acl_default', 'acl_extended', 'part']) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index dbf34e1a..53f60462 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -260,6 +260,7 @@ class FuseOperations(llfuse.Operations): size = 0 dsize = 0 if 'chunks' in item: + # if we would not need to compute dsize, we could get size quickly from item.size, if present. for key, chunksize, _ in item.chunks: size += chunksize if self.accounted_chunks.get(key, inode) == inode: diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 89b557e5..21a45188 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -104,7 +104,7 @@ def check_extension_modules(): raise ExtensionModuleError if platform.API_VERSION != platform.OS_API_VERSION != '1.1_01': raise ExtensionModuleError - if item.API_VERSION != '1.1_01': + if item.API_VERSION != '1.1_02': raise ExtensionModuleError @@ -1701,6 +1701,9 @@ class ItemFormatter(BaseFormatter): return len(item.get('chunks', [])) def calculate_size(self, item): + size = item.get('size') + if size is not None: + return size return sum(c.size for c in item.get('chunks', [])) def calculate_csize(self, item): diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 4ac960a6..a3e78c21 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -2,7 +2,7 @@ from .constants import ITEM_KEYS from .helpers import safe_encode, safe_decode from .helpers import StableDict -API_VERSION = '1.1_01' +API_VERSION = '1.1_02' class PropDict: @@ -156,6 +156,10 @@ class Item(PropDict): ctime = PropDict._make_property('ctime', int) mtime = PropDict._make_property('mtime', int) + # size is only present for items with a chunk list and then it is sum(chunk_sizes) + # compatibility note: this is a new feature, in old archives size will be missing. + size = PropDict._make_property('size', int) + hardlink_master = PropDict._make_property('hardlink_master', bool) chunks = PropDict._make_property('chunks', (list, type(None)), 'list or None') @@ -169,6 +173,9 @@ class Item(PropDict): part = PropDict._make_property('part', int) def file_size(self, hardlink_masters=None): + size = self.get('size') + if size is not None: + return size hardlink_masters = hardlink_masters or {} chunks, _ = hardlink_masters.get(self.get('source'), (None, None)) chunks = self.get('chunks', chunks) From fe8e14cb2ce030a22f3a982db156a38770706b0c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 14 Feb 2017 20:54:25 +0100 Subject: [PATCH 0670/1387] fuse: get rid of chunk accounting the chunk accounting code tried to reflect repo space usage via the st_blocks of the files. so, a specific chunk that was shared between multiple files [inodes] was only accounted for one specific file. thus, the overall "du" of everything in the fuse mounted repo was maybe correctly reflecting the repo space usage, but the decision which file has the chunk (the space) was kind of arbitrary and not really useful. otoh, a simple fuse getattr() was rather expensive due to this as it needed to iterate over the chunks list to compute the st_blocks value. also it needed quite some memory for the accounting. thus, st_blocks is now just ceil(size / blocksize). also: fixed bug that st_blocks was a floating point value previously. also: preparing for further optimization of size computation (see next cs) --- src/borg/fuse.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 53f60462..db84fcde 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -72,7 +72,6 @@ class FuseOperations(llfuse.Operations): self.contents = defaultdict(dict) self.default_dir = Item(mode=0o40755, mtime=int(time.time() * 1e9), uid=os.getuid(), gid=os.getgid()) self.pending_archives = {} - self.accounted_chunks = {} self.cache = ItemCache() data_cache_capacity = int(os.environ.get('BORG_MOUNT_DATA_CACHE_ENTRIES', os.cpu_count() or 1)) logger.debug('mount data cache capacity: %d chunks', data_cache_capacity) @@ -258,14 +257,9 @@ class FuseOperations(llfuse.Operations): def getattr(self, inode, ctx=None): item = self.get_item(inode) size = 0 - dsize = 0 if 'chunks' in item: - # if we would not need to compute dsize, we could get size quickly from item.size, if present. for key, chunksize, _ in item.chunks: size += chunksize - if self.accounted_chunks.get(key, inode) == inode: - self.accounted_chunks[key] = inode - dsize += chunksize entry = llfuse.EntryAttributes() entry.st_ino = inode entry.generation = 0 @@ -278,7 +272,7 @@ class FuseOperations(llfuse.Operations): entry.st_rdev = item.get('rdev', 0) entry.st_size = size entry.st_blksize = 512 - entry.st_blocks = dsize / 512 + entry.st_blocks = (size + entry.st_blksize - 1) // entry.st_blksize # note: older archives only have mtime (not atime nor ctime) mtime_ns = item.mtime if have_fuse_xtime_ns: From ae6742fb34740499a97b4edce36f984f4e9cd1ba Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 14 Feb 2017 21:08:38 +0100 Subject: [PATCH 0671/1387] fuse: use precomputed size from Item --- src/borg/fuse.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index db84fcde..3b3b3771 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -256,10 +256,6 @@ class FuseOperations(llfuse.Operations): def getattr(self, inode, ctx=None): item = self.get_item(inode) - size = 0 - if 'chunks' in item: - for key, chunksize, _ in item.chunks: - size += chunksize entry = llfuse.EntryAttributes() entry.st_ino = inode entry.generation = 0 @@ -270,9 +266,9 @@ class FuseOperations(llfuse.Operations): entry.st_uid = item.uid entry.st_gid = item.gid entry.st_rdev = item.get('rdev', 0) - entry.st_size = size + entry.st_size = item.file_size() entry.st_blksize = 512 - entry.st_blocks = (size + entry.st_blksize - 1) // entry.st_blksize + entry.st_blocks = (entry.st_size + entry.st_blksize - 1) // entry.st_blksize # note: older archives only have mtime (not atime nor ctime) mtime_ns = item.mtime if have_fuse_xtime_ns: From 0021052dbdd7147c027745c594e98424031855c8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 15 Feb 2017 01:24:20 +0100 Subject: [PATCH 0672/1387] reduce code duplication --- src/borg/archive.py | 6 +++--- src/borg/archiver.py | 4 +--- src/borg/helpers.py | 5 +---- src/borg/item.pyx | 22 +++++++++++++++------- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 8727b418..3c5dbe8c 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -777,7 +777,7 @@ Utilization of max. archive size: {csize_max:.0%} length = len(item.chunks) # the item should only have the *additional* chunks we processed after the last partial item: item.chunks = item.chunks[from_chunk:] - item.size = sum(chunk.size for chunk in item.chunks) + item.file_size(memorize=True) item.path += '.borg_part_%d' % number item.part = number number += 1 @@ -826,7 +826,7 @@ Utilization of max. archive size: {csize_max:.0%} ) fd = sys.stdin.buffer # binary self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd))) - item.size = sum(chunk.size for chunk in item.chunks) + item.file_size(memorize=True) self.stats.nfiles += 1 self.add_item(item) return 'i' # stdin @@ -887,7 +887,7 @@ Utilization of max. archive size: {csize_max:.0%} cache.memorize_file(path_hash, st, [c.id for c in item.chunks]) status = status or 'M' # regular file, modified (if not 'A' already) item.update(self.stat_attrs(st, path)) - item.size = sum(chunk.size for chunk in item.chunks) + item.file_size(memorize=True) if is_special_file: # we processed a special file like a regular file. reflect that in mode, # so it can be extracted / accessed in FUSE mount like a regular file: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index d526006f..a1bc65b6 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -605,9 +605,7 @@ class Archiver: if consider_ids is not None: # consider only specific chunks size = sum(chunk.size for chunk in item.chunks if chunk.id in consider_ids) else: # consider all chunks - size = item.get('size') - if size is None: - size = sum(chunk.size for chunk in item.chunks) + size = item.file_size() return size def get_owner(item): diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 21a45188..f6247cd3 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1701,10 +1701,7 @@ class ItemFormatter(BaseFormatter): return len(item.get('chunks', [])) def calculate_size(self, item): - size = item.get('size') - if size is not None: - return size - return sum(c.size for c in item.get('chunks', [])) + return item.file_size() def calculate_csize(self, item): return sum(c.csize for c in item.get('chunks', [])) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index a3e78c21..a0b9e3ef 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -172,16 +172,24 @@ class Item(PropDict): part = PropDict._make_property('part', int) - def file_size(self, hardlink_masters=None): + def file_size(self, hardlink_masters=None, memorize=False): + """determine the size of this item""" size = self.get('size') if size is not None: return size - hardlink_masters = hardlink_masters or {} - chunks, _ = hardlink_masters.get(self.get('source'), (None, None)) - chunks = self.get('chunks', chunks) - if chunks is None: - return 0 - return sum(chunk.size for chunk in chunks) + chunks = self.get('chunks') + having_chunks = chunks is not None + if not having_chunks: + # this item has no (own) chunks, but if this is a hardlink slave + # and we know the master, we can still compute the size. + hardlink_masters = hardlink_masters or {} + chunks, _ = hardlink_masters.get(self.get('source'), (None, None)) + if chunks is None: + return 0 + size = sum(chunk.size for chunk in chunks) + if memorize and having_chunks: + self.size = size + return size class EncryptedKey(PropDict): From 97bb1b7d9afe7b3f6117b7a27fccb2fd23c41103 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 Feb 2017 06:47:39 +0100 Subject: [PATCH 0673/1387] deduplicate / refactor item (c)size code --- src/borg/cache.py | 3 +-- src/borg/helpers.py | 6 +++-- src/borg/item.pyx | 53 ++++++++++++++++++++++++++++++--------------- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 21efcbc9..cc57e7bc 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -20,13 +20,12 @@ from .helpers import format_file_size from .helpers import yes from .helpers import remove_surrogates from .helpers import ProgressIndicatorPercent, ProgressIndicatorMessage -from .item import Item, ArchiveItem +from .item import Item, ArchiveItem, ChunkListEntry from .key import PlaintextKey from .locking import Lock from .platform import SaveFile from .remote import cache_if_remote -ChunkListEntry = namedtuple('ChunkListEntry', 'id size csize') FileCacheEntry = namedtuple('FileCacheEntry', 'age inode size mtime chunk_ids') diff --git a/src/borg/helpers.py b/src/borg/helpers.py index f6247cd3..2bd5f407 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1701,10 +1701,12 @@ class ItemFormatter(BaseFormatter): return len(item.get('chunks', [])) def calculate_size(self, item): - return item.file_size() + # note: does not support hardlink slaves, they will be size 0 + return item.file_size(compressed=False) def calculate_csize(self, item): - return sum(c.csize for c in item.get('chunks', [])) + # note: does not support hardlink slaves, they will be csize 0 + return item.file_size(compressed=True) def hash_item(self, hash_function, item): if 'chunks' not in item: diff --git a/src/borg/item.pyx b/src/borg/item.pyx index a0b9e3ef..c3125c57 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -1,3 +1,5 @@ +from collections import namedtuple + from .constants import ITEM_KEYS from .helpers import safe_encode, safe_decode from .helpers import StableDict @@ -113,6 +115,8 @@ class PropDict: return property(_get, _set, _del, doc=doc) +ChunkListEntry = namedtuple('ChunkListEntry', 'id size csize') + class Item(PropDict): """ Item abstraction that deals with validation and the low-level details internally: @@ -172,23 +176,38 @@ class Item(PropDict): part = PropDict._make_property('part', int) - def file_size(self, hardlink_masters=None, memorize=False): - """determine the size of this item""" - size = self.get('size') - if size is not None: - return size - chunks = self.get('chunks') - having_chunks = chunks is not None - if not having_chunks: - # this item has no (own) chunks, but if this is a hardlink slave - # and we know the master, we can still compute the size. - hardlink_masters = hardlink_masters or {} - chunks, _ = hardlink_masters.get(self.get('source'), (None, None)) - if chunks is None: - return 0 - size = sum(chunk.size for chunk in chunks) - if memorize and having_chunks: - self.size = size + def file_size(self, hardlink_masters=None, memorize=False, compressed=False): + """determine the (uncompressed or compressed) size of this item""" + attr = 'csize' if compressed else 'size' + try: + size = getattr(self, attr) + except AttributeError: + # no precomputed (c)size value available, compute it: + try: + chunks = getattr(self, 'chunks') + having_chunks = True + except AttributeError: + having_chunks = False + # this item has no (own) chunks list, but if this is a hardlink slave + # and we know the master, we can still compute the size. + if hardlink_masters is None: + chunks = None + else: + try: + master = getattr(self, 'source') + except AttributeError: + # not a hardlink slave, likely a directory or special file w/o chunks + chunks = None + else: + # hardlink slave, try to fetch hardlink master's chunks list + # todo: put precomputed size into hardlink_masters' values and use it, if present + chunks, _ = hardlink_masters.get(master, (None, None)) + if chunks is None: + return 0 + size = sum(getattr(ChunkListEntry(*chunk), attr) for chunk in chunks) + # if requested, memorize the precomputed (c)size for items that have an own chunks list: + if memorize and having_chunks: + setattr(self, attr, size) return size From 50068c596dc4f843ef90def50ef530cb5926f20f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 Feb 2017 07:02:11 +0100 Subject: [PATCH 0674/1387] rename Item.file_size -> get_size file_size is misleading here because one thinks of on-disk file size, but for compressed=True, there is no such on-disk file. --- src/borg/archive.py | 6 +++--- src/borg/archiver.py | 4 ++-- src/borg/fuse.py | 2 +- src/borg/helpers.py | 4 ++-- src/borg/item.pyx | 11 +++++++++-- src/borg/testsuite/item.py | 4 ++-- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 3c5dbe8c..7f89a215 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -777,7 +777,7 @@ Utilization of max. archive size: {csize_max:.0%} length = len(item.chunks) # the item should only have the *additional* chunks we processed after the last partial item: item.chunks = item.chunks[from_chunk:] - item.file_size(memorize=True) + item.get_size(memorize=True) item.path += '.borg_part_%d' % number item.part = number number += 1 @@ -826,7 +826,7 @@ Utilization of max. archive size: {csize_max:.0%} ) fd = sys.stdin.buffer # binary self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd))) - item.file_size(memorize=True) + item.get_size(memorize=True) self.stats.nfiles += 1 self.add_item(item) return 'i' # stdin @@ -887,7 +887,7 @@ Utilization of max. archive size: {csize_max:.0%} cache.memorize_file(path_hash, st, [c.id for c in item.chunks]) status = status or 'M' # regular file, modified (if not 'A' already) item.update(self.stat_attrs(st, path)) - item.file_size(memorize=True) + item.get_size(memorize=True) if is_special_file: # we processed a special file like a regular file. reflect that in mode, # so it can be extracted / accessed in FUSE mount like a regular file: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index a1bc65b6..ccf3c647 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -541,7 +541,7 @@ class Archiver: if progress: pi = ProgressIndicatorPercent(msg='%5.1f%% Extracting: %s', step=0.1) pi.output('Calculating size') - extracted_size = sum(item.file_size(hardlink_masters) for item in archive.iter_items(filter)) + extracted_size = sum(item.get_size(hardlink_masters) for item in archive.iter_items(filter)) pi.total = extracted_size else: pi = None @@ -605,7 +605,7 @@ class Archiver: if consider_ids is not None: # consider only specific chunks size = sum(chunk.size for chunk in item.chunks if chunk.id in consider_ids) else: # consider all chunks - size = item.file_size() + size = item.get_size() return size def get_owner(item): diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 3b3b3771..33c6b389 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -266,7 +266,7 @@ class FuseOperations(llfuse.Operations): entry.st_uid = item.uid entry.st_gid = item.gid entry.st_rdev = item.get('rdev', 0) - entry.st_size = item.file_size() + entry.st_size = item.get_size() entry.st_blksize = 512 entry.st_blocks = (entry.st_size + entry.st_blksize - 1) // entry.st_blksize # note: older archives only have mtime (not atime nor ctime) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 2bd5f407..ad03dca4 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1702,11 +1702,11 @@ class ItemFormatter(BaseFormatter): def calculate_size(self, item): # note: does not support hardlink slaves, they will be size 0 - return item.file_size(compressed=False) + return item.get_size(compressed=False) def calculate_csize(self, item): # note: does not support hardlink slaves, they will be csize 0 - return item.file_size(compressed=True) + return item.get_size(compressed=True) def hash_item(self, hash_function, item): if 'chunks' not in item: diff --git a/src/borg/item.pyx b/src/borg/item.pyx index c3125c57..a71da55e 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -176,8 +176,15 @@ class Item(PropDict): part = PropDict._make_property('part', int) - def file_size(self, hardlink_masters=None, memorize=False, compressed=False): - """determine the (uncompressed or compressed) size of this item""" + def get_size(self, hardlink_masters=None, memorize=False, compressed=False): + """ + Determine the (uncompressed or compressed) size of this item. + + For hardlink slaves, the size is computed via the hardlink master's + chunk list, if available (otherwise size will be returned as 0). + + If memorize is True, the computed size value will be stored into the item. + """ attr = 'csize' if compressed else 'size' try: size = getattr(self, attr) diff --git a/src/borg/testsuite/item.py b/src/borg/testsuite/item.py index 35934f3b..9c66b6a6 100644 --- a/src/borg/testsuite/item.py +++ b/src/borg/testsuite/item.py @@ -142,9 +142,9 @@ def test_item_file_size(): ChunkListEntry(csize=1, size=1000, id=None), ChunkListEntry(csize=1, size=2000, id=None), ]) - assert item.file_size() == 3000 + assert item.get_size() == 3000 def test_item_file_size_no_chunks(): item = Item() - assert item.file_size() == 0 + assert item.get_size() == 0 From 4f1db82f6d45e481acfaf0d302b7554d1e773238 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 23 Feb 2017 21:34:13 +0100 Subject: [PATCH 0675/1387] info : use Archive.info() for both JSON and human display --- src/borg/archive.py | 9 +++++---- src/borg/archiver.py | 45 ++++++++++++++++++++++++-------------------- src/borg/helpers.py | 5 +++++ 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 7b87c0bd..20746aa0 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -28,7 +28,7 @@ from .helpers import Chunk, ChunkIteratorFileWrapper, open_item from .helpers import Error, IntegrityError from .helpers import uid2user, user2uid, gid2group, group2gid from .helpers import parse_timestamp, to_localtime -from .helpers import format_time, format_timedelta, format_file_size, file_status +from .helpers import format_time, format_timedelta, format_file_size, file_status, FileSize from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates from .helpers import StableDict from .helpers import bin_to_hex @@ -70,9 +70,9 @@ class Statistics: def as_dict(self): return { - 'original_size': self.osize, - 'compressed_size': self.csize, - 'deduplicated_size': self.usize, + 'original_size': FileSize(self.osize), + 'compressed_size': FileSize(self.csize), + 'deduplicated_size': FileSize(self.usize), 'nfiles': self.nfiles, } @@ -379,6 +379,7 @@ class Archive: 'command_line': self.metadata.cmdline, 'hostname': self.metadata.hostname, 'username': self.metadata.username, + 'comment': self.metadata.get('comment', ''), }) return info diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 39504dc0..8ecba523 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -17,7 +17,7 @@ import textwrap import time import traceback from binascii import unhexlify -from datetime import datetime +from datetime import datetime, timedelta from itertools import zip_longest from .logger import create_logger, setup_logging @@ -37,7 +37,8 @@ from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from .helpers import Error, NoManifestError from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec from .helpers import PrefixSpec, SortBySpec, HUMAN_SORT_KEYS -from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter, format_time, format_file_size, format_archive +from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter +from .helpers import format_time, format_timedelta, format_file_size, format_archive from .helpers import safe_encode, remove_surrogates, bin_to_hex, prepare_dump_dict from .helpers import prune_within, prune_split from .helpers import to_localtime, timestamp @@ -52,7 +53,7 @@ from .helpers import parse_pattern, PatternMatcher, PathPrefixPattern from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm from .helpers import ErrorIgnoringTextIOWrapper from .helpers import ProgressIndicatorPercent -from .helpers import BorgJsonEncoder, basic_json_data, json_print +from .helpers import basic_json_data, json_print from .item import Item from .key import key_creator, tam_required_file, tam_required, RepoKey, PassphraseKey from .keymanager import KeyManager @@ -997,25 +998,29 @@ class Archiver: for i, archive_name in enumerate(archive_names, 1): archive = Archive(repository, key, manifest, archive_name, cache=cache, consider_part_files=args.consider_part_files) + info = archive.info() if args.json: - output_data.append(archive.info()) + output_data.append(info) else: - stats = archive.calc_stats(cache) - print('Archive name: %s' % archive.name) - print('Archive fingerprint: %s' % archive.fpr) - print('Comment: %s' % archive.metadata.get('comment', '')) - print('Hostname: %s' % archive.metadata.hostname) - print('Username: %s' % archive.metadata.username) - print('Time (start): %s' % format_time(to_localtime(archive.ts))) - print('Time (end): %s' % format_time(to_localtime(archive.ts_end))) - print('Duration: %s' % archive.duration_from_meta) - print('Number of files: %d' % stats.nfiles) - print('Command line: %s' % format_cmdline(archive.metadata.cmdline)) - print('Utilization of max. archive size: %d%%' % (100 * cache.chunks[archive.id].csize / MAX_DATA_SIZE)) - print(DASHES) - print(STATS_HEADER) - print(str(stats)) - print(str(cache)) + info['duration'] = format_timedelta(timedelta(seconds=info['duration'])) + info['command_line'] = format_cmdline(info['command_line']) + print(textwrap.dedent(""" + Archive name: {name} + Archive fingerprint: {id} + Comment: {comment} + Hostname: {hostname} + Username: {username} + Time (start): {start} + Time (end): {end} + Duration: {duration} + Number of files: {stats[nfiles]} + Command line: {command_line} + Utilization of max. archive size: {limits[max_archive_size]:.0%} + ------------------------------------------------------------------------------ + Original size Compressed size Deduplicated size + This archive: {stats[original_size]:>20s} {stats[compressed_size]:>20s} {stats[deduplicated_size]:>20s} + {cache} + """).strip().format(cache=cache, **info)) if self.exit_code: break if not args.json and len(archive_names) - i: diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 792e3457..09b15cc2 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -831,6 +831,11 @@ def format_file_size(v, precision=2, sign=False): return sizeof_fmt_decimal(v, suffix='B', sep=' ', precision=precision, sign=sign) +class FileSize(int): + def __format__(self, format_spec): + return format_file_size(int(self)).__format__(format_spec) + + def parse_file_size(s): """Return int from file size (1234, 55G, 1.7T).""" if not s: From adc4da280de41379cd5e9cebee32a40c78148006 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 Feb 2017 23:09:40 +0100 Subject: [PATCH 0676/1387] borg check: check file size consistency --- src/borg/archive.py | 7 +++++++ src/borg/item.pyx | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 7f89a215..852da6e0 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1306,6 +1306,13 @@ class ArchiveChecker: logger.info('{}: Completely healed previously damaged file!'.format(item.path)) del item.chunks_healthy item.chunks = chunk_list + if 'size' in item: + item_size = item.size + item_chunks_size = item.get_size(compressed=False, from_chunks=True) + if item_size != item_chunks_size: + # just warn, but keep the inconsistency, so that borg extract can warn about it. + logger.warning('{}: size inconsistency detected: size {}, chunks size {}'.format( + item.path, item_size, item_chunks_size)) def robust_iterator(archive): """Iterates through all archive items diff --git a/src/borg/item.pyx b/src/borg/item.pyx index a71da55e..627ffd1f 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -176,7 +176,7 @@ class Item(PropDict): part = PropDict._make_property('part', int) - def get_size(self, hardlink_masters=None, memorize=False, compressed=False): + def get_size(self, hardlink_masters=None, memorize=False, compressed=False, from_chunks=False): """ Determine the (uncompressed or compressed) size of this item. @@ -187,6 +187,8 @@ class Item(PropDict): """ attr = 'csize' if compressed else 'size' try: + if from_chunks: + raise AttributeError size = getattr(self, attr) except AttributeError: # no precomputed (c)size value available, compute it: From 7da0a9c98232850068e9dbc8114a4e7e48e8e7bc Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 20 Feb 2017 22:24:19 +0100 Subject: [PATCH 0677/1387] borg extract: check file size consistency --- src/borg/archive.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 852da6e0..91e94fa5 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -467,13 +467,20 @@ Utilization of max. archive size: {csize_max:.0%} has_damaged_chunks = 'chunks_healthy' in item if dry_run or stdout: if 'chunks' in item: + item_chunks_size = 0 for _, data in self.pipeline.fetch_many([c.id for c in item.chunks], is_preloaded=True): if pi: pi.show(increase=len(data), info=[remove_surrogates(item.path)]) if stdout: sys.stdout.buffer.write(data) + item_chunks_size += len(data) if stdout: sys.stdout.buffer.flush() + if 'size' in item: + item_size = item.size + if item_size != item_chunks_size: + logger.warning('{}: size inconsistency detected: size {}, chunks size {}'.format( + item.path, item_size, item_chunks_size)) if has_damaged_chunks: logger.warning('File %s has damaged (all-zero) chunks. Try running borg check --repair.' % remove_surrogates(item.path)) @@ -530,10 +537,15 @@ Utilization of max. archive size: {csize_max:.0%} else: fd.write(data) with backup_io('truncate'): - pos = fd.tell() + pos = item_chunks_size = fd.tell() fd.truncate(pos) fd.flush() self.restore_attrs(path, item, fd=fd.fileno()) + if 'size' in item: + item_size = item.size + if item_size != item_chunks_size: + logger.warning('{}: size inconsistency detected: size {}, chunks size {}'.format( + item.path, item_size, item_chunks_size)) if has_damaged_chunks: logger.warning('File %s has damaged (all-zero) chunks. Try running borg check --repair.' % remove_surrogates(item.path)) From 4c9bc96fb73987d20cb4df4800497904096ddecf Mon Sep 17 00:00:00 2001 From: Abogical Date: Tue, 21 Feb 2017 20:38:44 +0200 Subject: [PATCH 0678/1387] Print a warning for too big extended attributes --- src/borg/archive.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index f499f923..c9b90462 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -621,7 +621,10 @@ Number of files: {0.stats.nfiles}'''.format( try: xattr.setxattr(fd or path, k, v, follow_symlinks=False) except OSError as e: - if e.errno not in (errno.ENOTSUP, errno.EACCES): + if e.errno == errno.E2BIG: + logger.warning('%s: Value or key of extended attribute %s is too big for this filesystem' % + (path, k.decode())) + elif e.errno not in (errno.ENOTSUP, errno.EACCES): # only raise if the errno is not on our ignore list: # ENOTSUP == xattrs not supported here # EACCES == permission denied to set this specific xattr From e487b8404caf2f073fbecff9be509851e11d5b12 Mon Sep 17 00:00:00 2001 From: Abogical Date: Wed, 22 Feb 2017 16:30:22 +0200 Subject: [PATCH 0679/1387] Add testsuite to test handling of too big xattr --- src/borg/testsuite/archiver.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index cce0c92f..c62e1220 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1029,6 +1029,21 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('extract', self.repository_location + '::test') assert xattr.getxattr('input/file', 'security.capability') == capabilities + @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='xattr not supported on this system or on this version of' + 'fakeroot') + def test_extract_big_xattrs(self): + def patched_setxattr(*args, **kwargs): + raise OSError(errno.E2BIG, 'E2BIG') + self.create_regular_file('file') + xattr.setxattr('input/file', 'attribute', 'value') + self.cmd('init', self.repository_location, '-e' 'none') + self.cmd('create', self.repository_location + '::test', 'input') + with changedir('output'): + with patch.object(xattr, 'setxattr', patched_setxattr): + out = self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING) + assert out == (os.path.abspath('input/file') + ': Value or key of extended attribute attribute is too big' + 'for this filesystem\n') + def test_path_normalization(self): self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('dir1/dir2/file', size=1024 * 80) From 4d81b186ec5b64585bac1097b28b987a9f4bce15 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 19 Feb 2017 07:07:12 +0100 Subject: [PATCH 0680/1387] borg delete --force --force to delete severely corrupted archives, fixes #1975 --- src/borg/archive.py | 6 +++--- src/borg/archiver.py | 27 ++++++++++++++++++++++++--- src/borg/testsuite/archiver.py | 14 ++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 07d62e16..4bebecd7 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -662,7 +662,7 @@ Utilization of max. archive size: {csize_max:.0%} raise ChunksIndexError(cid) except Repository.ObjectNotFound as e: # object not in repo - strange, but we wanted to delete it anyway. - if not forced: + if forced == 0: raise error = True @@ -686,14 +686,14 @@ Utilization of max. archive size: {csize_max:.0%} except (TypeError, ValueError): # if items metadata spans multiple chunks and one chunk got dropped somehow, # it could be that unpacker yields bad types - if not forced: + if forced == 0: raise error = True if progress: pi.finish() except (msgpack.UnpackException, Repository.ObjectNotFound): # items metadata corrupted - if not forced: + if forced == 0: raise error = True # in forced delete mode, we try hard to delete at least the manifest entry, diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 041749a1..efdc16db 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -828,6 +828,26 @@ class Archiver: if not archive_names: return self.exit_code + if args.forced == 2: + deleted = False + for i, archive_name in enumerate(archive_names, 1): + try: + del manifest.archives[archive_name] + except KeyError: + self.exit_code = EXIT_WARNING + logger.warning('Archive {} not found ({}/{}).'.format(archive_name, i, len(archive_names))) + else: + deleted = True + logger.info('Deleted {} ({}/{}).'.format(archive_name, i, len(archive_names))) + if deleted: + manifest.write() + # note: might crash in compact() after committing the repo + repository.commit() + logger.info('Done. Run "borg check --repair" to clean up the mess.') + else: + logger.warning('Aborted.') + return self.exit_code + stats_logger = logging.getLogger('borg.output.stats') if args.stats: log_multi(DASHES, STATS_HEADER, logger=stats_logger) @@ -845,7 +865,7 @@ class Archiver: if args.stats: log_multi(stats.summary.format(label='Deleted data:', stats=stats), DASHES, logger=stats_logger) - if not args.forced and self.exit_code: + if args.forced == 0 and self.exit_code: break if args.stats: stats_logger.info(str(cache)) @@ -2383,8 +2403,9 @@ class Archiver: action='store_true', default=False, help='delete only the local cache for the given repository') subparser.add_argument('--force', dest='forced', - action='store_true', default=False, - help='force deletion of corrupted archives') + action='count', default=0, + help='force deletion of corrupted archives, ' + 'use --force --force in case --force does not work.') subparser.add_argument('--save-space', dest='save_space', action='store_true', default=False, help='work slower, but using less space') diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index cce0c92f..d13dee4c 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1166,6 +1166,20 @@ class ArchiverTestCase(ArchiverTestCaseBase): # Make sure the repo is gone self.assertFalse(os.path.exists(self.repository_path)) + def test_delete_double_force(self): + self.cmd('init', '--encryption=none', self.repository_location) + self.create_src_archive('test') + with Repository(self.repository_path, exclusive=True) as repository: + manifest, key = Manifest.load(repository) + archive = Archive(repository, key, manifest, 'test') + id = archive.metadata.items[0] + repository.put(id, b'corrupted items metadata stream chunk') + repository.commit() + self.cmd('delete', '--force', '--force', self.repository_location + '::test') + self.cmd('check', '--repair', self.repository_location) + output = self.cmd('list', self.repository_location) + self.assert_not_in('test', output) + def test_corrupted_repository(self): self.cmd('init', '--encryption=repokey', self.repository_location) self.create_src_archive('test') From c8ec698d739789b29b1c5ea53f5d5943476cd900 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 24 Feb 2017 04:22:12 +0100 Subject: [PATCH 0681/1387] Location: accept //servername/share/path --- src/borg/helpers.py | 13 ++++++++++--- src/borg/testsuite/helpers.py | 5 +++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 89b557e5..5d05b3ce 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1004,10 +1004,17 @@ class Location: # path must not contain :: (it ends at :: or string end), but may contain single colons. # to avoid ambiguities with other regexes, it must also not start with ":" nor with "//" nor with "ssh://". - path_re = r""" + scp_path_re = r""" (?!(:|//|ssh://)) # not starting with ":" or // or ssh:// (?P([^:]|(:(?!:)))+) # any chars, but no "::" """ + + # file_path must not contain :: (it ends at :: or string end), but may contain single colons. + # it must start with a / and that slash is part of the path. + file_path_re = r""" + (?P(([^/]*)/([^:]|(:(?!:)))+)) # start opt. servername, then /, then any chars, but no "::" + """ + # abs_path must not contain :: (it ends at :: or string end), but may contain single colons. # it must start with a / and that slash is part of the path. abs_path_re = r""" @@ -1032,7 +1039,7 @@ class Location: file_re = re.compile(r""" (?Pfile):// # file:// - """ + path_re + optional_archive_re, re.VERBOSE) # path or path::archive + """ + file_path_re + optional_archive_re, re.VERBOSE) # servername/path, path or path::archive # note: scp_re is also use for local pathes scp_re = re.compile(r""" @@ -1040,7 +1047,7 @@ class Location: """ + optional_user_re + r""" # user@ (optional) (?P[^:/]+): # host: (don't match / in host to disambiguate from file:) )? # user@host: part is optional - """ + path_re + optional_archive_re, re.VERBOSE) # path with optional archive + """ + scp_path_re + optional_archive_re, re.VERBOSE) # path with optional archive # get the repo from BORG_REPO env and the optional archive from param. # if the syntax requires giving REPOSITORY (see "borg mount"), diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 8dca6a39..57bf0175 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -60,6 +60,11 @@ class TestLocationWithoutEnv: assert repr(Location('user@host:/some/path')) == \ "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)" + def test_smb(self, monkeypatch): + monkeypatch.delenv('BORG_REPO', raising=False) + assert repr(Location('file:////server/share/path::archive')) == \ + "Location(proto='file', user=None, host=None, port=None, path='//server/share/path', archive='archive')" + def test_folder(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) assert repr(Location('path::archive')) == \ From f7d28e76a006b52d091fd6815cf76a45336998c5 Mon Sep 17 00:00:00 2001 From: kmq Date: Sun, 26 Feb 2017 13:47:26 +0200 Subject: [PATCH 0682/1387] document snapshot usage #2178 --- docs/faq.rst | 2 +- src/borg/archiver.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index 5c8cd775..94b78426 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -429,7 +429,7 @@ BORG_FILES_CACHE_TTL to at least 26 (or maybe even a small multiple of that), it would be much faster. Another possible reason is that files don't always have the same path, for -example if you mount a filesystem without stable mount points for each backup. +example if you mount a filesystem without stable mount points for each backup or if you are running the backup from a filesystem snapshot whose name is not stable. If the directory where you mount a filesystem is different every time, |project_name| assume they are different files. diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 8ecba523..d2707432 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2122,6 +2122,11 @@ class Archiver: potentially decreases reliability of change detection, while avoiding always reading all files on these file systems. + The mount points of filesystems or filesystem snapshots should be the same for every + creation of a new archive to ensure fast operation. This is because the file cache that + is used to determine changed files quickly uses absolute filenames. + If this is not possible, consider creating a bind mount to a stable location. + See the output of the "borg help patterns" command for more help on exclude patterns. See the output of the "borg help placeholders" command for more help on placeholders. From 70c11976bcb1ae068398f579786b5754e1ecee95 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 12 Nov 2016 13:32:57 +0100 Subject: [PATCH 0683/1387] Add --log-json option for structured logging output --- src/borg/archive.py | 42 +++++++++++++++++++++++++++--------------- src/borg/archiver.py | 20 ++++++++++++++++---- src/borg/helpers.py | 12 +++++++++++- src/borg/logger.py | 37 +++++++++++++++++++++++++++++++++++-- 4 files changed, 89 insertions(+), 22 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 20746aa0..1ac2133e 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1,4 +1,5 @@ import errno +import json import os import socket import stat @@ -49,7 +50,8 @@ flags_noatime = flags_normal | getattr(os, 'O_NOATIME', 0) class Statistics: - def __init__(self): + def __init__(self, output_json=False): + self.output_json = output_json self.osize = self.csize = self.usize = self.nfiles = 0 self.last_progress = 0 # timestamp when last progress was shown @@ -92,19 +94,29 @@ class Statistics: now = time.monotonic() if dt is None or now - self.last_progress > dt: self.last_progress = now - columns, lines = get_terminal_size() - if not final: - msg = '{0.osize_fmt} O {0.csize_fmt} C {0.usize_fmt} D {0.nfiles} N '.format(self) - path = remove_surrogates(item.path) if item else '' - space = columns - swidth(msg) - if space < 12: - msg = '' - space = columns - swidth(msg) - if space >= 8: - msg += ellipsis_truncate(path, space) + if self.output_json: + data = self.as_dict() + data.update({ + 'type': 'archive_progress', + 'path': remove_surrogates(item.path if item else ''), + }) + msg = json.dumps(data) + end = '\n' else: - msg = ' ' * columns - print(msg, file=stream or sys.stderr, end="\r", flush=True) + columns, lines = get_terminal_size() + if not final: + msg = '{0.osize_fmt} O {0.csize_fmt} C {0.usize_fmt} D {0.nfiles} N '.format(self) + path = remove_surrogates(item.path) if item else '' + space = columns - swidth(msg) + if space < 12: + msg = '' + space = columns - swidth(msg) + if space >= 8: + msg += ellipsis_truncate(path, space) + else: + msg = ' ' * columns + end = '\r' + print(msg, end=end, file=stream or sys.stderr, flush=True) def is_special(mode): @@ -264,14 +276,14 @@ class Archive: def __init__(self, repository, key, manifest, name, cache=None, create=False, checkpoint_interval=300, numeric_owner=False, noatime=False, noctime=False, progress=False, chunker_params=CHUNKER_PARAMS, start=None, start_monotonic=None, end=None, compression=None, compression_files=None, - consider_part_files=False): + consider_part_files=False, log_json=False): self.cwd = os.getcwd() self.key = key self.repository = repository self.cache = cache self.manifest = manifest self.hard_links = {} - self.stats = Statistics() + self.stats = Statistics(output_json=log_json) self.show_progress = progress self.name = name self.checkpoint_interval = checkpoint_interval diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 8ecba523..aa8d37bf 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -121,7 +121,7 @@ def with_archive(method): def wrapper(self, args, repository, key, manifest, **kwargs): archive = Archive(repository, key, manifest, args.location.archive, numeric_owner=getattr(args, 'numeric_owner', False), cache=kwargs.get('cache'), - consider_part_files=args.consider_part_files) + consider_part_files=args.consider_part_files, log_json=args.log_json) return method(self, args, repository=repository, manifest=manifest, key=key, archive=archive, **kwargs) return wrapper @@ -145,7 +145,14 @@ class Archiver: def print_file_status(self, status, path): if self.output_list and (self.output_filter is None or status in self.output_filter): - logging.getLogger('borg.output.list').info("%1s %s", status, remove_surrogates(path)) + if self.log_json: + print(json.dumps({ + 'type': 'file_status', + 'status': status, + 'path': remove_surrogates(path), + }), file=sys.stderr) + else: + logging.getLogger('borg.output.list').info("%1s %s", status, remove_surrogates(path)) @staticmethod def compare_chunk_contents(chunks1, chunks2): @@ -395,7 +402,8 @@ class Archiver: numeric_owner=args.numeric_owner, noatime=args.noatime, noctime=args.noctime, progress=args.progress, chunker_params=args.chunker_params, start=t0, start_monotonic=t0_monotonic, - compression=args.compression, compression_files=args.compression_files) + compression=args.compression, compression_files=args.compression_files, + log_json=args.log_json) create_inner(archive, cache) else: create_inner(None, None) @@ -1776,6 +1784,8 @@ class Archiver: action='append', metavar='TOPIC', default=[], help='enable TOPIC debugging (can be specified multiple times). ' 'The logger path is borg.debug. if TOPIC is not fully qualified.') + common_group.add_argument('--log-json', dest='log_json', action='store_true', + help='Output one JSON object per log line instead of formatted text.') common_group.add_argument('--lock-wait', dest='lock_wait', type=int, metavar='N', default=1, help='wait for the lock, but max. N seconds (default: %(default)d).') common_group.add_argument('--show-version', dest='show_version', action='store_true', default=False, @@ -3176,7 +3186,9 @@ class Archiver: self.lock_wait = args.lock_wait # This works around http://bugs.python.org/issue9351 func = getattr(args, 'func', None) or getattr(args, 'fallback_func') - setup_logging(level=args.log_level, is_serve=func == self.do_serve) # do not use loggers before this! + # do not use loggers before this! + setup_logging(level=args.log_level, is_serve=func == self.do_serve, json=args.log_json) + self.log_json = args.log_json self._setup_implied_logging(vars(args)) self._setup_topic_debugging(args) if args.show_version: diff --git a/src/borg/helpers.py b/src/borg/helpers.py index f3f97f24..0e6ecbb4 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1388,7 +1388,17 @@ class ProgressIndicatorBase: if not self.logger.handlers: self.handler = logging.StreamHandler(stream=sys.stderr) self.handler.setLevel(logging.INFO) - self.handler.terminator = '\r' + logger = logging.getLogger('borg') + # Some special attributes on the borg logger, created by setup_logging + # But also be able to work without that + try: + formatter = logger.formatter + terminator = '\n' if logger.json else '\r' + except AttributeError: + terminator = '\r' + else: + self.handler.setFormatter(formatter) + self.handler.terminator = terminator self.logger.addHandler(self.handler) if self.logger.level == logging.NOTSET: diff --git a/src/borg/logger.py b/src/borg/logger.py index 20510bb2..8bf0098c 100644 --- a/src/borg/logger.py +++ b/src/borg/logger.py @@ -31,6 +31,7 @@ The way to use this is as follows: """ import inspect +import json import logging import logging.config import logging.handlers # needed for handlers defined there being configurable in logging.conf file @@ -52,7 +53,7 @@ def _log_warning(message, category, filename, lineno, file=None, line=None): logger.warning(msg) -def setup_logging(stream=None, conf_fname=None, env_var='BORG_LOGGING_CONF', level='info', is_serve=False): +def setup_logging(stream=None, conf_fname=None, env_var='BORG_LOGGING_CONF', level='info', is_serve=False, json=False): """setup logging module according to the arguments provided if conf_fname is given (or the config file name can be determined via @@ -91,7 +92,11 @@ def setup_logging(stream=None, conf_fname=None, env_var='BORG_LOGGING_CONF', lev fmt = '$LOG %(levelname)s %(name)s Remote: %(message)s' else: fmt = '%(message)s' - handler.setFormatter(logging.Formatter(fmt)) + formatter = JsonFormatter(fmt) if json else logging.Formatter(fmt) + handler.setFormatter(formatter) + borg_logger = logging.getLogger('borg') + borg_logger.formatter = formatter + borg_logger.json = json logger.addHandler(handler) logger.setLevel(level.upper()) configured = True @@ -181,3 +186,31 @@ def create_logger(name=None): return self.__logger.critical(*args, **kw) return LazyLogger(name) + + +class JsonFormatter(logging.Formatter): + RECORD_ATTRIBUTES = ( + 'created', + 'levelname', + 'name', + 'message', + ) + + # Other attributes that are not very useful but do exist: + # processName, process, relativeCreated, stack_info, thread, threadName + # msg == message + # *args* are the unformatted arguments passed to the logger function, not useful now, + # become useful if sanitized properly (must be JSON serializable) in the code + + # fixed message IDs are assigned. + # exc_info, exc_text are generally uninteresting because the message will have that + + def format(self, record): + super().format(record) + data = { + 'type': 'log_message', + } + for attr in self.RECORD_ATTRIBUTES: + value = getattr(record, attr, None) + if value: + data[attr] = value + return json.dumps(data) From 9ed8e4b7a9a62b056608fc05a7f0988372145da5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 26 Feb 2017 01:20:03 +0100 Subject: [PATCH 0684/1387] document JSON --- docs/internals.rst | 1 + docs/internals/frontends.rst | 159 +++++++++++++++++++++++++++++++++++ docs/usage.rst | 3 +- 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 docs/internals/frontends.rst diff --git a/docs/internals.rst b/docs/internals.rst index 658f10ba..db5408ad 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -31,3 +31,4 @@ hash-table of all chunks that already exist. internals/security internals/data-structures + internals/frontends diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst new file mode 100644 index 00000000..5d17d2e9 --- /dev/null +++ b/docs/internals/frontends.rst @@ -0,0 +1,159 @@ +.. include:: ../global.rst.inc +.. highlight:: none + +.. _json_output: + +All about JSON: How to develop frontends +======================================== + +Borg does not have a public API on the Python level. That does not keep you from writing :code:`import borg`, +but does mean that there are no release-to-release guarantees on what you might find in that package, not +even for point releases (1.1.x), and there is no documentation beyond the code and the internals documents. + +Borg does on the other hand provide an API on a command-line level. In other words, a frontend should to +(for example) create a backup archive just invoke :ref:`borg_create`. + +Logging +------- + +Especially for graphical frontends it is important to be able to convey and reformat progress information +in meaningful ways. The ``--log-json`` option turns the stderr stream of Borg into a stream of JSON lines, +where each line is a JSON object. The *type* key of the object determines its other contents. + +Since JSON can only encode text, any string representing a file system path may miss non-text parts. + +The following types are in use: + +archive_progress + Output during operations creating archives (:ref:`borg_create` and :ref:`borg_recreate`). + The following keys exist, each represents the current progress. + + original_size + Original size of data processed so far (before compression and deduplication) + compressed_size + Compressed size + deduplicated_size + Deduplicated size + nfiles + Number of (regular) files processed so far + path + Current path + +file_status + This is only output by :ref:`borg_create` and :ref:`borg_recreate` if ``--list`` is specified. The usual + rules for the file listing applies, including the ``--filter`` option. + + status + Single-character status as for regular list output + path + Path of the file system object + +log_message + Any regular log output invokes this type. Regular log options and filtering applies to these as well. + + created + Unix timestamp (float) + levelname + Upper-case log level name (also called severity). Defined levels are: DEBUG, INFO, WARNING, CRITICAL + name + Name of the emitting entity + message + Formatted log message + +Standard output +--------------- + +*stdout* is different and more command-dependent. Commands like :ref:`borg_info`, :ref:`borg_create` +and :ref:`borg_list` implement a ``--json`` option which turns their regular output into a single JSON object. + +Dates are formatted according to ISO-8601 with the strftime format string '%a, %Y-%m-%d %H:%M:%S', +e.g. *Sat, 2016-02-25 23:50:06*. + +The root object at least contains a *repository* key with an object containing: + +id + The ID of the repository, normally 64 hex characters +location + Canonicalized repository path, thus this may be different from what is specified on the command line +last_modified + Date when the repository was last modified by the Borg client + +The *encryption* key, if present, contains: + +mode + Textual encryption mode name (same as :ref:`borg_init` ``--encryption`` names) +keyfile + Path to the local key file used for access. Depending on *mode* this key may be absent. + +The *cache* key, if present, contains: + +path + Path to the local repository cache +stats + Object containing cache stats: + + total_chunks + Number of chunks + total_unique_chunks + Number of unique chunks + total_size + Total uncompressed size of all chunks multiplied with their reference counts + total_csize + Total compressed and encrypted size of all chunks multiplied with their reference counts + unique_size + Uncompressed size of all chunks + unique_csize + Compressed and encrypted size of all chunks + +.. rubric:: Archive formats + +:ref:`borg_info` uses an extended format for archives, which is more expensive to retrieve, while +:ref:`borg_list` uses a simpler format that is faster to retrieve. Either return archives in an +array under the *archives* key, while :ref:`borg_create` returns a single archive object under the +*archive* key. + +Both formats contain a *name* key with the archive name, and the *id* key with the hexadecimal archive ID. + + info and create further have: + +start + Start timestamp +end + End timestamp +duration + Duration in seconds between start and end in seconds (float) +stats + Archive statistics (freshly calculated, this is what makes "info" more expensive) + + original_size + Size of files and metadata before compression + compressed_size + Size after compression + deduplicated_size + Deduplicated size (against the current repository, not when the archive was created) + nfiles + Number of regular files in the archive +limits + Object describing the utilization of Borg limits + + max_archive_size + Float between 0 and 1 describing how large this archive is relative to the maximum size allowed by Borg +command_line + Array of strings of the command line that created the archive + + The note about paths from above applies here as well. + +:ref:`borg_info` further has: + +hostname + Hostname of the creating host +username + Name of the creating user +comment + Archive comment, if any + +.. rubric:: File listings + +Listing the contents of an archive can produce *a lot* of JSON. Each item (file, directory, ...) is described +by one object in the *files* array of the :ref:`borg_list` output. Refer to the *borg list* documentation for +the available keys and their meaning. diff --git a/docs/usage.rst b/docs/usage.rst index d11aff60..d6cc35e1 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -15,7 +15,8 @@ General .. include:: usage_general.rst.inc In case you are interested in more details (like formulas), please see -:ref:`internals`. +:ref:`internals`. For details on the available JSON output, refer to +:ref:`json_output`. Common options ++++++++++++++ From 92e5db0c2e7abd872b82611722780c25eea8642f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Feb 2017 18:38:29 +0100 Subject: [PATCH 0685/1387] use lz4 compression by default, fixes #2179 not for create_src_archive() though as this triggers a bug that has to get fixed outside this PR first. --- src/borg/archiver.py | 8 ++++---- src/borg/testsuite/archiver.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index b41df11a..250e9123 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1629,17 +1629,17 @@ class Archiver: borg create /path/to/repo::{hostname}-{now:%Y-%m-%d_%H:%M:%S} ... borg prune --prefix '{hostname}-' ...\n\n''') helptext['compression'] = textwrap.dedent(''' - Compression is off by default, if you want some, you have to specify what you want. + Compression is lz4 by default. If you want something else, you have to specify what you want. Valid compression specifiers are: none - Do not compress. (default) + Do not compress. lz4 - Use lz4 compression. High speed, low compression. + Use lz4 compression. High speed, low compression. (default) zlib[,L] @@ -2276,7 +2276,7 @@ class Archiver: help='specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, ' 'HASH_MASK_BITS, HASH_WINDOW_SIZE). default: %d,%d,%d,%d' % CHUNKER_PARAMS) archive_group.add_argument('-C', '--compression', dest='compression', - type=CompressionSpec, default=dict(name='none'), metavar='COMPRESSION', + type=CompressionSpec, default=dict(name='lz4'), metavar='COMPRESSION', help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') archive_group.add_argument('--compression-from', dest='compression_files', diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index ca0f4102..b42a1a9b 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -262,7 +262,7 @@ class ArchiverTestCaseBase(BaseTestCase): return output def create_src_archive(self, name): - self.cmd('create', self.repository_location + '::' + name, src_dir) + self.cmd('create', '--compression=none', self.repository_location + '::' + name, src_dir) def open_archive(self, name): repository = Repository(self.repository_path, exclusive=True) From 2ad5f903fe0755e4141a9a676e31340c0dc6600b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 24 Feb 2017 01:45:14 +0100 Subject: [PATCH 0686/1387] add test for borg delete --force --- src/borg/testsuite/archiver.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 03f4a6c9..b2685c0d 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1193,6 +1193,23 @@ class ArchiverTestCase(ArchiverTestCaseBase): # Make sure the repo is gone self.assertFalse(os.path.exists(self.repository_path)) + def test_delete_force(self): + self.cmd('init', '--encryption=none', self.repository_location) + self.create_src_archive('test') + with Repository(self.repository_path, exclusive=True) as repository: + manifest, key = Manifest.load(repository) + archive = Archive(repository, key, manifest, 'test') + for item in archive.iter_items(): + if 'chunks' in item: + first_chunk_id = item.chunks[0].id + repository.delete(first_chunk_id) + repository.commit() + break + self.cmd('delete', '--force', self.repository_location + '::test') + self.cmd('check', '--repair', self.repository_location) + output = self.cmd('list', self.repository_location) + self.assert_not_in('test', output) + def test_delete_double_force(self): self.cmd('init', '--encryption=none', self.repository_location) self.create_src_archive('test') From 757921dbdc67759b5a68d2580328cb52ce29c2be Mon Sep 17 00:00:00 2001 From: kmq Date: Sun, 26 Feb 2017 20:33:15 +0200 Subject: [PATCH 0687/1387] Document relative path usage #1868 --- docs/usage.rst | 5 +++++ src/borg/archiver.py | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index d11aff60..dfd77b56 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -108,6 +108,11 @@ Examples # As above, but add nanoseconds $ borg create /path/to/repo::{hostname}-{user}-{now:%Y-%m-%dT%H:%M:%S.%f} ~ + # Backing up relative paths by moving into the correct directory first + $ cd /home/user/Documents + # The root directory of the archive will be "projectA" + $ borg create /path/to/repo::daily-projectA-{now:%Y-%m-%d} projectA + .. include:: usage/extract.rst.inc diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f81c583c..238ba598 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2133,9 +2133,12 @@ class Archiver: create_epilog = process_epilog(""" This command creates a backup archive containing all files found while recursively - traversing all paths specified. When giving '-' as path, borg will read data - from standard input and create a file 'stdin' in the created archive from that - data. + traversing all paths specified. Paths are added to the archive as they are given, + that means if relative paths are desired, the command has to be run from the correct + directory. + + When giving '-' as path, borg will read data from standard input and create a + file 'stdin' in the created archive from that data. The archive will consume almost no disk space for files or parts of files that have already been stored in other archives. From 332f7898bc7a05fa5ddcd0b9c99e2dac759e0a0d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 26 Feb 2017 21:12:23 +0100 Subject: [PATCH 0688/1387] update docs for create -C default change --- docs/faq.rst | 20 ++++++++++++-------- docs/quickstart.rst | 8 ++++---- docs/usage.rst | 2 +- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 94b78426..a56c87c3 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -108,19 +108,23 @@ Are there other known limitations? An easy workaround is to create multiple archives with less items each. See also the :ref:`archive_limitation` and :issue:`1452`. -Why is my backup bigger than with attic? Why doesn't |project_name| do compression by default? ----------------------------------------------------------------------------------------------- +Why is my backup bigger than with attic? +---------------------------------------- Attic was rather unflexible when it comes to compression, it always compressed using zlib level 6 (no way to switch compression off or adjust the level or algorithm). -|project_name| offers a lot of different compression algorithms and -levels. Which of them is the best for you pretty much depends on your -use case, your data, your hardware -- so you need to do an informed -decision about whether you want to use compression, which algorithm -and which level you want to use. This is why compression defaults to -none. +The default in Borg is lz4, which is fast enough to not use significant CPU time +in most cases, but can only achieve modest compression. It still compresses +easily compressed data fairly well. + +zlib compression with all levels (1-9) as well as LZMA (1-6) are available +as well, for cases where they are worth it. + +Which choice is the best option depends on a number of factors, like +bandwidth to the repository, how well the data compresses, available CPU +power and so on. If a backup stops mid-way, does the already-backed-up data stay there? ---------------------------------------------------------------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e1e24e84..52847168 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -118,12 +118,12 @@ sudoers(5), or try ``sudo BORG_PASSPHRASE='yourphrase' borg`` syntax. Backup compression ------------------ -Default is no compression, but we support different methods with high speed -or high compression: +The default is lz4 (very fast, but low compression ratio), but other methods are +supported for different situations. -If you have a fast repo storage and you want some compression: :: +If you have a fast repo storage and you want minimum CPU usage, no compression:: - $ borg create --compression lz4 /path/to/repo::arch ~ + $ borg create --compression none /path/to/repo::arch ~ If you have a less fast repo storage and you want a bit more compression (N=0..9, 0 means no compression, 9 means high compression): :: diff --git a/docs/usage.rst b/docs/usage.rst index d11aff60..8bb85423 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -70,7 +70,7 @@ Examples --exclude 'sh:/home/*/.thumbnails' # Backup the root filesystem into an archive named "root-YYYY-MM-DD" - # use zlib compression (good, but slow) - default is no compression + # use zlib compression (good, but slow) - default is lz4 (fast, low compression ratio) $ borg create -C zlib,6 /path/to/repo::root-{now:%Y-%m-%d} / --one-file-system # Backup a remote host locally ("pull" style) using sshfs From 0721cb1ede4bc538ccf08d1bacd6ebd4b062a596 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 27 Feb 2017 15:30:55 +0100 Subject: [PATCH 0689/1387] files cache: update inode number, fixes #2226 --- src/borg/cache.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index f1a8ef85..a17a5343 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -575,7 +575,15 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" entry = FileCacheEntry(*msgpack.unpackb(entry)) if (entry.size == st.st_size and entry.mtime == st.st_mtime_ns and (ignore_inode or entry.inode == st.st_ino)): - self.files[path_hash] = msgpack.packb(entry._replace(age=0)) + # we ignored the inode number in the comparison above or it is still same. + # if it is still the same, replacing it in the tuple doesn't change it. + # if we ignored it, a reason for doing that is that files were moved to a new + # disk / new fs (so a one-time change of inode number is expected) and we wanted + # to avoid everything getting chunked again. to be able to re-enable the inode + # number comparison in a future backup run (and avoid chunking everything + # again at that time), we need to update the inode number in the cache with what + # we see in the filesystem. + self.files[path_hash] = msgpack.packb(entry._replace(inode=st.st_ino, age=0)) return entry.chunk_ids else: return None From 6288c9f7511607842e79ef08ede821f91e9e5f74 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 27 Feb 2017 20:30:20 +0100 Subject: [PATCH 0690/1387] enhance JSON progress information separate output types, extra information --- docs/internals/frontends.rst | 27 +++++++++++++++ src/borg/helpers.py | 64 +++++++++++++++++++++++++++--------- 2 files changed, 76 insertions(+), 15 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 5d17d2e9..b6bc18dc 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -39,6 +39,33 @@ archive_progress path Current path +progress_message + A message-based progress information with no concrete progress information, just a message + saying what is currently worked on. + + operation + integer ID of the operation + finished + boolean indicating whether the operation has finished, only the last object for an *operation* + can have this property set to *true*. + message + current progress message (may be empty/absent) + +progress_percent + Absolute progress display with defined end/total and current value. + + operation + integer ID of the operation + finished + boolean indicating whether the operation has finished, only the last object for an *operation* + can have this property set to *true*. + message + A formatted progress message, this will include the percentage and perhaps other information + current + Current value (always less-or-equal to *total*) + total + Total value + file_status This is only output by :ref:`borg_create` and :ref:`borg_recreate` if ``--list`` is specified. The usual rules for the file listing applies, including the ``--filter`` option. diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 8f1e9cbc..7f2a4e67 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1377,10 +1377,20 @@ def ellipsis_truncate(msg, space): class ProgressIndicatorBase: LOGGER = 'borg.output.progress' + JSON_TYPE = None + json = False + + operation_id_counter = 0 + + @classmethod + def operation_id(cls): + cls.operation_id_counter += 1 + return cls.operation_id_counter def __init__(self): self.handler = None self.logger = logging.getLogger(self.LOGGER) + self.id = self.operation_id() # If there are no handlers, set one up explicitly because the # terminator and propagation needs to be set. If there are, @@ -1394,6 +1404,7 @@ class ProgressIndicatorBase: try: formatter = logger.formatter terminator = '\n' if logger.json else '\r' + self.json = logger.json except AttributeError: terminator = '\r' else: @@ -1404,12 +1415,30 @@ class ProgressIndicatorBase: if self.logger.level == logging.NOTSET: self.logger.setLevel(logging.WARN) self.logger.propagate = False + self.emit = self.logger.getEffectiveLevel() == logging.INFO def __del__(self): if self.handler is not None: self.logger.removeHandler(self.handler) self.handler.close() + def output_json(self, *, finished=False, **kwargs): + assert self.json + if not self.emit: + return + print(json.dumps(dict( + operation=self.id, + type=self.JSON_TYPE, + finished=finished, + **kwargs, + )), file=sys.stderr) + + def finish(self): + if self.json: + self.output_json(finished=True) + else: + self.output('') + def justify_to_terminal_size(message): terminal_space = get_terminal_size(fallback=(-1, -1))[0] @@ -1420,14 +1449,18 @@ def justify_to_terminal_size(message): class ProgressIndicatorMessage(ProgressIndicatorBase): - def output(self, msg): - self.logger.info(justify_to_terminal_size(msg)) + JSON_TYPE = 'progress_message' - def finish(self): - self.output('') + def output(self, msg): + if self.json: + self.output_json(message=msg) + else: + self.logger.info(justify_to_terminal_size(msg)) class ProgressIndicatorPercent(ProgressIndicatorBase): + JSON_TYPE = 'progress_percent' + def __init__(self, total=0, step=5, start=0, msg="%3.0f%%"): """ Percentage-based progress indicator @@ -1466,22 +1499,23 @@ class ProgressIndicatorPercent(ProgressIndicatorBase): if pct is not None: # truncate the last argument, if no space is available if info is not None: - # no need to truncate if we're not outputing to a terminal - terminal_space = get_terminal_size(fallback=(-1, -1))[0] - if terminal_space != -1: - space = terminal_space - len(self.msg % tuple([pct] + info[:-1] + [''])) - info[-1] = ellipsis_truncate(info[-1], space) + if not self.json: + # no need to truncate if we're not outputing to a terminal + terminal_space = get_terminal_size(fallback=(-1, -1))[0] + if terminal_space != -1: + space = terminal_space - len(self.msg % tuple([pct] + info[:-1] + [''])) + info[-1] = ellipsis_truncate(info[-1], space) return self.output(self.msg % tuple([pct] + info), justify=False) return self.output(self.msg % pct) def output(self, message, justify=True): - if justify: - message = justify_to_terminal_size(message) - self.logger.info(message) - - def finish(self): - self.output('') + if self.json: + self.output_json(message=message, current=self.counter, total=self.total) + else: + if justify: + message = justify_to_terminal_size(message) + self.logger.info(message) class ProgressIndicatorEndless: From d5515b69529720d1686ff298ad340e3ab4ce5795 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 27 Feb 2017 20:38:02 +0100 Subject: [PATCH 0691/1387] add msgid to progress output --- docs/internals/frontends.rst | 12 ++++++++---- src/borg/archive.py | 5 +++-- src/borg/archiver.py | 5 +++-- src/borg/cache.py | 7 ++++--- src/borg/helpers.py | 17 ++++++++++------- src/borg/repository.py | 9 ++++++--- 6 files changed, 34 insertions(+), 21 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index b6bc18dc..e11a1a3f 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -44,7 +44,9 @@ progress_message saying what is currently worked on. operation - integer ID of the operation + unique, opaque integer ID of the operation + msgid + Message ID of the operation (may be *none*) finished boolean indicating whether the operation has finished, only the last object for an *operation* can have this property set to *true*. @@ -52,10 +54,12 @@ progress_message current progress message (may be empty/absent) progress_percent - Absolute progress display with defined end/total and current value. + Absolute progress information with defined end/total and current value. operation - integer ID of the operation + unique, opaque integer ID of the operation + msgid + Message ID of the operation (may be *none*) finished boolean indicating whether the operation has finished, only the last object for an *operation* can have this property set to *true*. @@ -81,7 +85,7 @@ log_message created Unix timestamp (float) levelname - Upper-case log level name (also called severity). Defined levels are: DEBUG, INFO, WARNING, CRITICAL + Upper-case log level name (also called severity). Defined levels are: DEBUG, INFO, WARNING, ERROR, CRITICAL name Name of the emitting entity message diff --git a/src/borg/archive.py b/src/borg/archive.py index 1df1401d..6fc0739e 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -734,7 +734,7 @@ Utilization of max. archive size: {csize_max:.0%} try: unpacker = msgpack.Unpacker(use_list=False) items_ids = self.metadata.items - pi = ProgressIndicatorPercent(total=len(items_ids), msg="Decrementing references %3.0f%%") + pi = ProgressIndicatorPercent(total=len(items_ids), msg="Decrementing references %3.0f%%", msgid='archive.delete') for (i, (items_id, data)) in enumerate(zip(items_ids, self.repository.get_many(items_ids))): if progress: pi.show(i) @@ -1153,7 +1153,8 @@ class ArchiveChecker: chunks_count_segments = 0 errors = 0 defect_chunks = [] - pi = ProgressIndicatorPercent(total=chunks_count_index, msg="Verifying data %6.2f%%", step=0.01) + pi = ProgressIndicatorPercent(total=chunks_count_index, msg="Verifying data %6.2f%%", step=0.01, + msgid='check.verify_data') marker = None while True: chunk_ids = self.repository.scan(limit=100, marker=marker) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 243e91d2..ad8a286d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -555,7 +555,7 @@ class Archiver: filter = self.build_filter(matcher, peek_and_store_hardlink_masters, strip_components) if progress: - pi = ProgressIndicatorPercent(msg='%5.1f%% Extracting: %s', step=0.1) + pi = ProgressIndicatorPercent(msg='%5.1f%% Extracting: %s', step=0.1, msgid='extract') pi.output('Calculating size') extracted_size = sum(item.get_size(hardlink_masters) for item in archive.iter_items(filter)) pi.total = extracted_size @@ -589,7 +589,8 @@ class Archiver: self.print_warning('%s: %s', remove_surrogates(orig_path), e) if not args.dry_run: - pi = ProgressIndicatorPercent(total=len(dirs), msg='Setting directory permissions %3.0f%%') + pi = ProgressIndicatorPercent(total=len(dirs), msg='Setting directory permissions %3.0f%%', + msgid='extract.permissions') while dirs: pi.show() dir_item = dirs.pop(-1) diff --git a/src/borg/cache.py b/src/borg/cache.py index 504001e7..56e16560 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -320,7 +320,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def begin_txn(self): # Initialize transaction snapshot - pi = ProgressIndicatorMessage() + pi = ProgressIndicatorMessage(msgid='cache.begin_transaction') txn_dir = os.path.join(self.path, 'txn.tmp') os.mkdir(txn_dir) pi.output('Initializing cache transaction: Reading config') @@ -340,7 +340,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if not self.txn_active: return self.security_manager.save(self.manifest, self.key, self) - pi = ProgressIndicatorMessage() + pi = ProgressIndicatorMessage(msgid='cache.commit') if self.files is not None: if self._newest_mtime is None: # was never set because no files were modified/added @@ -468,7 +468,8 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" chunk_idx = None if self.progress: pi = ProgressIndicatorPercent(total=len(archive_ids), step=0.1, - msg='%3.0f%% Syncing chunks cache. Processing archive %s') + msg='%3.0f%% Syncing chunks cache. Processing archive %s', + msgid='cache.sync') for archive_id in archive_ids: archive_name = lookup_name(archive_id) if self.progress: diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 7f2a4e67..b4efd7e7 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1384,13 +1384,15 @@ class ProgressIndicatorBase: @classmethod def operation_id(cls): + """Unique number, can be used by receiving applications to distinguish different operations.""" cls.operation_id_counter += 1 return cls.operation_id_counter - def __init__(self): + def __init__(self, msgid=None): self.handler = None self.logger = logging.getLogger(self.LOGGER) self.id = self.operation_id() + self.msgid = msgid # If there are no handlers, set one up explicitly because the # terminator and propagation needs to be set. If there are, @@ -1426,12 +1428,13 @@ class ProgressIndicatorBase: assert self.json if not self.emit: return - print(json.dumps(dict( + kwargs.update(dict( operation=self.id, + msgid=self.msgid, type=self.JSON_TYPE, - finished=finished, - **kwargs, - )), file=sys.stderr) + finished=finished + )) + print(json.dumps(kwargs), file=sys.stderr) def finish(self): if self.json: @@ -1461,7 +1464,7 @@ class ProgressIndicatorMessage(ProgressIndicatorBase): class ProgressIndicatorPercent(ProgressIndicatorBase): JSON_TYPE = 'progress_percent' - def __init__(self, total=0, step=5, start=0, msg="%3.0f%%"): + def __init__(self, total=0, step=5, start=0, msg="%3.0f%%", msgid=None): """ Percentage-based progress indicator @@ -1476,7 +1479,7 @@ class ProgressIndicatorPercent(ProgressIndicatorBase): self.step = step self.msg = msg - super().__init__() + super().__init__(msgid=msgid) def progress(self, current=None, increase=1): if current is not None: diff --git a/src/borg/repository.py b/src/borg/repository.py index 81935f4a..41d865d5 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -483,7 +483,8 @@ class Repository: unused = [] logger.debug('compaction started.') - pi = ProgressIndicatorPercent(total=len(self.compact), msg='Compacting segments %3.0f%%', step=1) + pi = ProgressIndicatorPercent(total=len(self.compact), msg='Compacting segments %3.0f%%', step=1, + msgid='repository.compact_segments') for segment, freeable_space in sorted(self.compact.items()): if not self.io.segment_exists(segment): logger.warning('segment %d not found, but listed in compaction data', segment) @@ -584,7 +585,8 @@ class Repository: self.prepare_txn(index_transaction_id, do_cleanup=False) try: segment_count = sum(1 for _ in self.io.segment_iterator()) - pi = ProgressIndicatorPercent(total=segment_count, msg="Replaying segments %3.0f%%") + pi = ProgressIndicatorPercent(total=segment_count, msg='Replaying segments %3.0f%%', + msgid='repository.replay_segments') for i, (segment, filename) in enumerate(self.io.segment_iterator()): pi.show(i) if index_transaction_id is not None and segment <= index_transaction_id: @@ -694,7 +696,8 @@ class Repository: self.prepare_txn(None) # self.index, self.compact, self.segments all empty now! segment_count = sum(1 for _ in self.io.segment_iterator()) logger.debug('Found %d segments', segment_count) - pi = ProgressIndicatorPercent(total=segment_count, msg="Checking segments %3.1f%%", step=0.1) + pi = ProgressIndicatorPercent(total=segment_count, msg='Checking segments %3.1f%%', step=0.1, + msgid='repository.check') for i, (segment, filename) in enumerate(self.io.segment_iterator()): pi.show(i) if segment > transaction_id: From 9f446aa6d434c1b926968d73e3a2dff119aca95b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 27 Feb 2017 20:51:20 +0100 Subject: [PATCH 0692/1387] pass --log-json to remote Obviously this means that --log-json with remote repos requires 1.1 on the remote end, but if you don't have that, then random "Remote:" lines would break stderr anyway. --- src/borg/logger.py | 2 +- src/borg/remote.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/borg/logger.py b/src/borg/logger.py index 8bf0098c..6d4a4402 100644 --- a/src/borg/logger.py +++ b/src/borg/logger.py @@ -92,7 +92,7 @@ def setup_logging(stream=None, conf_fname=None, env_var='BORG_LOGGING_CONF', lev fmt = '$LOG %(levelname)s %(name)s Remote: %(message)s' else: fmt = '%(message)s' - formatter = JsonFormatter(fmt) if json else logging.Formatter(fmt) + formatter = JsonFormatter(fmt) if json and not is_serve else logging.Formatter(fmt) handler.setFormatter(formatter) borg_logger = logging.getLogger('borg') borg_logger.formatter = formatter diff --git a/src/borg/remote.py b/src/borg/remote.py index 05ba6c8c..7bb9ece0 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -636,6 +636,12 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. opts.append('--critical') else: raise ValueError('log level missing, fix this code') + try: + borg_logger = logging.getLogger('borg') + if borg_logger.json: + opts.append('--log-json') + except AttributeError: + pass env_vars = [] if yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', env_msg=None, prompt=False): env_vars.append('BORG_HOSTNAME_IS_UNIQUE=yes') From fcad0ddab41603b9ef28edd948a01a2860f6a76a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 27 Feb 2017 21:24:56 +0100 Subject: [PATCH 0693/1387] pass msgid for common errors --- docs/internals/frontends.rst | 41 ++++++++++++++++++++++++++++++++++-- src/borg/archiver.py | 9 ++++++-- src/borg/logger.py | 16 ++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index e11a1a3f..85b00134 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -45,7 +45,7 @@ progress_message operation unique, opaque integer ID of the operation - msgid + :ref:`msgid ` Message ID of the operation (may be *none*) finished boolean indicating whether the operation has finished, only the last object for an *operation* @@ -58,7 +58,7 @@ progress_percent operation unique, opaque integer ID of the operation - msgid + :ref:`msgid ` Message ID of the operation (may be *none*) finished boolean indicating whether the operation has finished, only the last object for an *operation* @@ -90,6 +90,8 @@ log_message Name of the emitting entity message Formatted log message + :ref:`msgid ` + Message ID, may be *none* or absent Standard output --------------- @@ -188,3 +190,38 @@ comment Listing the contents of an archive can produce *a lot* of JSON. Each item (file, directory, ...) is described by one object in the *files* array of the :ref:`borg_list` output. Refer to the *borg list* documentation for the available keys and their meaning. + +.. _msgid: + +Message IDs +----------- + +Message IDs are strings that essentially give a log message or operation a name, without actually using the +full text, since texts change more frequently. Message IDs are unambiguous and reduce the need to parse +log messages. + +Assigned message IDs are: + +.. note:: + + This list is incomplete. + +Errors + - Archive.AlreadyExists + - Archive.DoesNotExist + - Archive.IncompatibleFilesystemEncodingError + - IntegrityError + - NoManifestError + - PlaceholderError + +Operations + - cache.begin_transaction + - cache.commit + - cache.sync + - repository.compact_segments + - repository.replay_segments + - repository.check_segments + - check.verify_data + - extract + - extract.permissions + - archive.delete diff --git a/src/borg/archiver.py b/src/borg/archiver.py index ad8a286d..88fe2328 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -3287,7 +3287,7 @@ def main(): # pragma: no cover signal_handler('SIGUSR2', sig_trace_handler), \ signal_handler('SIGINFO', sig_info_handler): archiver = Archiver() - msg = tb = None + msg = msgid = tb = None tb_log_level = logging.ERROR try: args = archiver.get_args(sys.argv, os.environ.get('SSH_ORIGINAL_COMMAND')) @@ -3304,11 +3304,13 @@ def main(): # pragma: no cover exit_code = archiver.run(args) except Error as e: msg = e.get_message() + msgid = type(e).__qualname__ tb_log_level = logging.ERROR if e.traceback else logging.DEBUG tb = "%s\n%s" % (traceback.format_exc(), sysinfo()) exit_code = e.exit_code except RemoteRepository.RPCError as e: important = e.exception_class not in ('LockTimeout', ) + msgid = e.exception_class tb_log_level = logging.ERROR if important else logging.DEBUG if important: msg = e.exception_full @@ -3319,6 +3321,7 @@ def main(): # pragma: no cover exit_code = EXIT_ERROR except Exception: msg = 'Local Exception' + msgid = 'Exception' tb_log_level = logging.ERROR tb = '%s\n%s' % (traceback.format_exc(), sysinfo()) exit_code = EXIT_ERROR @@ -3329,14 +3332,16 @@ def main(): # pragma: no cover exit_code = EXIT_ERROR except SigTerm: msg = 'Received SIGTERM' + msgid = 'Signal.SIGTERM' tb_log_level = logging.DEBUG tb = '%s\n%s' % (traceback.format_exc(), sysinfo()) exit_code = EXIT_ERROR except SigHup: msg = 'Received SIGHUP.' + msgid = 'Signal.SIGHUP' exit_code = EXIT_ERROR if msg: - logger.error(msg) + logger.error(msg, msgid=msgid) if tb: logger.log(tb_log_level, tb) if args.show_rc: diff --git a/src/borg/logger.py b/src/borg/logger.py index 6d4a4402..672c1e89 100644 --- a/src/borg/logger.py +++ b/src/borg/logger.py @@ -165,24 +165,38 @@ def create_logger(name=None): return self.__logger.setLevel(*args, **kw) def log(self, *args, **kw): + if 'msgid' in kw: + kw.setdefault('extra', {})['msgid'] = kw.pop('msgid') return self.__logger.log(*args, **kw) def exception(self, *args, **kw): + if 'msgid' in kw: + kw.setdefault('extra', {})['msgid'] = kw.pop('msgid') return self.__logger.exception(*args, **kw) def debug(self, *args, **kw): + if 'msgid' in kw: + kw.setdefault('extra', {})['msgid'] = kw.pop('msgid') return self.__logger.debug(*args, **kw) def info(self, *args, **kw): + if 'msgid' in kw: + kw.setdefault('extra', {})['msgid'] = kw.pop('msgid') return self.__logger.info(*args, **kw) def warning(self, *args, **kw): + if 'msgid' in kw: + kw.setdefault('extra', {})['msgid'] = kw.pop('msgid') return self.__logger.warning(*args, **kw) def error(self, *args, **kw): + if 'msgid' in kw: + kw.setdefault('extra', {})['msgid'] = kw.pop('msgid') return self.__logger.error(*args, **kw) def critical(self, *args, **kw): + if 'msgid' in kw: + kw.setdefault('extra', {})['msgid'] = kw.pop('msgid') return self.__logger.critical(*args, **kw) return LazyLogger(name) @@ -194,6 +208,8 @@ class JsonFormatter(logging.Formatter): 'levelname', 'name', 'message', + # msgid is an attribute we made up in Borg to expose a non-changing handle for log messages + 'msgid', ) # Other attributes that are not very useful but do exist: From d3271096048afcfefd1afcc28bbbc597e74ab57b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 27 Feb 2017 22:36:09 +0100 Subject: [PATCH 0694/1387] json progress: emit info (current $something) --- docs/internals/frontends.rst | 9 ++++++++- src/borg/helpers.py | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 85b00134..66ea8584 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -22,7 +22,8 @@ where each line is a JSON object. The *type* key of the object determines its ot Since JSON can only encode text, any string representing a file system path may miss non-text parts. -The following types are in use: +The following types are in use. Progress information is governed by the usual rules for progress information, +it is not produced unless ``--progress`` is specified. archive_progress Output during operations creating archives (:ref:`borg_create` and :ref:`borg_recreate`). @@ -67,6 +68,8 @@ progress_percent A formatted progress message, this will include the percentage and perhaps other information current Current value (always less-or-equal to *total*) + info + Array that describes the current item, may be *none*, contents depend on *msgid* total Total value @@ -218,10 +221,14 @@ Operations - cache.begin_transaction - cache.commit - cache.sync + + *info* is one string element, the name of the archive currently synced. - repository.compact_segments - repository.replay_segments - repository.check_segments - check.verify_data - extract + + *info* is one string element, the name of the path currently extracted. - extract.permissions - archive.delete diff --git a/src/borg/helpers.py b/src/borg/helpers.py index b4efd7e7..da6ef776 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1508,13 +1508,13 @@ class ProgressIndicatorPercent(ProgressIndicatorBase): if terminal_space != -1: space = terminal_space - len(self.msg % tuple([pct] + info[:-1] + [''])) info[-1] = ellipsis_truncate(info[-1], space) - return self.output(self.msg % tuple([pct] + info), justify=False) + return self.output(self.msg % tuple([pct] + info), justify=False, info=info) return self.output(self.msg % pct) - def output(self, message, justify=True): + def output(self, message, justify=True, info=None): if self.json: - self.output_json(message=message, current=self.counter, total=self.total) + self.output_json(message=message, current=self.counter, total=self.total, info=info) else: if justify: message = justify_to_terminal_size(message) From 51350953b05c4056c3e1b5f197c04bfefa8b4001 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 27 Feb 2017 22:54:01 +0100 Subject: [PATCH 0695/1387] json: examples --- docs/internals/frontends.rst | 130 ++++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 66ea8584..358f30fa 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -1,5 +1,5 @@ .. include:: ../global.rst.inc -.. highlight:: none +.. highlight:: json .. _json_output: @@ -141,6 +141,31 @@ stats unique_csize Compressed and encrypted size of all chunks +Example *borg info* output:: + + { + "cache": { + "path": "/home/user/.cache/borg/0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23", + "stats": { + "total_chunks": 511533, + "total_csize": 17948017540, + "total_size": 22635749792, + "total_unique_chunks": 54892, + "unique_csize": 1920405405, + "unique_size": 2449675468 + } + }, + "encryption": { + "mode": "repokey" + }, + "repository": { + "id": "0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23", + "last_modified": "Mon, 2017-02-27 21:21:58", + "location": "/home/user/testrepo" + }, + "security_dir": "/home/user/.config/borg/security/0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23" + } + .. rubric:: Archive formats :ref:`borg_info` uses an extended format for archives, which is more expensive to retrieve, while @@ -188,12 +213,115 @@ username comment Archive comment, if any +Example of a simple archive listing (``borg list --last 1 --json``):: + + { + "archives": [ + { + "archive": "2017-02-27T21:21:51", + "barchive": "2017-02-27T21:21:51", + "id": "80cd07219ad725b3c5f665c1dcf119435c4dee1647a560ecac30f8d40221a46a", + "name": "2017-02-27T21:21:51", + "time": "Mon, 2017-02-27 21:21:52" + } + ], + "encryption": { + "mode": "repokey" + }, + "repository": { + "id": "0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23", + "last_modified": "Mon, 2017-02-27 21:21:58", + "location": "/home/user/repository" + } + } + +The same archive with more information (``borg info --last 1 --json``):: + + { + "archives": [ + { + "command_line": [ + "/home/user/.local/bin/borg", + "create", + "/home/user/repository", + "..." + ], + "comment": "", + "duration": 5.641542, + "end": "Mon, 2017-02-27 21:21:58", + "hostname": "host", + "id": "80cd07219ad725b3c5f665c1dcf119435c4dee1647a560ecac30f8d40221a46a", + "limits": { + "max_archive_size": 0.0001330855110409714 + }, + "name": "2017-02-27T21:21:51", + "start": "Mon, 2017-02-27 21:21:52", + "stats": { + "compressed_size": 1880961894, + "deduplicated_size": 2791, + "nfiles": 53669, + "original_size": 2400471280 + }, + "username": "user" + } + ], + "cache": { + "path": "/home/user/.cache/borg/0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23", + "stats": { + "total_chunks": 511533, + "total_csize": 17948017540, + "total_size": 22635749792, + "total_unique_chunks": 54892, + "unique_csize": 1920405405, + "unique_size": 2449675468 + } + }, + "encryption": { + "mode": "repokey" + }, + "repository": { + "id": "0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23", + "last_modified": "Mon, 2017-02-27 21:21:58", + "location": "/home/user/repository" + } + } + .. rubric:: File listings Listing the contents of an archive can produce *a lot* of JSON. Each item (file, directory, ...) is described by one object in the *files* array of the :ref:`borg_list` output. Refer to the *borg list* documentation for the available keys and their meaning. +Example (excerpt):: + + { + "encryption": { + "mode": "repokey" + }, + "repository": { + "id": "0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23", + "last_modified": "Mon, 2017-02-27 21:21:58", + "location": "/home/user/repository" + }, + "files": [ + { + "type": "d", + "mode": "drwxr-xr-x", + "user": "user", + "group": "user", + "uid": 1000, + "gid": 1000, + "path": "linux", + "healthy": true, + "source": "", + "linktarget": "", + "flags": null, + "isomtime": "Sat, 2016-05-07 19:46:01", + "size": 0 + } + ] + } + .. _msgid: Message IDs From 8e1edaf258b96b94a36408ba816e1831be3c75fe Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 28 Feb 2017 01:30:11 +0100 Subject: [PATCH 0696/1387] ArchiveFormatter: add "start" key for compatibility with "info" --- docs/internals/frontends.rst | 10 +++++----- src/borg/helpers.py | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 358f30fa..6409ce6f 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -173,12 +173,11 @@ Example *borg info* output:: array under the *archives* key, while :ref:`borg_create` returns a single archive object under the *archive* key. -Both formats contain a *name* key with the archive name, and the *id* key with the hexadecimal archive ID. +Both formats contain a *name* key with the archive name, the *id* key with the hexadecimal archive ID, +and the *start* key with the start timestamp. - info and create further have: +info and create further have: -start - Start timestamp end End timestamp duration @@ -222,7 +221,8 @@ Example of a simple archive listing (``borg list --last 1 --json``):: "barchive": "2017-02-27T21:21:51", "id": "80cd07219ad725b3c5f665c1dcf119435c4dee1647a560ecac30f8d40221a46a", "name": "2017-02-27T21:21:51", - "time": "Mon, 2017-02-27 21:21:52" + "time": "Mon, 2017-02-27 21:21:52", + "start": "Mon, 2017-02-27 21:21:52" } ], "encryption": { diff --git a/src/borg/helpers.py b/src/borg/helpers.py index da6ef776..9f5de739 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1622,6 +1622,7 @@ class ArchiveFormatter(BaseFormatter): 'archive': remove_surrogates(archive.name), 'id': bin_to_hex(archive.id), 'time': format_time(to_localtime(archive.ts)), + 'start': format_time(to_localtime(archive.ts)), } @staticmethod From 6ee0585b337ee63dab345cc42ad9abe1e75dced3 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 28 Feb 2017 02:04:46 +0100 Subject: [PATCH 0697/1387] extract: fix missing call to ProgressIndicator.finish --- src/borg/archiver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 88fe2328..c1a778a2 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -588,6 +588,9 @@ class Archiver: except BackupOSError as e: self.print_warning('%s: %s', remove_surrogates(orig_path), e) + if pi: + pi.finish() + if not args.dry_run: pi = ProgressIndicatorPercent(total=len(dirs), msg='Setting directory permissions %3.0f%%', msgid='extract.permissions') From 7cce650a38b1a3064c6baf1cb92dd778071dfffc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 28 Feb 2017 11:58:43 +0100 Subject: [PATCH 0698/1387] json docs: rather complete error list --- docs/internals/frontends.rst | 122 ++++++++++++++++++++++++++++++++--- scripts/errorlist.py | 14 ++++ 2 files changed, 127 insertions(+), 9 deletions(-) create mode 100755 scripts/errorlist.py diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 6409ce6f..0553cd00 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -96,6 +96,39 @@ log_message :ref:`msgid ` Message ID, may be *none* or absent +.. rubric:: Examples (reformatted, each object would be on exactly one line) + +:ref:`borg_extract` progress:: + + {"message": "100.0% Extracting: src/borgbackup.egg-info/entry_points.txt", + "current": 13000228, "total": 13004993, "info": ["src/borgbackup.egg-info/entry_points.txt"], + "operation": 1, "msgid": "extract", "type": "progress_percent", "finished": false} + {"message": "100.0% Extracting: src/borgbackup.egg-info/SOURCES.txt", + "current": 13004993, "total": 13004993, "info": ["src/borgbackup.egg-info/SOURCES.txt"], + "operation": 1, "msgid": "extract", "type": "progress_percent", "finished": false} + {"operation": 1, "msgid": "extract", "type": "progress_percent", "finished": true} + +:ref:`borg_create` file listing with progress:: + + {"original_size": 0, "compressed_size": 0, "deduplicated_size": 0, "nfiles": 0, "type": "archive_progress", "path": "src"} + {"type": "file_status", "status": "U", "path": "src/borgbackup.egg-info/entry_points.txt"} + {"type": "file_status", "status": "U", "path": "src/borgbackup.egg-info/SOURCES.txt"} + {"type": "file_status", "status": "d", "path": "src/borgbackup.egg-info"} + {"type": "file_status", "status": "d", "path": "src"} + {"original_size": 13176040, "compressed_size": 11386863, "deduplicated_size": 503, "nfiles": 277, "type": "archive_progress", "path": ""} + +Internal transaction progress:: + + {"message": "Saving files cache", "operation": 2, "msgid": "cache.commit", "type": "progress_message", "finished": false} + {"message": "Saving cache config", "operation": 2, "msgid": "cache.commit", "type": "progress_message", "finished": false} + {"message": "Saving chunks cache", "operation": 2, "msgid": "cache.commit", "type": "progress_message", "finished": false} + {"operation": 2, "msgid": "cache.commit", "type": "progress_message", "finished": true} + +A debug log message:: + + {"message": "35 self tests completed in 0.08 seconds", + "type": "log_message", "created": 1488278449.5575905, "levelname": "DEBUG", "name": "borg.archiver"} + Standard output --------------- @@ -333,17 +366,88 @@ log messages. Assigned message IDs are: -.. note:: - - This list is incomplete. +.. See scripts/errorlist.py; this is slightly edited. Errors - - Archive.AlreadyExists - - Archive.DoesNotExist - - Archive.IncompatibleFilesystemEncodingError - - IntegrityError - - NoManifestError - - PlaceholderError + Archive.AlreadyExists + Archive {} already exists + Archive.DoesNotExist + Archive {} does not exist + Archive.IncompatibleFilesystemEncodingError + Failed to encode filename "{}" into file system encoding "{}". Consider configuring the LANG environment variable. + Cache.CacheInitAbortedError + Cache initialization aborted + Cache.EncryptionMethodMismatch + Repository encryption method changed since last access, refusing to continue + Cache.RepositoryAccessAborted + Repository access aborted + Cache.RepositoryIDNotUnique + Cache is newer than repository - do you have multiple, independently updated repos with same ID? + Cache.RepositoryReplay + Cache is newer than repository - this is either an attack or unsafe (multiple repos with same ID) + Buffer.MemoryLimitExceeded + Requested buffer size {} is above the limit of {}. + ExtensionModuleError + The Borg binary extension modules do not seem to be properly installed + IntegrityError + Data integrity error: {} + NoManifestError + Repository has no manifest. + PlaceholderError + Formatting Error: "{}".format({}): {}({}) + KeyfileInvalidError + Invalid key file for repository {} found in {}. + KeyfileMismatchError + Mismatch between repository {} and key file {}. + KeyfileNotFoundError + No key file for repository {} found in {}. + PassphraseWrong + passphrase supplied in BORG_PASSPHRASE is incorrect + PasswordRetriesExceeded + exceeded the maximum password retries + RepoKeyNotFoundError + No key entry found in the config of repository {}. + UnsupportedManifestError + Unsupported manifest envelope. A newer version is required to access this repository. + UnsupportedPayloadError + Unsupported payload type {}. A newer version is required to access this repository. + NotABorgKeyFile + This file is not a borg key backup, aborting. + RepoIdMismatch + This key backup seems to be for a different backup repository, aborting. + UnencryptedRepo + Keymanagement not available for unencrypted repositories. + UnknownKeyType + Keytype {0} is unknown. + LockError + Failed to acquire the lock {}. + LockErrorT + Failed to acquire the lock {}. + ConnectionClosed + Connection closed by remote host + InvalidRPCMethod + RPC method {} is not valid + PathNotAllowed + Repository path not allowed + RemoteRepository.RPCServerOutdated + Borg server is too old for {}. Required version {} + UnexpectedRPCDataFormatFromClient + Borg {}: Got unexpected RPC data format from client. + UnexpectedRPCDataFormatFromServer + Got unexpected RPC data format from server: + {} + Repository.AlreadyExists + Repository {} already exists. + Repository.CheckNeeded + Inconsistency detected. Please run "borg check {}". + Repository.DoesNotExist + Repository {} does not exist. + Repository.InsufficientFreeSpaceError + Insufficient free space to complete transaction (required: {}, available: {}). + Repository.InvalidRepository + {} is not a valid repository. Check repo config. + Repository.ObjectNotFound + Object with key {} not found in repository {}. Operations - cache.begin_transaction diff --git a/scripts/errorlist.py b/scripts/errorlist.py new file mode 100755 index 00000000..bd33faf4 --- /dev/null +++ b/scripts/errorlist.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +from textwrap import indent + +import borg.archiver +from borg.helpers import Error, ErrorWithTraceback + +classes = Error.__subclasses__() + ErrorWithTraceback.__subclasses__() + +for cls in sorted(classes, key=lambda cls: (cls.__module__, cls.__qualname__)): + if cls is ErrorWithTraceback: + continue + print(' ', cls.__qualname__) + print(indent(cls.__doc__, ' ' * 8)) From 5cd424e4be8e0b44608d6f9f12e485e88ecdf712 Mon Sep 17 00:00:00 2001 From: Alexander 'Leo' Bergolth Date: Tue, 28 Feb 2017 12:01:37 +0100 Subject: [PATCH 0699/1387] --patterns-from was accessing args.roots instead of args.paths add a test case that parses a command containing --patterns-from --- src/borg/helpers.py | 2 +- src/borg/testsuite/archiver.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 8f1e9cbc..2f4429a3 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -407,7 +407,7 @@ class ArgparsePatternFileAction(argparse.Action): self.parse(f, args) def parse(self, fobj, args): - load_pattern_file(fobj, args.roots, args.patterns) + load_pattern_file(fobj, args.paths, args.patterns) class ArgparseExcludeFileAction(ArgparsePatternFileAction): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 6793e0de..42ebfd8f 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -231,6 +231,7 @@ class ArchiverTestCaseBase(BaseTestCase): self.keys_path = os.path.join(self.tmpdir, 'keys') self.cache_path = os.path.join(self.tmpdir, 'cache') self.exclude_file_path = os.path.join(self.tmpdir, 'excludes') + self.patterns_file_path = os.path.join(self.tmpdir, 'patterns') os.environ['BORG_KEYS_DIR'] = self.keys_path os.environ['BORG_CACHE_DIR'] = self.cache_path os.mkdir(self.input_path) @@ -240,6 +241,8 @@ class ArchiverTestCaseBase(BaseTestCase): os.mkdir(self.cache_path) with open(self.exclude_file_path, 'wb') as fd: fd.write(b'input/file2\n# A comment line, then a blank line\n\n') + with open(self.patterns_file_path, 'wb') as fd: + fd.write(b'+input/file_important\n- input/file*\n# A comment line, then a blank line\n\n') self._old_wd = os.getcwd() os.chdir(self.tmpdir) @@ -907,9 +910,23 @@ class ArchiverTestCase(ArchiverTestCaseBase): '--pattern=+input/file_important', '--pattern=-input/file*', self.repository_location + '::test', 'input') self.assert_in("A input/file_important", output) + self.assert_in('x input/file1', output) + self.assert_in('x input/file2', output) + + def test_create_pattern_file(self): + """test file patterns during create""" + self.cmd('init', '--encryption=repokey', self.repository_location) + self.create_regular_file('file1', size=1024 * 80) + self.create_regular_file('file2', size=1024 * 80) + self.create_regular_file('otherfile', size=1024 * 80) + self.create_regular_file('file_important', size=1024 * 80) + output = self.cmd('create', '-v', '--list', + '--pattern=-input/otherfile', '--patterns-from=' + self.patterns_file_path, + self.repository_location + '::test', 'input') self.assert_in("A input/file_important", output) self.assert_in('x input/file1', output) self.assert_in('x input/file2', output) + self.assert_in('x input/otherfile', output) def test_extract_pattern_opt(self): self.cmd('init', '--encryption=repokey', self.repository_location) From 9f3a970cec49bd3bd7f70d2921a99bfe3b1c3725 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Feb 2017 07:17:23 +0100 Subject: [PATCH 0700/1387] borg benchmark crud command, fixes #1788 --- src/borg/archiver.py | 133 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 243e91d2..c5dace28 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -9,6 +9,7 @@ import logging import os import re import shlex +import shutil import signal import stat import subprocess @@ -17,6 +18,7 @@ import textwrap import time import traceback from binascii import unhexlify +from contextlib import contextmanager from datetime import datetime, timedelta from itertools import zip_longest @@ -57,7 +59,7 @@ from .helpers import basic_json_data, json_print from .item import Item from .key import key_creator, tam_required_file, tam_required, RepoKey, PassphraseKey from .keymanager import KeyManager -from .platform import get_flags, umount, get_process_id +from .platform import get_flags, umount, get_process_id, SyncFile from .remote import RepositoryServer, RemoteRepository, cache_if_remote from .repository import Repository, LIST_SCAN_LIMIT from .selftest import selftest @@ -322,6 +324,72 @@ class Archiver: logger.info('Key updated') return EXIT_SUCCESS + def do_benchmark_crud(self, args): + def measurement_run(repo, path): + archive = repo + '::borg-benchmark-crud' + compression = '--compression=none' + # measure create perf (without files cache to always have it chunking) + t_start = time.monotonic() + rc = self.do_create(self.parse_args(['create', compression, '--no-files-cache', archive + '1', path])) + t_end = time.monotonic() + dt_create = t_end - t_start + assert rc == 0 + # now build files cache + rc1 = self.do_create(self.parse_args(['create', compression, archive + '2', path])) + rc2 = self.do_delete(self.parse_args(['delete', archive + '2'])) + assert rc1 == rc2 == 0 + # measure a no-change update (archive1 is still present) + t_start = time.monotonic() + rc1 = self.do_create(self.parse_args(['create', compression, archive + '3', path])) + t_end = time.monotonic() + dt_update = t_end - t_start + rc2 = self.do_delete(self.parse_args(['delete', archive + '3'])) + assert rc1 == rc2 == 0 + # measure extraction (dry-run: without writing result to disk) + t_start = time.monotonic() + rc = self.do_extract(self.parse_args(['extract', '--dry-run', archive + '1'])) + t_end = time.monotonic() + dt_extract = t_end - t_start + assert rc == 0 + # measure archive deletion (of LAST present archive with the data) + t_start = time.monotonic() + rc = self.do_delete(self.parse_args(['delete', archive + '1'])) + t_end = time.monotonic() + dt_delete = t_end - t_start + assert rc == 0 + return dt_create, dt_update, dt_extract, dt_delete + + @contextmanager + def test_files(path, count, size, random): + path = os.path.join(path, 'borg-test-data') + os.makedirs(path) + for i in range(count): + fname = os.path.join(path, 'file_%d' % i) + data = b'\0' * size if not random else os.urandom(size) + with SyncFile(fname, binary=True) as fd: # used for posix_fadvise's sake + fd.write(data) + yield path + shutil.rmtree(path) + + for msg, count, size, random in [ + ('Z-BIG', 10, 100000000, False), + ('R-BIG', 10, 100000000, True), + ('Z-MEDIUM', 1000, 1000000, False), + ('R-MEDIUM', 1000, 1000000, True), + ('Z-SMALL', 10000, 10000, False), + ('R-SMALL', 10000, 10000, True), + ]: + with test_files(args.path, count, size, random) as path: + dt_create, dt_update, dt_extract, dt_delete = measurement_run(args.location.canonical_path(), path) + total_size_MB = count * size / 1e06 + file_size_formatted = format_file_size(size) + content = 'random' if random else 'all-zero' + fmt = '%s-%-10s %9.2f MB/s (%d * %s %s files: %.2fs)' + print(fmt % ('C', msg, total_size_MB / dt_create, count, file_size_formatted, content, dt_create)) + print(fmt % ('R', msg, total_size_MB / dt_extract, count, file_size_formatted, content, dt_extract)) + print(fmt % ('U', msg, total_size_MB / dt_update, count, file_size_formatted, content, dt_update)) + print(fmt % ('D', msg, total_size_MB / dt_delete, count, file_size_formatted, content, dt_delete)) + @with_repository(fake='dry_run', exclusive=True) def do_create(self, args, repository, manifest=None, key=None): """Create new archive""" @@ -3141,6 +3209,69 @@ class Archiver: subparser.add_argument('ids', metavar='IDs', nargs='+', type=str, help='hex object ID(s) to show refcounts for') + benchmark_epilog = process_epilog("These commands do various benchmarks.") + + subparser = subparsers.add_parser('benchmark', parents=[common_parser], add_help=False, + description='benchmark command', + epilog=benchmark_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='benchmark command') + + benchmark_parsers = subparser.add_subparsers(title='required arguments', metavar='') + subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser)) + + bench_crud_epilog = process_epilog(""" + This command benchmarks borg CRUD (create, read, update, delete) operations. + + It creates input data below the given PATH and backups this data into the given REPO. + The REPO must already exist (it could be a fresh empty repo or an existing repo, the + command will create / read / update / delete some archives named borg-test-data* there. + + Make sure you have free space there, you'll need about 1GB each (+ overhead). + + If your repository is encrypted and borg needs a passphrase to unlock the key, use: + + BORG_PASSPHRASE=mysecret borg benchmark crud REPO PATH + + Measurements are done with different input file sizes and counts. + The file contents are very artificial (either all zero or all random), + thus the measurement results do not necessarily reflect performance with real data. + Also, due to the kind of content used, no compression is used in these benchmarks. + + C- == borg create (1st archive creation, no compression, do not use files cache) + C-Z- == all-zero files. full dedup, this is primarily measuring reader/chunker/hasher. + C-R- == random files. no dedup, measuring throughput through all processing stages. + + R- == borg extract (extract archive, dry-run, do everything, but do not write files to disk) + R-Z- == all zero files. Measuring heavily duplicated files. + R-R- == random files. No duplication here, measuring throughput through all processing + stages, except writing to disk. + + U- == borg create (2nd archive creation of unchanged input files, measure files cache speed) + The throughput value is kind of virtual here, it does not actually read the file. + U-Z- == needs to check the 2 all-zero chunks' existence in the repo. + U-R- == needs to check existence of a lot of different chunks in the repo. + + D- == borg delete archive (delete last remaining archive, measure deletion + compaction) + D-Z- == few chunks to delete / few segments to compact/remove. + D-R- == many chunks to delete / many segments to compact/remove. + + Please note that there might be quite some variance in these measurements. + Try multiple measurements and having a otherwise idle machine (and network, if you use it). + """) + subparser = benchmark_parsers.add_parser('crud', parents=[common_parser], add_help=False, + description=self.do_benchmark_crud.__doc__, + epilog=bench_crud_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='benchmarks borg CRUD (create, extract, update, delete).') + subparser.set_defaults(func=self.do_benchmark_crud) + + subparser.add_argument('location', metavar='REPO', + type=location_validator(archive=False), + help='repo to use for benchmark (must exist)') + + subparser.add_argument('path', metavar='PATH', help='path were to create benchmark input data') + return parser @staticmethod From 7e9845fc68a202a9cdf84e04f2f983c2b4814560 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 28 Feb 2017 21:44:06 +0100 Subject: [PATCH 0701/1387] added borg benchmark crud output to docs/misc/ --- docs/misc/benchmark-crud.txt | 64 ++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 docs/misc/benchmark-crud.txt diff --git a/docs/misc/benchmark-crud.txt b/docs/misc/benchmark-crud.txt new file mode 100644 index 00000000..f4ca363b --- /dev/null +++ b/docs/misc/benchmark-crud.txt @@ -0,0 +1,64 @@ +borg benchmark crud +=================== + +Here is some example of borg benchmark crud output. + +I ran it on my laptop, Core i5-4200u, 8GB RAM, SATA SSD, Linux, ext4 fs. +"src" as well as repo is local, on this SSD. + +$ BORG_PASSPHRASE=secret borg init --encryption repokey-blake2 repo +$ BORG_PASSPHRASE=secret borg benchmark crud repo src + +C-Z-BIG 116.06 MB/s (10 * 100.00 MB all-zero files: 8.62s) +R-Z-BIG 197.00 MB/s (10 * 100.00 MB all-zero files: 5.08s) +U-Z-BIG 418.07 MB/s (10 * 100.00 MB all-zero files: 2.39s) +D-Z-BIG 724.94 MB/s (10 * 100.00 MB all-zero files: 1.38s) +C-R-BIG 42.21 MB/s (10 * 100.00 MB random files: 23.69s) +R-R-BIG 134.45 MB/s (10 * 100.00 MB random files: 7.44s) +U-R-BIG 316.83 MB/s (10 * 100.00 MB random files: 3.16s) +D-R-BIG 251.10 MB/s (10 * 100.00 MB random files: 3.98s) +C-Z-MEDIUM 118.53 MB/s (1000 * 1.00 MB all-zero files: 8.44s) +R-Z-MEDIUM 218.49 MB/s (1000 * 1.00 MB all-zero files: 4.58s) +U-Z-MEDIUM 591.59 MB/s (1000 * 1.00 MB all-zero files: 1.69s) +D-Z-MEDIUM 730.04 MB/s (1000 * 1.00 MB all-zero files: 1.37s) +C-R-MEDIUM 31.46 MB/s (1000 * 1.00 MB random files: 31.79s) +R-R-MEDIUM 129.64 MB/s (1000 * 1.00 MB random files: 7.71s) +U-R-MEDIUM 621.86 MB/s (1000 * 1.00 MB random files: 1.61s) +D-R-MEDIUM 234.82 MB/s (1000 * 1.00 MB random files: 4.26s) +C-Z-SMALL 19.81 MB/s (10000 * 10.00 kB all-zero files: 5.05s) +R-Z-SMALL 97.69 MB/s (10000 * 10.00 kB all-zero files: 1.02s) +U-Z-SMALL 36.35 MB/s (10000 * 10.00 kB all-zero files: 2.75s) +D-Z-SMALL 57.04 MB/s (10000 * 10.00 kB all-zero files: 1.75s) +C-R-SMALL 9.81 MB/s (10000 * 10.00 kB random files: 10.19s) +R-R-SMALL 92.21 MB/s (10000 * 10.00 kB random files: 1.08s) +U-R-SMALL 64.62 MB/s (10000 * 10.00 kB random files: 1.55s) +D-R-SMALL 51.62 MB/s (10000 * 10.00 kB random files: 1.94s) + + +A second run some time later gave: + +C-Z-BIG 115.22 MB/s (10 * 100.00 MB all-zero files: 8.68s) +R-Z-BIG 196.06 MB/s (10 * 100.00 MB all-zero files: 5.10s) +U-Z-BIG 439.50 MB/s (10 * 100.00 MB all-zero files: 2.28s) +D-Z-BIG 671.11 MB/s (10 * 100.00 MB all-zero files: 1.49s) +C-R-BIG 43.40 MB/s (10 * 100.00 MB random files: 23.04s) +R-R-BIG 133.17 MB/s (10 * 100.00 MB random files: 7.51s) +U-R-BIG 464.50 MB/s (10 * 100.00 MB random files: 2.15s) +D-R-BIG 245.19 MB/s (10 * 100.00 MB random files: 4.08s) +C-Z-MEDIUM 110.82 MB/s (1000 * 1.00 MB all-zero files: 9.02s) +R-Z-MEDIUM 217.96 MB/s (1000 * 1.00 MB all-zero files: 4.59s) +U-Z-MEDIUM 601.54 MB/s (1000 * 1.00 MB all-zero files: 1.66s) +D-Z-MEDIUM 686.99 MB/s (1000 * 1.00 MB all-zero files: 1.46s) +C-R-MEDIUM 39.91 MB/s (1000 * 1.00 MB random files: 25.06s) +R-R-MEDIUM 128.91 MB/s (1000 * 1.00 MB random files: 7.76s) +U-R-MEDIUM 599.00 MB/s (1000 * 1.00 MB random files: 1.67s) +D-R-MEDIUM 230.69 MB/s (1000 * 1.00 MB random files: 4.33s) +C-Z-SMALL 14.78 MB/s (10000 * 10.00 kB all-zero files: 6.76s) +R-Z-SMALL 96.86 MB/s (10000 * 10.00 kB all-zero files: 1.03s) +U-Z-SMALL 35.22 MB/s (10000 * 10.00 kB all-zero files: 2.84s) +D-Z-SMALL 64.93 MB/s (10000 * 10.00 kB all-zero files: 1.54s) +C-R-SMALL 11.08 MB/s (10000 * 10.00 kB random files: 9.02s) +R-R-SMALL 92.34 MB/s (10000 * 10.00 kB random files: 1.08s) +U-R-SMALL 64.49 MB/s (10000 * 10.00 kB random files: 1.55s) +D-R-SMALL 46.96 MB/s (10000 * 10.00 kB random files: 2.13s) + From d5707929fd76f68306122186f3e221ee6345c853 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 1 Mar 2017 01:53:23 +0100 Subject: [PATCH 0702/1387] [docs] improve remote-path description (ported bebehei's 1.0-maint change to master) --- docs/usage_general.rst.inc | 2 +- src/borg/archiver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage_general.rst.inc b/docs/usage_general.rst.inc index fc96f5b2..f718b45b 100644 --- a/docs/usage_general.rst.inc +++ b/docs/usage_general.rst.inc @@ -148,7 +148,7 @@ General: When set, use this command instead of ``ssh``. This can be used to specify ssh options, such as a custom identity file ``ssh -i /path/to/private/key``. See ``man ssh`` for other options. BORG_REMOTE_PATH - When set, use the given path/filename as remote path (default is "borg"). + When set, use the given path as borg executable on the remote (defaults to "borg" if unset). Using ``--remote-path PATH`` commandline option overrides the environment variable. BORG_FILES_CACHE_TTL When set to a numeric value, this determines the maximum "time to live" for the files cache diff --git a/src/borg/archiver.py b/src/borg/archiver.py index c5dace28..9f98411a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1888,7 +1888,7 @@ class Archiver: common_group.add_argument('--umask', dest='umask', type=lambda s: int(s, 8), default=UMASK_DEFAULT, metavar='M', help='set umask to M (local and remote, default: %(default)04o)') common_group.add_argument('--remote-path', dest='remote_path', metavar='PATH', - help='set remote path to executable (default: "borg")') + help='use PATH as borg executable on the remote (default: "borg")') common_group.add_argument('--remote-ratelimit', dest='remote_ratelimit', type=int, metavar='rate', help='set remote network upload rate limit in kiByte/s (default: 0=unlimited)') common_group.add_argument('--consider-part-files', dest='consider_part_files', From abb0a20d4f2539e25a0277b7a1c6bc2426888fc1 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 1 Mar 2017 16:58:06 +0100 Subject: [PATCH 0703/1387] list: files->items, clarifications --- docs/internals/frontends.rst | 15 ++++++--------- src/borg/helpers.py | 4 +++- src/borg/testsuite/archiver.py | 12 ++++++------ 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 0553cd00..df6d5032 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -42,7 +42,7 @@ archive_progress progress_message A message-based progress information with no concrete progress information, just a message - saying what is currently worked on. + saying what is currently being worked on. operation unique, opaque integer ID of the operation @@ -209,7 +209,7 @@ array under the *archives* key, while :ref:`borg_create` returns a single archiv Both formats contain a *name* key with the archive name, the *id* key with the hexadecimal archive ID, and the *start* key with the start timestamp. -info and create further have: +*borg info* and *borg create* further have: end End timestamp @@ -250,11 +250,8 @@ Example of a simple archive listing (``borg list --last 1 --json``):: { "archives": [ { - "archive": "2017-02-27T21:21:51", - "barchive": "2017-02-27T21:21:51", "id": "80cd07219ad725b3c5f665c1dcf119435c4dee1647a560ecac30f8d40221a46a", - "name": "2017-02-27T21:21:51", - "time": "Mon, 2017-02-27 21:21:52", + "name": "host-system-backup-2017-02-27", "start": "Mon, 2017-02-27 21:21:52" } ], @@ -287,7 +284,7 @@ The same archive with more information (``borg info --last 1 --json``):: "limits": { "max_archive_size": 0.0001330855110409714 }, - "name": "2017-02-27T21:21:51", + "name": "host-system-backup-2017-02-27", "start": "Mon, 2017-02-27 21:21:52", "stats": { "compressed_size": 1880961894, @@ -322,7 +319,7 @@ The same archive with more information (``borg info --last 1 --json``):: .. rubric:: File listings Listing the contents of an archive can produce *a lot* of JSON. Each item (file, directory, ...) is described -by one object in the *files* array of the :ref:`borg_list` output. Refer to the *borg list* documentation for +by one object in the *items* array of the :ref:`borg_list` output. Refer to the *borg list* documentation for the available keys and their meaning. Example (excerpt):: @@ -336,7 +333,7 @@ Example (excerpt):: "last_modified": "Mon, 2017-02-27 21:21:58", "location": "/home/user/repository" }, - "files": [ + "items": [ { "type": "d", "mode": "drwxr-xr-x", diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 9f5de739..db9fc92d 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1617,11 +1617,13 @@ class ArchiveFormatter(BaseFormatter): def get_item_data(self, archive): return { + # *name* is the key used by borg-info for the archive name, this makes the formats more compatible 'name': remove_surrogates(archive.name), 'barchive': archive.name, 'archive': remove_surrogates(archive.name), 'id': bin_to_hex(archive.id), 'time': format_time(to_localtime(archive.ts)), + # *start* is the key used by borg-info for this timestamp, this makes the formats more compatible 'start': format_time(to_localtime(archive.ts)), } @@ -1726,7 +1728,7 @@ class ItemFormatter(BaseFormatter): begin = json_dump(basic_json_data(self.archive.manifest)) begin, _, _ = begin.rpartition('\n}') # remove last closing brace, we want to extend the object begin += ',\n' - begin += ' "files": [\n' + begin += ' "items": [\n' return begin def end(self): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 6793e0de..b59b2cb5 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1535,17 +1535,17 @@ class ArchiverTestCase(ArchiverTestCaseBase): list_archive = json.loads(self.cmd('list', '--json', self.repository_location + '::test')) assert list_repo['repository'] == list_archive['repository'] - files = list_archive['files'] - assert len(files) == 2 - file1 = files[1] + items = list_archive['items'] + assert len(items) == 2 + file1 = items[1] assert file1['path'] == 'input/file1' assert file1['size'] == 81920 list_archive = json.loads(self.cmd('list', '--json', '--format={sha256}', self.repository_location + '::test')) assert list_repo['repository'] == list_archive['repository'] - files = list_archive['files'] - assert len(files) == 2 - file1 = files[1] + items = list_archive['items'] + assert len(items) == 2 + file1 = items[1] assert file1['path'] == 'input/file1' assert file1['sha256'] == 'b2915eb69f260d8d3c25249195f2c8f4f716ea82ec760ae929732c0262442b2b' From c50ffc21b045837502b7e4007ab174a6525ae778 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 2 Mar 2017 00:24:22 +0100 Subject: [PATCH 0704/1387] list: only load cache if needed --- src/borg/archiver.py | 23 ++++++++++++++++------- src/borg/helpers.py | 9 +++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 9f98411a..3961762b 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1032,21 +1032,30 @@ class Archiver: def _list_archive(self, args, repository, manifest, key, write): matcher, _ = self.build_matcher(args.patterns, args.paths) - with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: + if args.format is not None: + format = args.format + elif args.short: + format = "{path}{NL}" + else: + format = "{mode} {user:6} {group:6} {size:8} {isomtime} {path}{extra}{NL}" + + def _list_inner(cache): archive = Archive(repository, key, manifest, args.location.archive, cache=cache, consider_part_files=args.consider_part_files) - if args.format is not None: - format = args.format - elif args.short: - format = "{path}{NL}" - else: - format = "{mode} {user:6} {group:6} {size:8} {isomtime} {path}{extra}{NL}" formatter = ItemFormatter(archive, format, json=args.json) write(safe_encode(formatter.begin())) for item in archive.iter_items(lambda item: matcher.match(item.path)): write(safe_encode(formatter.format_item(item))) write(safe_encode(formatter.end())) + + # Only load the cache if it will be used + if ItemFormatter.format_needs_cache(format): + with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: + _list_inner(cache) + else: + _list_inner(cache=None) + return self.exit_code def _list_repository(self, args, manifest, write): diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 2f4429a3..be1d9741 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1617,6 +1617,10 @@ class ItemFormatter(BaseFormatter): ('health', ) ) + KEYS_REQUIRING_CACHE = ( + 'dsize', 'dcsize', 'unique_chunks', + ) + @classmethod def available_keys(cls): class FakeArchive: @@ -1648,6 +1652,11 @@ class ItemFormatter(BaseFormatter): assert not keys, str(keys) return "\n".join(help) + @classmethod + def format_needs_cache(cls, format): + format_keys = {f[1] for f in Formatter().parse(format)} + return any(key in cls.KEYS_REQUIRING_CACHE for key in format_keys) + def __init__(self, archive, format, *, json=False): self.archive = archive self.json = json From c190a87eef5dbde74b9d74fba90d5c1a916e5943 Mon Sep 17 00:00:00 2001 From: kmq Date: Sun, 26 Feb 2017 22:15:03 +0200 Subject: [PATCH 0705/1387] Improve automated backup script in doc fixes #2214 --- docs/quickstart.rst | 107 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 84 insertions(+), 23 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 52847168..350d638d 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -57,34 +57,95 @@ Also helpful: Automating backups ------------------ -The following example script backs up ``/home`` and ``/var/www`` to a remote -server. The script also uses the :ref:`borg_prune` subcommand to maintain a -certain number of old archives: +The following example script is meant to be run daily by the ``root`` user on +different local machines. It backs up a machine's important files (but not the +complete operating system) to a repository ``~/backup/main`` on a remote server. +Some files which aren't necessarily needed in this backup are excluded. See +:ref:`borg_patterns` on how to add more exclude options. -:: +After the backup this script also uses the :ref:`borg_prune` subcommand to keep +only a certain number of old archives and deletes the others in order to preserve +disk space. - #!/bin/sh - # setting this, so the repo does not need to be given on the commandline: - export BORG_REPO=username@remoteserver.com:backup +Before running, make sure that the repository is initialized as documented in +:ref:`remote_repos` and that the script has the correct permissions to be executable +by the root user, but not executable or readable by anyone else, i.e. root:root 0700. - # setting this, so you won't be asked for your passphrase - make sure the - # script has appropriate owner/group and mode, e.g. root.root 600: - export BORG_PASSPHRASE=mysecret +You can use this script as a starting point and modify it where it's necessary to fit +your setup. - # Backup most important stuff: - borg create --stats -C lz4 ::'{hostname}-{now:%Y-%m-%d}' \ - /etc \ - /home \ - /var \ - --exclude '/home/*/.cache' \ - --exclude '*.pyc' +Do not forget to test your created backups to make sure everything you need is being +backed up and that the ``prune`` command is keeping and deleting the correct backups. - # Use the `prune` subcommand to maintain 7 daily, 4 weekly and 6 monthly - # archives of THIS machine. The '{hostname}-' prefix is very important to - # limit prune's operation to this machine's archives and not apply to - # other machine's archives also. - borg prune --list $REPOSITORY --prefix '{hostname}-' \ - --keep-daily=7 --keep-weekly=4 --keep-monthly=6 + + :: + #!/bin/sh + + # Setting this, so the repo does not need to be given on the commandline: + export BORG_REPO=ssh://username@example.com:2022/~/backup/main + + # Setting this, so you won't be asked for your repository passphrase: + export BORG_PASSPHRASE='XYZl0ngandsecurepa_55_phrasea&&123' + + # some helpers and error handling: + function info () { echo -e "\n"`date` $@"\n" >&2; } + trap "echo `date` Backup interrupted >&2; exit 2" SIGINT SIGTERM + + info "Starting backup" + + # Backup the most important directories into an archive named after + # the machine this script is currently running on: + + borg create \ + --verbose \ + --filter AME \ + --list \ + --stats \ + --show-rc \ + --compression lz4 \ + --exclude-caches \ + --exclude '/home/*/.cache/*' \ + --exclude '/var/cache/*' \ + --exclude '/var/tmp/*' \ + \ + ::'{hostname}-{now}' \ + /etc \ + /home \ + /root \ + /var \ + + backup_exit=$? + + info "Pruning repository" + + # Use the `prune` subcommand to maintain 7 daily, 4 weekly and 6 monthly + # archives of THIS machine. The '{hostname}-' prefix is very important to + # limit prune's operation to this machine's archives and not apply to + # other machines' archives also: + + borg prune \ + --list \ + --prefix '{hostname}-' \ + --show-rc \ + --keep-daily 7 \ + --keep-weekly 4 \ + --keep-monthly 6 \ + + prune_exit=$? + + global_exit=$(( ${backup_exit} > ${prune_exit} ? ${backup_exit} : ${prune_exit} )) + + if [ ${global_exit} -eq 1 ]; + then + info "Backup and/or Prune finished with a warning" + fi + + if [ ${global_exit} -gt 1 ]; + then + info "Backup and/or Prune finished with an error" + fi + + exit ${global_exit} Pitfalls with shell variables and environment variables ------------------------------------------------------- From 1bbfb24d1aeb8ae1847517badf813ff01a8b047b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 3 Mar 2017 15:27:17 +0100 Subject: [PATCH 0706/1387] quickstart: fix rst issue --- docs/quickstart.rst | 106 ++++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 350d638d..466b8306 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -77,75 +77,75 @@ your setup. Do not forget to test your created backups to make sure everything you need is being backed up and that the ``prune`` command is keeping and deleting the correct backups. +:: - :: - #!/bin/sh + #!/bin/sh - # Setting this, so the repo does not need to be given on the commandline: - export BORG_REPO=ssh://username@example.com:2022/~/backup/main + # Setting this, so the repo does not need to be given on the commandline: + export BORG_REPO=ssh://username@example.com:2022/~/backup/main - # Setting this, so you won't be asked for your repository passphrase: - export BORG_PASSPHRASE='XYZl0ngandsecurepa_55_phrasea&&123' + # Setting this, so you won't be asked for your repository passphrase: + export BORG_PASSPHRASE='XYZl0ngandsecurepa_55_phrasea&&123' - # some helpers and error handling: - function info () { echo -e "\n"`date` $@"\n" >&2; } - trap "echo `date` Backup interrupted >&2; exit 2" SIGINT SIGTERM + # some helpers and error handling: + function info () { echo -e "\n"`date` $@"\n" >&2; } + trap "echo `date` Backup interrupted >&2; exit 2" SIGINT SIGTERM - info "Starting backup" + info "Starting backup" - # Backup the most important directories into an archive named after - # the machine this script is currently running on: + # Backup the most important directories into an archive named after + # the machine this script is currently running on: - borg create \ - --verbose \ - --filter AME \ - --list \ - --stats \ - --show-rc \ - --compression lz4 \ - --exclude-caches \ - --exclude '/home/*/.cache/*' \ - --exclude '/var/cache/*' \ - --exclude '/var/tmp/*' \ - \ - ::'{hostname}-{now}' \ - /etc \ - /home \ - /root \ - /var \ + borg create \ + --verbose \ + --filter AME \ + --list \ + --stats \ + --show-rc \ + --compression lz4 \ + --exclude-caches \ + --exclude '/home/*/.cache/*' \ + --exclude '/var/cache/*' \ + --exclude '/var/tmp/*' \ + \ + ::'{hostname}-{now}' \ + /etc \ + /home \ + /root \ + /var \ - backup_exit=$? + backup_exit=$? - info "Pruning repository" + info "Pruning repository" - # Use the `prune` subcommand to maintain 7 daily, 4 weekly and 6 monthly - # archives of THIS machine. The '{hostname}-' prefix is very important to - # limit prune's operation to this machine's archives and not apply to - # other machines' archives also: + # Use the `prune` subcommand to maintain 7 daily, 4 weekly and 6 monthly + # archives of THIS machine. The '{hostname}-' prefix is very important to + # limit prune's operation to this machine's archives and not apply to + # other machines' archives also: - borg prune \ - --list \ - --prefix '{hostname}-' \ - --show-rc \ - --keep-daily 7 \ - --keep-weekly 4 \ - --keep-monthly 6 \ + borg prune \ + --list \ + --prefix '{hostname}-' \ + --show-rc \ + --keep-daily 7 \ + --keep-weekly 4 \ + --keep-monthly 6 \ - prune_exit=$? + prune_exit=$? - global_exit=$(( ${backup_exit} > ${prune_exit} ? ${backup_exit} : ${prune_exit} )) + global_exit=$(( ${backup_exit} > ${prune_exit} ? ${backup_exit} : ${prune_exit} )) - if [ ${global_exit} -eq 1 ]; - then - info "Backup and/or Prune finished with a warning" - fi + if [ ${global_exit} -eq 1 ]; + then + info "Backup and/or Prune finished with a warning" + fi - if [ ${global_exit} -gt 1 ]; - then - info "Backup and/or Prune finished with an error" - fi + if [ ${global_exit} -gt 1 ]; + then + info "Backup and/or Prune finished with an error" + fi - exit ${global_exit} + exit ${global_exit} Pitfalls with shell variables and environment variables ------------------------------------------------------- From 503e9a27e648c00c5fe982fe9fd5bdd6920d556c Mon Sep 17 00:00:00 2001 From: TW Date: Sat, 4 Mar 2017 00:01:02 +0100 Subject: [PATCH 0707/1387] Fix compression exceptions (#2224) * trigger bug in --verify-data, see #2221 * raise decompression errors as DecompressionError, fixes #2221 this is a subclass of IntegrityError, so borg check --verify-data works correctly if the decompressor stumbles over corrupted data before the plaintext gets verified (in a unencrypted repository, otherwise the MAC check would fail first). * fixup: fix exception docstring, add placeholder, change wording --- src/borg/compress.pyx | 16 +++++++++++----- src/borg/helpers.py | 6 +++++- src/borg/testsuite/archiver.py | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index 9257ced0..91616725 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -4,9 +4,9 @@ try: except ImportError: lzma = None -from .helpers import Buffer +from .helpers import Buffer, DecompressionError -API_VERSION = '1.1_01' +API_VERSION = '1.1_02' cdef extern from "lz4.h": int LZ4_compress_limitedOutput(const char* source, char* dest, int inputSize, int maxOutputSize) nogil @@ -112,7 +112,7 @@ class LZ4(CompressorBase): break if osize > 2 ** 30: # this is insane, get out of here - raise Exception('lz4 decompress failed') + raise DecompressionError('lz4 decompress failed') # likely the buffer was too small, get a bigger one: osize = int(1.5 * osize) return dest[:rsize] @@ -138,7 +138,10 @@ class LZMA(CompressorBase): def decompress(self, data): data = super().decompress(data) - return lzma.decompress(data) + try: + return lzma.decompress(data) + except lzma.LZMAError as e: + raise DecompressionError(str(e)) from None class ZLIB(CompressorBase): @@ -167,7 +170,10 @@ class ZLIB(CompressorBase): def decompress(self, data): # note: for compatibility no super call, do not strip ID bytes - return zlib.decompress(data) + try: + return zlib.decompress(data) + except zlib.error as e: + raise DecompressionError(str(e)) from None COMPRESSOR_TABLE = { diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 3c38d77d..37adad23 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -81,6 +81,10 @@ class IntegrityError(ErrorWithTraceback): """Data integrity error: {}""" +class DecompressionError(IntegrityError): + """Decompression error: {}""" + + class ExtensionModuleError(Error): """The Borg binary extension modules do not seem to be properly installed""" @@ -99,7 +103,7 @@ def check_extension_modules(): raise ExtensionModuleError if chunker.API_VERSION != '1.1_01': raise ExtensionModuleError - if compress.API_VERSION != '1.1_01': + if compress.API_VERSION != '1.1_02': raise ExtensionModuleError if crypto.API_VERSION != '1.1_01': raise ExtensionModuleError diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index d2142a34..1bff8d5b 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -265,7 +265,7 @@ class ArchiverTestCaseBase(BaseTestCase): return output def create_src_archive(self, name): - self.cmd('create', '--compression=none', self.repository_location + '::' + name, src_dir) + self.cmd('create', '--compression=lz4', self.repository_location + '::' + name, src_dir) def open_archive(self, name): repository = Repository(self.repository_path, exclusive=True) From fd0649767a734083cb38d72d8e12def8b0557c64 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 3 Mar 2017 15:41:08 +0100 Subject: [PATCH 0708/1387] hashindex: rebuild hashtable if we have too little empty buckets, fixes #2246 if there are too many deleted buckets (tombstones), hashtable performance goes down the drain. in the worst case of 0 empty buckets and lots of tombstones, this results in full table scans for new / unknown keys. thus we make sure we always have a good amount of empty buckets. --- src/borg/_hashindex.c | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/borg/_hashindex.c b/src/borg/_hashindex.c index adcb90fd..51290c5a 100644 --- a/src/borg/_hashindex.c +++ b/src/borg/_hashindex.c @@ -47,11 +47,13 @@ typedef struct { void *buckets; int num_entries; int num_buckets; + int num_empty; int key_size; int value_size; off_t bucket_size; int lower_limit; int upper_limit; + int min_empty; } HashIndex; /* prime (or w/ big prime factors) hash table sizes @@ -77,6 +79,7 @@ static int hash_sizes[] = { #define HASH_MIN_LOAD .25 #define HASH_MAX_LOAD .75 /* don't go higher than 0.75, otherwise performance severely suffers! */ +#define HASH_MAX_EFF_LOAD .93 #define MAX(x, y) ((x) > (y) ? (x): (y)) #define NELEMS(x) (sizeof(x) / sizeof((x)[0])) @@ -171,8 +174,10 @@ hashindex_resize(HashIndex *index, int capacity) free(index->buckets); index->buckets = new->buckets; index->num_buckets = new->num_buckets; + index->num_empty = index->num_buckets - index->num_entries; index->lower_limit = new->lower_limit; index->upper_limit = new->upper_limit; + index->min_empty = new->min_empty; free(new); return 1; } @@ -191,6 +196,11 @@ int get_upper_limit(int num_buckets){ return (int)(num_buckets * HASH_MAX_LOAD); } +int get_min_empty(int num_buckets){ + /* Differently from load, the effective load also considers tombstones (deleted buckets). */ + return (int)(num_buckets * (1.0 - HASH_MAX_EFF_LOAD)); +} + int size_idx(int size){ /* find the hash_sizes index with entry >= size */ int elems = NELEMS(hash_sizes); @@ -224,6 +234,19 @@ int shrink_size(int current){ return hash_sizes[i]; } +int +count_empty(HashIndex *index) +{ /* count empty (never used) buckets. this does NOT include deleted buckets (tombstones). + * TODO: if we ever change HashHeader, save the count there so we do not need this function. + */ + int i, count = 0, capacity = index->num_buckets; + for(i = 0; i < capacity; i++) { + if(BUCKET_IS_EMPTY(index, i)) + count++; + } + return count; +} + /* Public API */ static HashIndex * hashindex_read(const char *path) @@ -303,6 +326,17 @@ hashindex_read(const char *path) index->bucket_size = index->key_size + index->value_size; index->lower_limit = get_lower_limit(index->num_buckets); index->upper_limit = get_upper_limit(index->num_buckets); + index->min_empty = get_min_empty(index->num_buckets); + index->num_empty = count_empty(index); + if(index->num_empty < index->min_empty) { + /* too many tombstones here / not enough empty buckets, do a same-size rebuild */ + if(!hashindex_resize(index, index->num_buckets)) { + free(index->buckets); + free(index); + index = NULL; + goto fail; + } + } fail: if(fclose(fd) < 0) { EPRINTF_PATH(path, "fclose failed"); @@ -330,9 +364,11 @@ hashindex_init(int capacity, int key_size, int value_size) index->key_size = key_size; index->value_size = value_size; index->num_buckets = capacity; + index->num_empty = capacity; index->bucket_size = index->key_size + index->value_size; index->lower_limit = get_lower_limit(index->num_buckets); index->upper_limit = get_upper_limit(index->num_buckets); + index->min_empty = get_min_empty(index->num_buckets); for(i = 0; i < capacity; i++) { BUCKET_MARK_EMPTY(index, i); } @@ -406,6 +442,15 @@ hashindex_set(HashIndex *index, const void *key, const void *value) while(!BUCKET_IS_EMPTY(index, idx) && !BUCKET_IS_DELETED(index, idx)) { idx = (idx + 1) % index->num_buckets; } + if(BUCKET_IS_EMPTY(index, idx)){ + index->num_empty--; + if(index->num_empty < index->min_empty) { + /* too many tombstones here / not enough empty buckets, do a same-size rebuild */ + if(!hashindex_resize(index, index->num_buckets)) { + return 0; + } + } + } ptr = BUCKET_ADDR(index, idx); memcpy(ptr, key, index->key_size); memcpy(ptr + index->key_size, value, index->value_size); From 4b33c3fe143a1cc52ec8e566232ae03e33440a2b Mon Sep 17 00:00:00 2001 From: Abdel-Rahman Date: Sun, 5 Mar 2017 14:33:42 +0200 Subject: [PATCH 0709/1387] Add return code functions (#2199) --- src/borg/archiver.py | 4 ++-- src/borg/helpers.py | 20 ++++++++++++++++++++ src/borg/testsuite/archiver.py | 1 + 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 0f0867dd..357defa7 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -36,7 +36,7 @@ from .cache import Cache from .constants import * # NOQA from .crc32 import crc32 from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR -from .helpers import Error, NoManifestError +from .helpers import Error, NoManifestError, set_ec from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec from .helpers import PrefixSpec, SortBySpec, HUMAN_SORT_KEYS from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter @@ -3372,7 +3372,7 @@ class Archiver: self.prerun_checks(logger) if is_slow_msgpack(): logger.warning("Using a pure-python msgpack! This will result in lower performance.") - return func(args) + return set_ec(func(args)) def sig_info_handler(sig_no, stack): # pragma: no cover diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 37adad23..fe82b92f 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -52,6 +52,26 @@ def Chunk(data, **meta): return _Chunk(meta, data) +''' +The global exit_code variable is used so that modules other than archiver can increase the program exit code if a +warning or error occured during their operation. This is different from archiver.exit_code, which is only accessible +from the archiver object. +''' +exit_code = EXIT_SUCCESS + + +def set_ec(ec): + ''' + Sets the exit code of the program, if an exit code higher or equal than this is set, this does nothing. This + makes EXIT_ERROR override EXIT_WARNING, etc.. + + ec: exit code to set + ''' + global exit_code + exit_code = max(exit_code, ec) + return exit_code + + class Error(Exception): """Error base class""" diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index a8f4d95a..e2184a67 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -79,6 +79,7 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw): archiver = Archiver() archiver.prerun_checks = lambda *args: None archiver.exit_code = EXIT_SUCCESS + helpers.exit_code = EXIT_SUCCESS try: args = archiver.parse_args(list(args)) # argparse parsing may raise SystemExit when the command line is bad or From 98e80ef2fbfdce30ba10abc2e94a942c7d5eb7b1 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 7 Mar 2017 05:07:51 +0100 Subject: [PATCH 0710/1387] help python development by testing 3.6-dev --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4122d564..1c9dbcaf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,10 @@ matrix: os: linux dist: trusty env: TOXENV=flake8 + - python: "3.6-dev" + os: linux + dist: trusty + env: TOXENV=py36 - language: generic os: osx osx_image: xcode6.4 From e4391dec54287bc72a939003c15943f888d127d9 Mon Sep 17 00:00:00 2001 From: Mark Edgington Date: Tue, 7 Mar 2017 19:55:46 -0500 Subject: [PATCH 0711/1387] docs: improve --exclude-if-present and --keep-exclude-tags --- src/borg/archiver.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 357defa7..cb4350a1 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2254,6 +2254,15 @@ class Archiver: '\*/.bundler/gems' to get the same effect. See ``borg help patterns`` for more information. + In addition to using ``--exclude`` patterns, it is possible to use + ``--exclude-if-present`` to specify the name of a filesystem object (e.g. a file + or folder name) which, when contained within another folder, will prevent the + containing folder from being backed up. By default, the containing folder and + all of its contents will be omitted from the backup. If, however, you wish to + only include the objects specified by ``--exclude-if-present`` in your backup, + and not include any other contents of the containing folder, this can be enabled + through using the ``--keep-exclude-tags`` option. + Item flags ++++++++++ @@ -2337,8 +2346,8 @@ class Archiver: 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', action='store_true', default=False, - help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise ' - 'excluded caches/directories') + help='if tag objects are specified with --exclude-if-present, don\'t omit the tag ' + 'objects themselves from the backup archive') exclude_group.add_argument('--pattern', action=ArgparsePatternAction, metavar="PATTERN", help='include/exclude paths matching PATTERN') @@ -2510,8 +2519,8 @@ class Archiver: 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', action='store_true', default=False, - help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise ' - 'excluded caches/directories') + help='if tag objects are specified with --exclude-if-present, don\'t omit the tag ' + 'objects themselves from the backup archive') exclude_group.add_argument('--pattern', action=ArgparsePatternAction, metavar="PATTERN", help='include/exclude paths matching PATTERN') @@ -2626,8 +2635,8 @@ class Archiver: 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', action='store_true', default=False, - help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise ' - 'excluded caches/directories') + help='if tag objects are specified with --exclude-if-present, don\'t omit the tag ' + 'objects themselves from the backup archive') exclude_group.add_argument('--pattern', action=ArgparsePatternAction, metavar="PATTERN", help='include/exclude paths matching PATTERN') @@ -2921,9 +2930,9 @@ class Archiver: This is an *experimental* feature. Do *not* use this on your only backup. - --exclude, --exclude-from and PATH have the exact same semantics - as in "borg create". If PATHs are specified the resulting archive - will only contain files from these PATHs. + --exclude, --exclude-from, --exclude-if-present, --keep-exclude-tags, and PATH + have the exact same semantics as in "borg create". If PATHs are specified the + resulting archive will only contain files from these PATHs. Note that all paths in an archive are relative, therefore absolute patterns/paths will *not* match (--exclude, --exclude-from, --compression-from, PATHs). @@ -2991,8 +3000,8 @@ class Archiver: 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', action='store_true', default=False, - help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise ' - 'excluded caches/directories') + help='if tag objects are specified with --exclude-if-present, don\'t omit the tag ' + 'objects themselves from the backup archive') exclude_group.add_argument('--pattern', action=ArgparsePatternAction, metavar="PATTERN", help='include/exclude paths matching PATTERN') From fc41c98a86835d52a5ab481f7c1064a35f91561b Mon Sep 17 00:00:00 2001 From: enkore Date: Wed, 8 Mar 2017 17:08:54 +0100 Subject: [PATCH 0712/1387] Redo key_creator, key_factory, centralise key knowledge (#2272) * key: put key metadata (name, storage) into key classses * keymanager: use key-declared storage types --- src/borg/key.py | 80 ++++++++++++++++++++++++++++-------------- src/borg/keymanager.py | 27 +++++--------- 2 files changed, 62 insertions(+), 45 deletions(-) diff --git a/src/borg/key.py b/src/borg/key.py index 4358c644..2b8d8d64 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -87,45 +87,38 @@ class TAMUnsupportedSuiteError(IntegrityError): traceback = False +class KeyBlobStorage: + NO_STORAGE = 'no_storage' + KEYFILE = 'keyfile' + REPO = 'repository' + + def key_creator(repository, args): - if args.encryption == 'keyfile': - return KeyfileKey.create(repository, args) - elif args.encryption == 'repokey': - return RepoKey.create(repository, args) - elif args.encryption == 'keyfile-blake2': - return Blake2KeyfileKey.create(repository, args) - elif args.encryption == 'repokey-blake2': - return Blake2RepoKey.create(repository, args) - elif args.encryption == 'authenticated': - return AuthenticatedKey.create(repository, args) - elif args.encryption == 'none': - return PlaintextKey.create(repository, args) + for key in AVAILABLE_KEY_TYPES: + if key.ARG_NAME == args.encryption: + return key.create(repository, args) else: raise ValueError('Invalid encryption mode "%s"' % args.encryption) -def key_factory(repository, manifest_data): +def identify_key(manifest_data): key_type = manifest_data[0] - if key_type == KeyfileKey.TYPE: - return KeyfileKey.detect(repository, manifest_data) - elif key_type == RepoKey.TYPE: - return RepoKey.detect(repository, manifest_data) - elif key_type == PassphraseKey.TYPE: + if key_type == PassphraseKey.TYPE: # we just dispatch to repokey mode and assume the passphrase was migrated to a repokey. # see also comment in PassphraseKey class. - return RepoKey.detect(repository, manifest_data) - elif key_type == PlaintextKey.TYPE: - return PlaintextKey.detect(repository, manifest_data) - elif key_type == Blake2KeyfileKey.TYPE: - return Blake2KeyfileKey.detect(repository, manifest_data) - elif key_type == Blake2RepoKey.TYPE: - return Blake2RepoKey.detect(repository, manifest_data) - elif key_type == AuthenticatedKey.TYPE: - return AuthenticatedKey.detect(repository, manifest_data) + return RepoKey + + for key in AVAILABLE_KEY_TYPES: + if key.TYPE == key_type: + return key else: raise UnsupportedPayloadError(key_type) +def key_factory(repository, manifest_data): + return identify_key(manifest_data).detect(repository, manifest_data) + + def tam_required_file(repository): security_dir = get_security_dir(bin_to_hex(repository.id)) return os.path.join(security_dir, 'tam_required') @@ -139,6 +132,13 @@ def tam_required(repository): class KeyBase: TYPE = None # override in subclasses + # Human-readable name + NAME = 'UNDEFINED' + # Name used in command line / API (e.g. borg init --encryption=...) + ARG_NAME = 'UNDEFINED' + # Storage type (no key blob storage / keyfile / repo) + STORAGE = KeyBlobStorage.NO_STORAGE + def __init__(self, repository): self.TYPE_STR = bytes([self.TYPE]) self.repository = repository @@ -236,6 +236,8 @@ class KeyBase: class PlaintextKey(KeyBase): TYPE = 0x02 NAME = 'plaintext' + ARG_NAME = 'none' + STORAGE = KeyBlobStorage.NO_STORAGE chunk_seed = 0 @@ -466,6 +468,9 @@ class PassphraseKey(ID_HMAC_SHA_256, AESKeyBase): # This class is kept for a while to support migration from passphrase to repokey mode. TYPE = 0x01 NAME = 'passphrase' + ARG_NAME = None + STORAGE = KeyBlobStorage.NO_STORAGE + iterations = 100000 # must not be changed ever! @classmethod @@ -623,6 +628,9 @@ class KeyfileKeyBase(AESKeyBase): class KeyfileKey(ID_HMAC_SHA_256, KeyfileKeyBase): TYPE = 0x00 NAME = 'key file' + ARG_NAME = 'keyfile' + STORAGE = KeyBlobStorage.KEYFILE + FILE_ID = 'BORG_KEY' def sanity_check(self, filename, id): @@ -683,6 +691,8 @@ class KeyfileKey(ID_HMAC_SHA_256, KeyfileKeyBase): class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase): TYPE = 0x03 NAME = 'repokey' + ARG_NAME = 'repokey' + STORAGE = KeyBlobStorage.REPO def find_key(self): loc = self.repository._location.canonical_path() @@ -715,6 +725,9 @@ class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase): class Blake2KeyfileKey(ID_BLAKE2b_256, KeyfileKey): TYPE = 0x04 NAME = 'key file BLAKE2b' + ARG_NAME = 'keyfile-blake2' + STORAGE = KeyBlobStorage.KEYFILE + FILE_ID = 'BORG_KEY' MAC = blake2b_256 @@ -722,12 +735,17 @@ class Blake2KeyfileKey(ID_BLAKE2b_256, KeyfileKey): class Blake2RepoKey(ID_BLAKE2b_256, RepoKey): TYPE = 0x05 NAME = 'repokey BLAKE2b' + ARG_NAME = 'repokey-blake2' + STORAGE = KeyBlobStorage.REPO + MAC = blake2b_256 class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): TYPE = 0x06 NAME = 'authenticated BLAKE2b' + ARG_NAME = 'authenticated' + STORAGE = KeyBlobStorage.REPO def encrypt(self, chunk): chunk = self.compress(chunk) @@ -742,3 +760,11 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): data = self.compressor.decompress(payload) self.assert_id(id, data) return Chunk(data) + + +AVAILABLE_KEY_TYPES = ( + PlaintextKey, + PassphraseKey, + KeyfileKey, RepoKey, + Blake2KeyfileKey, Blake2RepoKey, AuthenticatedKey, +) diff --git a/src/borg/keymanager.py b/src/borg/keymanager.py index 49799afc..c4c1f602 100644 --- a/src/borg/keymanager.py +++ b/src/borg/keymanager.py @@ -4,7 +4,7 @@ import textwrap from hashlib import sha256 import pkgutil -from .key import KeyfileKey, RepoKey, PassphraseKey, KeyfileNotFoundError, PlaintextKey +from .key import KeyfileKey, KeyfileNotFoundError, KeyBlobStorage, identify_key from .helpers import Manifest, NoManifestError, Error, yes, bin_to_hex from .repository import Repository @@ -31,10 +31,6 @@ def sha256_truncated(data, num): return h.hexdigest()[:num] -KEYBLOB_LOCAL = 'local' -KEYBLOB_REPO = 'repo' - - class KeyManager: def __init__(self, repository): self.repository = repository @@ -42,32 +38,27 @@ class KeyManager: self.keyblob_storage = None try: - cdata = self.repository.get(Manifest.MANIFEST_ID) + manifest_data = self.repository.get(Manifest.MANIFEST_ID) except Repository.ObjectNotFound: raise NoManifestError - key_type = cdata[0] - if key_type == KeyfileKey.TYPE: - self.keyblob_storage = KEYBLOB_LOCAL - elif key_type == RepoKey.TYPE or key_type == PassphraseKey.TYPE: - self.keyblob_storage = KEYBLOB_REPO - elif key_type == PlaintextKey.TYPE: + key = identify_key(manifest_data) + self.keyblob_storage = key.STORAGE + if self.keyblob_storage == KeyBlobStorage.NO_STORAGE: raise UnencryptedRepo() - else: - raise UnknownKeyType(key_type) def load_keyblob(self): - if self.keyblob_storage == KEYBLOB_LOCAL: + if self.keyblob_storage == KeyBlobStorage.KEYFILE: k = KeyfileKey(self.repository) target = k.find_key() with open(target, 'r') as fd: self.keyblob = ''.join(fd.readlines()[1:]) - elif self.keyblob_storage == KEYBLOB_REPO: + elif self.keyblob_storage == KeyBlobStorage.REPO: self.keyblob = self.repository.load_key().decode() def store_keyblob(self, args): - if self.keyblob_storage == KEYBLOB_LOCAL: + if self.keyblob_storage == KeyBlobStorage.KEYFILE: k = KeyfileKey(self.repository) try: target = k.find_key() @@ -75,7 +66,7 @@ class KeyManager: target = k.get_new_target(args) self.store_keyfile(target) - elif self.keyblob_storage == KEYBLOB_REPO: + elif self.keyblob_storage == KeyBlobStorage.REPO: self.repository.save_key(self.keyblob.encode('utf-8')) def get_keyfile_data(self): From 1551be54bebcdb9b1b03278a4c860a0833991254 Mon Sep 17 00:00:00 2001 From: Florent Hemmi Date: Tue, 7 Mar 2017 14:38:45 +0100 Subject: [PATCH 0713/1387] Docs: one link per distro in the installation page --- docs/installation.rst | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 5a904eaf..c4a1b84c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -42,10 +42,10 @@ package which can be installed with the package manager. Distribution Source Command ============ ============================================= ======= Arch Linux `[community]`_ ``pacman -S borg`` -Debian `jessie-backports`_, `stretch`_, `sid`_ ``apt install borgbackup`` +Debian `Debian packages`_ ``apt install borgbackup`` Gentoo `ebuild`_ ``emerge borgbackup`` GNU Guix `GNU Guix`_ ``guix package --install borg`` -Fedora/RHEL `Fedora official repository`_, `EPEL`_ ``dnf install borgbackup`` +Fedora/RHEL `Fedora official repository`_ ``dnf install borgbackup`` FreeBSD `FreeBSD ports`_ ``cd /usr/ports/archivers/py-borgbackup && make install clean`` Mageia `cauldron`_ ``urpmi borgbackup`` NetBSD `pkgsrc`_ ``pkg_add py-borgbackup`` @@ -55,15 +55,12 @@ OpenIndiana `OpenIndiana hipster repository`_ ``pkg install borg`` openSUSE `openSUSE official repository`_ ``zypper in python3-borgbackup`` OS X `Brew cask`_ ``brew cask install borgbackup`` Raspbian `Raspbian testing`_ ``apt install borgbackup`` -Ubuntu `16.04`_, backports (PPA): `15.10`_, `14.04`_ ``apt install borgbackup`` +Ubuntu `Ubuntu packages`_, `Ubuntu PPA`_ ``apt install borgbackup`` ============ ============================================= ======= .. _[community]: https://www.archlinux.org/packages/?name=borg -.. _jessie-backports: https://packages.debian.org/jessie-backports/borgbackup -.. _stretch: https://packages.debian.org/stretch/borgbackup -.. _sid: https://packages.debian.org/sid/borgbackup +.. _Debian packages: https://packages.debian.org/search?keywords=borgbackup&searchon=names&exact=1&suite=all§ion=all .. _Fedora official repository: https://apps.fedoraproject.org/packages/borgbackup -.. _EPEL: https://admin.fedoraproject.org/pkgdb/package/rpms/borgbackup/ .. _FreeBSD ports: http://www.freshports.org/archivers/py-borgbackup/ .. _ebuild: https://packages.gentoo.org/packages/app-backup/borgbackup .. _GNU Guix: https://www.gnu.org/software/guix/package-list.html#borg @@ -75,9 +72,8 @@ Ubuntu `16.04`_, backports (PPA): `15.10`_, `14.04`_ ``apt install borgbac .. _openSUSE official repository: http://software.opensuse.org/package/borgbackup .. _Brew cask: http://caskroom.io/ .. _Raspbian testing: http://archive.raspbian.org/raspbian/pool/main/b/borgbackup/ -.. _16.04: https://launchpad.net/ubuntu/xenial/+source/borgbackup -.. _15.10: https://launchpad.net/~costamagnagianfranco/+archive/ubuntu/borgbackup -.. _14.04: https://launchpad.net/~costamagnagianfranco/+archive/ubuntu/borgbackup +.. _Ubuntu packages: http://packages.ubuntu.com/xenial/borgbackup +.. _Ubuntu PPA: https://launchpad.net/~costamagnagianfranco/+archive/ubuntu/borgbackup Please ask package maintainers to build a package or, if you can package / submit it yourself, please help us with that! See :issue:`105` on From 63b5cbfc99a2d7f2e064c539ddc08170862a3ed3 Mon Sep 17 00:00:00 2001 From: Abdel-Rahman Date: Wed, 8 Mar 2017 18:13:42 +0200 Subject: [PATCH 0714/1387] extract: warning RC for unextracted big extended attributes, followup (#2258) * Set warning exit code when xattr is too big * Warnings for more extended attributes errors (ENOTSUP, EACCES) * Add tests for all xattr warnings --- src/borg/archive.py | 18 ++++++++++++------ src/borg/testsuite/archiver.py | 27 ++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 9cab897d..0756730b 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -26,7 +26,7 @@ from .constants import * # NOQA from .hashindex import ChunkIndex, ChunkIndexEntry from .helpers import Manifest from .helpers import Chunk, ChunkIteratorFileWrapper, open_item -from .helpers import Error, IntegrityError +from .helpers import Error, IntegrityError, set_ec from .helpers import uid2user, user2uid, gid2group, group2gid from .helpers import parse_timestamp, to_localtime from .helpers import format_time, format_timedelta, format_file_size, file_status, FileSize @@ -689,13 +689,19 @@ Utilization of max. archive size: {csize_max:.0%} xattr.setxattr(fd or path, k, v, follow_symlinks=False) except OSError as e: if e.errno == errno.E2BIG: + # xattr is too big logger.warning('%s: Value or key of extended attribute %s is too big for this filesystem' % (path, k.decode())) - elif e.errno not in (errno.ENOTSUP, errno.EACCES): - # only raise if the errno is not on our ignore list: - # ENOTSUP == xattrs not supported here - # EACCES == permission denied to set this specific xattr - # (this may happen related to security.* keys) + set_ec(EXIT_WARNING) + elif e.errno == errno.ENOTSUP: + # xattrs not supported here + logger.warning('%s: Extended attributes are not supported on this filesystem' % path) + set_ec(EXIT_WARNING) + elif e.errno == errno.EACCES: + # permission denied to set this specific xattr (this may happen related to security.* keys) + logger.warning('%s: Permission denied when setting extended attribute %s' % (path, k.decode())) + set_ec(EXIT_WARNING) + else: raise def set_meta(self, key, value): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index e2184a67..52cb05a6 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1049,18 +1049,35 @@ class ArchiverTestCase(ArchiverTestCaseBase): @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='xattr not supported on this system or on this version of' 'fakeroot') - def test_extract_big_xattrs(self): - def patched_setxattr(*args, **kwargs): + def test_extract_xattrs_errors(self): + def patched_setxattr_E2BIG(*args, **kwargs): raise OSError(errno.E2BIG, 'E2BIG') + + def patched_setxattr_ENOTSUP(*args, **kwargs): + raise OSError(errno.ENOTSUP, 'ENOTSUP') + + def patched_setxattr_EACCES(*args, **kwargs): + raise OSError(errno.EACCES, 'EACCES') + self.create_regular_file('file') xattr.setxattr('input/file', 'attribute', 'value') self.cmd('init', self.repository_location, '-e' 'none') self.cmd('create', self.repository_location + '::test', 'input') with changedir('output'): - with patch.object(xattr, 'setxattr', patched_setxattr): + input_abspath = os.path.abspath('input/file') + with patch.object(xattr, 'setxattr', patched_setxattr_E2BIG): out = self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING) - assert out == (os.path.abspath('input/file') + ': Value or key of extended attribute attribute is too big' - 'for this filesystem\n') + assert out == (input_abspath + ': Value or key of extended attribute attribute is too big for this ' + 'filesystem\n') + os.remove(input_abspath) + with patch.object(xattr, 'setxattr', patched_setxattr_ENOTSUP): + out = self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING) + assert out == (input_abspath + ': Extended attributes are not supported on this filesystem\n') + os.remove(input_abspath) + with patch.object(xattr, 'setxattr', patched_setxattr_EACCES): + out = self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING) + assert out == (input_abspath + ': Permission denied when setting extended attribute attribute\n') + assert os.path.isfile(input_abspath) def test_path_normalization(self): self.cmd('init', '--encryption=repokey', self.repository_location) From 85a2d2fc08fc3611bc5774aa8c3c2ff8744a2509 Mon Sep 17 00:00:00 2001 From: Milkey Mouse Date: Tue, 7 Mar 2017 20:30:18 -0800 Subject: [PATCH 0715/1387] Add warning about running build_usage on Python >3.4 (fixes #2123) Python 3.6 added some new guaranteed hashing algorithms that will show up as available in the docs even though the baseline for support is Python 3.4; running build_usage with Python 3.4 will fix this. # Conflicts: # docs/development.rst --- docs/development.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/development.rst b/docs/development.rst index f88167a5..2e711a91 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -297,7 +297,10 @@ Checklist: - update ``CHANGES.rst``, based on ``git log $PREVIOUS_RELEASE..`` - check version number of upcoming release in ``CHANGES.rst`` - verify that ``MANIFEST.in`` and ``setup.py`` are complete -- ``python setup.py build_api ; python setup.py build_usage ; python setup.py build_man`` and commit +- ``python setup.py build_api ; python setup.py build_usage ; python + setup.py build_man`` and commit (be sure to build with Python 3.4 as + Python 3.6 added `more guaranteed hashing algorithms + `_) - tag the release:: git tag -s -m "tagged/signed release X.Y.Z" X.Y.Z From e98b5b20df49373e8ac8d48073dde3ecf2b2cb33 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 3 Mar 2017 15:24:57 +0100 Subject: [PATCH 0716/1387] yes(): handle JSON output --- docs/internals/frontends.rst | 84 +++++++++++++++++++++++++++++++++++- docs/usage_general.rst.inc | 1 + src/borg/helpers.py | 34 +++++++++++---- 3 files changed, 110 insertions(+), 9 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index df6d5032..5ce72f8d 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -96,6 +96,8 @@ log_message :ref:`msgid ` Message ID, may be *none* or absent +See Prompts_ for the types used by prompts. + .. rubric:: Examples (reformatted, each object would be on exactly one line) :ref:`borg_extract` progress:: @@ -129,10 +131,78 @@ A debug log message:: {"message": "35 self tests completed in 0.08 seconds", "type": "log_message", "created": 1488278449.5575905, "levelname": "DEBUG", "name": "borg.archiver"} +Prompts +------- + +Prompts assume a JSON form as well when the ``--log-json`` option is specified. Responses +are still read verbatim from *stdin*, while prompts are JSON messages printed to *stderr*, +just like log messages. + +Prompts use the *question_prompt*, *question_prompt_retry*, *question_invalid_answer*, +*question_accepted_default*, *question_accepted_true*, *question_accepted_false* and +*question_env_answer* types. + +The *message* property contains the same string displayed regularly in the same situation, +while the *msgid* property may contain a msgid_, typically the name of the +environment variable that can be used to override the prompt. It is the same for all JSON +messages pertaining to the same prompt. + +The *is_prompt* boolean property distinguishes informational messages from prompts, it +is true for *question_prompt* and *question_prompt_retry* types, otherwise it is false. + +.. rubric:: Examples (reformatted, each object would be on exactly one line) + +Providing an invalid answer:: + + {"type": "question_prompt", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "is_prompt": true, + "message": "... Type 'YES' if you understand this and want to continue: "} + incorrect answer # input on stdin + {"type": "question_invalid_answer", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "is_prompt": false, + "message": "Invalid answer, aborting."} + +Providing a false (negative) answer:: + + {"type": "question_prompt", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "is_prompt": true, + "message": "... Type 'YES' if you understand this and want to continue: "} + NO # input on stdin + {"type": "question_accepted_false", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", + "message": "Aborting.", "is_prompt": false} + +Providing a true (affirmative) answer:: + + {"type": "question_prompt", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "is_prompt": true, + "message": "... Type 'YES' if you understand this and want to continue: "} + YES # input on stdin + # no further output, just like the prompt without --log-json + +Passphrase prompts +------------------ + +Passphrase prompts should be handled differently. Use the environment variables *BORG_PASSPHRASE* +and *BORG_NEW_PASSPHRASE* (see :ref:`env_vars` for reference) to pass passphrases to Borg, don't +use the interactive passphrase prompts. + +When setting a new passphrase (:ref:`borg_init`, :ref:`borg_key_change-passphrase`) normally +Borg prompts whether it should display the passphrase. This can be suppressed by setting +the environment variable *BORG_DISPLAY_PASSPHRASE* to *no*. + +When "confronted" with an unknown repository, where the application does not know whether +the repository is encrypted, the following algorithm can be followed to detect encryption: + +1. Set *BORG_PASSPHRASE* to gibberish (for example a freshly generated UUID4, which cannot + possibly be the passphrase) +2. Invoke ``borg list repository ...`` +3. If this fails, due the repository being encrypted and the passphrase obviously being + wrong, you'll get an error with the *PassphraseWrong* msgid. + + The repository is encrypted, for further access the application will need the passphrase. + +4. If this does not fail, then the repository is not encrypted. + Standard output --------------- -*stdout* is different and more command-dependent. Commands like :ref:`borg_info`, :ref:`borg_create` +*stdout* is different and more command-dependent than logging. Commands like :ref:`borg_info`, :ref:`borg_create` and :ref:`borg_list` implement a ``--json`` option which turns their regular output into a single JSON object. Dates are formatted according to ISO-8601 with the strftime format string '%a, %Y-%m-%d %H:%M:%S', @@ -461,3 +531,15 @@ Operations *info* is one string element, the name of the path currently extracted. - extract.permissions - archive.delete + +Prompts + BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK + For "Warning: Attempting to access a previously unknown unencrypted repository" + BORG_RELOCATED_REPO_ACCESS_IS_OK + For "Warning: The repository at location ... was previously located at ..." + BORG_CHECK_I_KNOW_WHAT_I_AM_DOING + For "Warning: 'check --repair' is an experimental feature that might result in data loss." + BORG_DELETE_I_KNOW_WHAT_I_AM_DOING + For "You requested to completely DELETE the repository *including* all archives it contains:" + BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING + For "recreate is an experimental feature." diff --git a/docs/usage_general.rst.inc b/docs/usage_general.rst.inc index f718b45b..160a4649 100644 --- a/docs/usage_general.rst.inc +++ b/docs/usage_general.rst.inc @@ -117,6 +117,7 @@ Borg can exit with the following return codes (rc): If you use ``--show-rc``, the return code is also logged at the indicated level as the last log entry. +.. _env_vars: Environment Variables ~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/borg/helpers.py b/src/borg/helpers.py index fe82b92f..6a924220 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1307,7 +1307,8 @@ DEFAULTISH = ('Default', 'DEFAULT', 'default', 'D', 'd', '', ) def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, retry_msg=None, invalid_msg=None, env_msg='{} (from {})', falsish=FALSISH, truish=TRUISH, defaultish=DEFAULTISH, - default=False, retry=True, env_var_override=None, ofile=None, input=input, prompt=True): + default=False, retry=True, env_var_override=None, ofile=None, input=input, prompt=True, + msgid=None): """Output (usually a question) and let user input an answer. Qualifies the answer according to falsish, truish and defaultish as True, False or . If it didn't qualify and retry is False (no retries wanted), return the default [which @@ -1337,6 +1338,23 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, :param input: input function [input from builtins] :return: boolean answer value, True or False """ + def output(msg, msg_type, is_prompt=False, **kwargs): + json_output = getattr(logging.getLogger('borg'), 'json', False) + if json_output: + kwargs.update(dict( + type='question_%s' % msg_type, + msgid=msgid, + message=msg, + is_prompt=is_prompt, + )) + print(json.dumps(kwargs), file=sys.stderr) + else: + if is_prompt: + print(msg, file=ofile, end='', flush=True) + else: + print(msg, file=ofile) + + msgid = msgid or env_var_override # note: we do not assign sys.stderr as default above, so it is # really evaluated NOW, not at function definition time. if ofile is None: @@ -1344,13 +1362,13 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, if default not in (True, False): raise ValueError("invalid default value, must be True or False") if msg: - print(msg, file=ofile, end='', flush=True) + output(msg, 'prompt', is_prompt=True) while True: answer = None if env_var_override: answer = os.environ.get(env_var_override) if answer is not None and env_msg: - print(env_msg.format(answer, env_var_override), file=ofile) + output(env_msg.format(answer, env_var_override), 'env_answer', env_var=env_var_override) if answer is None: if not prompt: return default @@ -1361,23 +1379,23 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, answer = truish[0] if default else falsish[0] if answer in defaultish: if default_msg: - print(default_msg, file=ofile) + output(default_msg, 'accepted_default') return default if answer in truish: if true_msg: - print(true_msg, file=ofile) + output(true_msg, 'accepted_true') return True if answer in falsish: if false_msg: - print(false_msg, file=ofile) + output(false_msg, 'accepted_false') return False # if we get here, the answer was invalid if invalid_msg: - print(invalid_msg, file=ofile) + output(invalid_msg, 'invalid_answer') if not retry: return default if retry_msg: - print(retry_msg, file=ofile, end='', flush=True) + output(retry_msg, 'prompt_retry', is_prompt=True) # in case we used an environment variable and it gave an invalid answer, do not use it again: env_var_override = None From e07a289e34a5d293cb981e4d3dce6662df5e8e1b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 3 Mar 2017 23:57:50 +0100 Subject: [PATCH 0717/1387] frontends.rst: it's "null" in json --- docs/internals/frontends.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 5ce72f8d..ef7731f5 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -47,7 +47,7 @@ progress_message operation unique, opaque integer ID of the operation :ref:`msgid ` - Message ID of the operation (may be *none*) + Message ID of the operation (may be *null*) finished boolean indicating whether the operation has finished, only the last object for an *operation* can have this property set to *true*. @@ -60,7 +60,7 @@ progress_percent operation unique, opaque integer ID of the operation :ref:`msgid ` - Message ID of the operation (may be *none*) + Message ID of the operation (may be *null*) finished boolean indicating whether the operation has finished, only the last object for an *operation* can have this property set to *true*. @@ -69,7 +69,7 @@ progress_percent current Current value (always less-or-equal to *total*) info - Array that describes the current item, may be *none*, contents depend on *msgid* + Array that describes the current item, may be *null*, contents depend on *msgid* total Total value @@ -94,7 +94,7 @@ log_message message Formatted log message :ref:`msgid ` - Message ID, may be *none* or absent + Message ID, may be *null* or absent See Prompts_ for the types used by prompts. From cdb4df0885ae81529df1491ec3e825b3c019fafe Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 9 Mar 2017 21:12:07 +0100 Subject: [PATCH 0718/1387] --log-json: time property on most progress/log objects, remove is_prompt --- docs/internals/frontends.rst | 24 ++++++++++++++---------- src/borg/archive.py | 1 + src/borg/helpers.py | 4 ++-- src/borg/logger.py | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index ef7731f5..da6245a2 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -39,6 +39,8 @@ archive_progress Number of (regular) files processed so far path Current path + time + Unix timestamp (float) progress_message A message-based progress information with no concrete progress information, just a message @@ -53,6 +55,8 @@ progress_message can have this property set to *true*. message current progress message (may be empty/absent) + time + Unix timestamp (float) progress_percent Absolute progress information with defined end/total and current value. @@ -72,6 +76,8 @@ progress_percent Array that describes the current item, may be *null*, contents depend on *msgid* total Total value + time + Unix timestamp (float) file_status This is only output by :ref:`borg_create` and :ref:`borg_recreate` if ``--list`` is specified. The usual @@ -85,7 +91,7 @@ file_status log_message Any regular log output invokes this type. Regular log options and filtering applies to these as well. - created + time Unix timestamp (float) levelname Upper-case log level name (also called severity). Defined levels are: DEBUG, INFO, WARNING, ERROR, CRITICAL @@ -138,23 +144,21 @@ Prompts assume a JSON form as well when the ``--log-json`` option is specified. are still read verbatim from *stdin*, while prompts are JSON messages printed to *stderr*, just like log messages. -Prompts use the *question_prompt*, *question_prompt_retry*, *question_invalid_answer*, -*question_accepted_default*, *question_accepted_true*, *question_accepted_false* and -*question_env_answer* types. +Prompts use the *question_prompt* and *question_prompt_retry* types for the prompt itself, +and *question_invalid_answer*, *question_accepted_default*, *question_accepted_true*, +*question_accepted_false* and *question_env_answer* types for information about +prompt processing. The *message* property contains the same string displayed regularly in the same situation, while the *msgid* property may contain a msgid_, typically the name of the environment variable that can be used to override the prompt. It is the same for all JSON messages pertaining to the same prompt. -The *is_prompt* boolean property distinguishes informational messages from prompts, it -is true for *question_prompt* and *question_prompt_retry* types, otherwise it is false. - .. rubric:: Examples (reformatted, each object would be on exactly one line) Providing an invalid answer:: - {"type": "question_prompt", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "is_prompt": true, + {"type": "question_prompt", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "message": "... Type 'YES' if you understand this and want to continue: "} incorrect answer # input on stdin {"type": "question_invalid_answer", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "is_prompt": false, @@ -162,7 +166,7 @@ Providing an invalid answer:: Providing a false (negative) answer:: - {"type": "question_prompt", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "is_prompt": true, + {"type": "question_prompt", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "message": "... Type 'YES' if you understand this and want to continue: "} NO # input on stdin {"type": "question_accepted_false", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", @@ -170,7 +174,7 @@ Providing a false (negative) answer:: Providing a true (affirmative) answer:: - {"type": "question_prompt", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "is_prompt": true, + {"type": "question_prompt", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "message": "... Type 'YES' if you understand this and want to continue: "} YES # input on stdin # no further output, just like the prompt without --log-json diff --git a/src/borg/archive.py b/src/borg/archive.py index 0756730b..7752f516 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -97,6 +97,7 @@ class Statistics: if self.output_json: data = self.as_dict() data.update({ + 'time': time.time(), 'type': 'archive_progress', 'path': remove_surrogates(item.path if item else ''), }) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 6a924220..e147767b 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1345,7 +1345,6 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, type='question_%s' % msg_type, msgid=msgid, message=msg, - is_prompt=is_prompt, )) print(json.dumps(kwargs), file=sys.stderr) else: @@ -1474,7 +1473,8 @@ class ProgressIndicatorBase: operation=self.id, msgid=self.msgid, type=self.JSON_TYPE, - finished=finished + finished=finished, + time=time.time(), )) print(json.dumps(kwargs), file=sys.stderr) diff --git a/src/borg/logger.py b/src/borg/logger.py index 672c1e89..6300776d 100644 --- a/src/borg/logger.py +++ b/src/borg/logger.py @@ -204,7 +204,6 @@ def create_logger(name=None): class JsonFormatter(logging.Formatter): RECORD_ATTRIBUTES = ( - 'created', 'levelname', 'name', 'message', @@ -224,6 +223,7 @@ class JsonFormatter(logging.Formatter): super().format(record) data = { 'type': 'log_message', + 'time': record.created, } for attr in self.RECORD_ATTRIBUTES: value = getattr(record, attr, None) From 55759f76e6a39db21d13b5455ae2ec99cead7860 Mon Sep 17 00:00:00 2001 From: Milkey Mouse Date: Thu, 9 Mar 2017 12:31:04 -0800 Subject: [PATCH 0719/1387] Address SSH batch mode in docs (fixes #2202) (#2270) --- docs/usage.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index 17070d61..0844159c 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -729,3 +729,11 @@ for e.g. regular pruning. Further protections can be implemented, but are outside of Borg's scope. For example, file system snapshots or wrapping ``borg serve`` to set special permissions or ACLs on new data files. + +SSH batch mode +~~~~~~~~~~~~~~ + +When running |project_name| using an automated script, ``ssh`` might still ask for a password, +even if there is an SSH key for the target server. Use this to make scripts more robust:: + + export BORG_RSH='ssh -oBatchMode=yes' From 00f98d8ad1a5789880d9f28dee8cd92714dd4278 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 10 Mar 2017 23:12:45 +0100 Subject: [PATCH 0720/1387] docs/development: update merge remarks --- docs/development.rst | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/development.rst b/docs/development.rst index 2e711a91..ca138fda 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -81,8 +81,20 @@ were collected: 2. Branch a backporting branch off the maintenance branch. 3. Cherry pick and backport the changes from each labelled PR, remove the label for each PR you've backported. + + To preserve authorship metadata, do not follow the ``git cherry-pick`` + instructions to use ``git commit`` after resolving conflicts. Instead, + stage conflict resolutions and run ``git cherry-pick --continue``, + much like using ``git rebase``. + + To avoid merge issues (a cherry pick is a form of merge), use + these options (similar to the ``git merge`` options used previously, + the ``-x`` option adds a reference to the original commit):: + + git cherry-pick --strategy recursive -X rename-threshold=5% -x + 4. Make a PR of the backporting branch against the maintenance branch - for backport review. Mention the backported PRs in this PR, eg: + for backport review. Mention the backported PRs in this PR, e.g.: Includes changes from #2055 #2057 #2381 @@ -274,15 +286,6 @@ If you encounter issues, see also our `Vagrantfile` for details. without external dependencies. -Merging maintenance branches ----------------------------- - -As mentioned above bug fixes will usually be merged into a maintenance branch (x.y-maint) and then -merged back into the master branch. Large diffs between these branches can make automatic merges troublesome, -therefore we recommend to use these merge parameters:: - - git merge 1.0-maint -s recursive -X rename-threshold=20% - .. _releasing: Creating a new release From a842001385f8d48046da2c7bfc1f406aa00e52cc Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 11 Mar 2017 00:00:25 +0100 Subject: [PATCH 0721/1387] fix error msg, it is --keep-within, not --within --- src/borg/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index e147767b..dc4d3485 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -301,9 +301,9 @@ def prune_within(archives, within): hours = int(within[:-1]) * multiplier[within[-1]] except (KeyError, ValueError): # I don't like how this displays the original exception too: - raise argparse.ArgumentTypeError('Unable to parse --within option: "%s"' % within) + raise argparse.ArgumentTypeError('Unable to parse --keep-within option: "%s"' % within) if hours <= 0: - raise argparse.ArgumentTypeError('Number specified using --within option must be positive') + raise argparse.ArgumentTypeError('Number specified using --keep-within option must be positive') target = datetime.now(timezone.utc) - timedelta(seconds=hours * 3600) return [a for a in archives if a.ts > target] From 23f6a82f1bcc1e6d4bc57a736f9b792497494768 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 11 Mar 2017 05:39:30 +0100 Subject: [PATCH 0722/1387] fix borg key/debug/benchmark crashing without subcommand, fixes #2240 --- src/borg/archiver.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index cb4350a1..931933bc 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -3334,10 +3334,11 @@ class Archiver: args = self.preprocess_args(args) parser = self.build_parser() args = parser.parse_args(args or ['-h']) - if args.func == self.do_create: + # This works around http://bugs.python.org/issue9351 + func = getattr(args, 'func', None) or getattr(args, 'fallback_func') + if func == self.do_create and not args.paths: # need at least 1 path but args.paths may also be populated from patterns - if not args.paths: - parser.error('Need at least one PATH argument.') + parser.error('Need at least one PATH argument.') return args def prerun_checks(self, logger): From 21178617381302abb30ee4afcef72039bdca166d Mon Sep 17 00:00:00 2001 From: Milkey Mouse Date: Wed, 8 Mar 2017 22:38:37 -0800 Subject: [PATCH 0723/1387] Securely erase config file, fixes #2257 The SaveFile code, while ensuring atomicity, did not allow for secure erasure of the config file (containing the old encrypted key). Now it creates a hardlink to the file, lets SaveFile do its thing, and writes random data over the old file (via the hardlink). A secure erase is needed because the config file can contain the old key after changing one's password. --- src/borg/helpers.py | 10 ++++++++++ src/borg/repository.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index dc4d3485..9c112d11 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -2256,3 +2256,13 @@ def json_dump(obj): def json_print(obj): print(json_dump(obj)) + + +def secure_erase(path): + """Attempt to securely erase a file by writing random data over it before deleting it.""" + with open(path, 'r+b') as fd: + length = os.stat(fd.fileno()).st_size + fd.write(os.urandom(length)) + fd.flush() + os.fsync(fd.fileno()) + os.unlink(path) diff --git a/src/borg/repository.py b/src/borg/repository.py index 41d865d5..d7b84ab3 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -18,6 +18,7 @@ from .helpers import Location from .helpers import ProgressIndicatorPercent from .helpers import bin_to_hex from .helpers import yes +from .helpers import secure_erase from .locking import Lock, LockError, LockErrorT from .logger import create_logger from .lrucache import LRUCache @@ -182,9 +183,27 @@ class Repository: def save_config(self, path, config): config_path = os.path.join(path, 'config') + old_config_path = os.path.join(path, 'config.old') + + if os.path.isfile(old_config_path): + logger.warning("Old config file not securely erased on previous config update") + secure_erase(old_config_path) + + if os.path.isfile(config_path): + try: + os.link(config_path, old_config_path) + except OSError as e: + if e.errno in (errno.EMLINK, errno.EPERM): + logger.warning("Hardlink failed, cannot securely erase old config file") + else: + raise + with SaveFile(config_path) as fd: config.write(fd) + if os.path.isfile(old_config_path): + secure_erase(old_config_path) + def save_key(self, keydata): assert self.config keydata = keydata.decode('utf-8') # remote repo: msgpack issue #99, getting bytes From 86867e51711c2cf099b5d6b50eb29fb6f2c6b8b2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 13 Mar 2017 10:04:45 +0100 Subject: [PATCH 0724/1387] docs/security: counter tracking Copied from #2266 --- docs/internals/security.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/internals/security.rst b/docs/internals/security.rst index 5d36cb60..617bf90b 100644 --- a/docs/internals/security.rst +++ b/docs/internals/security.rst @@ -167,6 +167,36 @@ Decryption:: ASSERT( CONSTANT-TIME-COMPARISON( chunk-id, AUTHENTICATOR(id_key, decompressed) ) ) +The client needs to track which counter values have been used, since +encrypting a chunk requires a starting counter value and no two chunks +may have overlapping counter ranges (otherwise the bitwise XOR of the +overlapping plaintexts is revealed). + +The client does not directly track the counter value, because it +changes often (with each encrypted chunk), instead it commits a +"reservation" to the security database and the repository by taking +the current counter value and adding 4 GiB / 16 bytes (the block size) +to the counter. Thus the client only needs to commit a new reservation +every few gigabytes of encrypted data. + +This mechanism also avoids reusing counter values in case the client +crashes or the connection to the repository is severed, since any +reservation would have been committed to both the security database +and the repository before any data is encrypted. Borg uses its +standard mechanism (SaveFile) to ensure that reservations are durable +(on most hardware / storage systems), therefore a crash of the +client's host would not impact tracking of reservations. + +However, this design is not infallible, and requires synchronization +between clients, which is handled through the repository. Therefore in +a multiple-client scenario a repository can trick a client into +reusing counter values by ignoring counter reservations and replaying +the manifest (which will fail if the client has seen a more recent +manifest or has a more recent nonce reservation). If the repository is +untrusted, but a trusted synchronization channel exists between +clients, the security database could be synchronized between them over +said trusted channel. This is not part of Borgs functionality. + .. [#] Using the :ref:`borg key migrate-to-repokey ` command a user can convert repositories created using Attic in "passphrase" mode to "repokey" mode. In this case the keys were directly derived from From d1738ec31507f55a710b106d09b66761b3a977ba Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 13 Mar 2017 10:16:08 +0100 Subject: [PATCH 0725/1387] docs/security: reiterate that RPC in Borg does no networking --- docs/internals/security.rst | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/internals/security.rst b/docs/internals/security.rst index 617bf90b..978338b7 100644 --- a/docs/internals/security.rst +++ b/docs/internals/security.rst @@ -285,9 +285,21 @@ over an encrypted SSH channel (the system's SSH client is used for this by piping data from/to it). This means that the authorization and transport security properties -are inherited from SSH and the configuration of the SSH client -and the SSH server. Therefore the remainder of this section -will focus on the security of the RPC protocol within Borg. +are inherited from SSH and the configuration of the SSH client and the +SSH server -- Borg RPC does not contain *any* networking +code. Networking is done by the SSH client running in a separate +process, Borg only communicates over the standard pipes (stdout, +stderr and stdin) with this process. This also means that Borg doesn't +have to directly use a SSH client (or SSH at all). For example, +``sudo`` or ``qrexec`` could be used as an intermediary. + +By using the system's SSH client and not implementing a +(cryptographic) network protocol Borg sidesteps many security issues +that would normally impact distributing statically linked / standalone +binaries. + +The remainder of this section will focus on the security of the RPC +protocol within Borg. The assumed worst-case a server can inflict to a client is a denial of repository service. From b287fba22e142ba8010f06c1803209adba5c5bdc Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 15 Mar 2017 01:22:21 +0100 Subject: [PATCH 0726/1387] fix caskroom link, fixes #2299 --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index c4a1b84c..1d2c316a 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -70,7 +70,7 @@ Ubuntu `Ubuntu packages`_, `Ubuntu PPA`_ ``apt install borgbac .. _OpenBSD ports: http://cvsweb.openbsd.org/cgi-bin/cvsweb/ports/sysutils/borgbackup/ .. _OpenIndiana hipster repository: http://pkg.openindiana.org/hipster/en/search.shtml?token=borg&action=Search .. _openSUSE official repository: http://software.opensuse.org/package/borgbackup -.. _Brew cask: http://caskroom.io/ +.. _Brew cask: https://caskroom.github.io/ .. _Raspbian testing: http://archive.raspbian.org/raspbian/pool/main/b/borgbackup/ .. _Ubuntu packages: http://packages.ubuntu.com/xenial/borgbackup .. _Ubuntu PPA: https://launchpad.net/~costamagnagianfranco/+archive/ubuntu/borgbackup From 883a7eefb2d0301cd0e178ccd2da8480b2e1ac95 Mon Sep 17 00:00:00 2001 From: enkore Date: Wed, 15 Mar 2017 17:08:07 +0100 Subject: [PATCH 0727/1387] Archive: allocate zeros when needed (#2308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes huge memory usage of mount (8 MiB × number of archives) --- src/borg/archive.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 7752f516..216459e2 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -325,7 +325,7 @@ class Archive: if info is None: raise self.DoesNotExist(name) self.load(info.id) - self.zeros = b'\0' * (1 << chunker_params[1]) + self.zeros = None def _load_meta(self, id): _, data = self.key.decrypt(id, self.repository.get(id)) @@ -578,6 +578,8 @@ Utilization of max. archive size: {csize_max:.0%} # Extract chunks, since the item which had the chunks was not extracted with backup_io('open'): fd = open(path, 'wb') + if sparse and self.zeros is None: + self.zeros = b'\0' * (1 << self.chunker_params[1]) with fd: ids = [c.id for c in item.chunks] for _, data in self.pipeline.fetch_many(ids, is_preloaded=True): From 988e452bc0796b941b33f9af79a300e5f9fcca74 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 16 Mar 2017 15:13:03 +0100 Subject: [PATCH 0728/1387] faq: mention --remote-ratelimit in bandwidth limit question --- docs/faq.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index a56c87c3..48596066 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -441,8 +441,12 @@ If the directory where you mount a filesystem is different every time, Is there a way to limit bandwidth with |project_name|? ------------------------------------------------------ -There is no command line option to limit bandwidth with |project_name|, but -bandwidth limiting can be accomplished with pipeviewer_: +To limit upload (i.e. :ref:`borg_create`) bandwidth, use the +``--remote-ratelimit`` option. + +There is no built-in way to limit *download* +(i.e. :ref:`borg_extract`) bandwidth, but limiting download bandwidth +can be accomplished with pipeviewer_: Create a wrapper script: /usr/local/bin/pv-wrapper :: From 42371181fcd296dad404c6b6e92e3108fb1793c7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 11 Mar 2017 04:13:40 +0100 Subject: [PATCH 0729/1387] support switching the pattern style default in patterns file --- src/borg/archiver.py | 4 +++- src/borg/helpers.py | 37 ++++++++++++++++++++++++----------- src/borg/testsuite/helpers.py | 27 +++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 931933bc..ce6c2bf2 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1663,7 +1663,7 @@ class Archiver: Note that the default pattern style for `--pattern` and `--patterns-from` is shell style (`sh:`), so those patterns behave similar to rsync include/exclude - patterns. + patterns. The pattern style can be set via the `P` prefix. Patterns (`--pattern`) and excludes (`--exclude`) from the command line are considered first (in the order of appearance). Then patterns from `--patterns-from` @@ -1671,6 +1671,8 @@ class Archiver: An example `--patterns-from` file could look like that:: + # "sh:" pattern style is the default, so the following line is not needed: + P sh R / # can be rebuild - /home/*/.cache diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 9c112d11..db826d9f 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -391,18 +391,23 @@ def parse_timestamp(timestamp): return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc) -def parse_add_pattern(patternstr, roots, patterns): +def parse_add_pattern(patternstr, roots, patterns, fallback): """Parse a pattern string and add it to roots or patterns depending on the pattern type.""" - pattern = parse_inclexcl_pattern(patternstr) + pattern = parse_inclexcl_pattern(patternstr, fallback=fallback) if pattern.ptype is RootPath: roots.append(pattern.pattern) + elif pattern.ptype is PatternStyle: + fallback = pattern.pattern else: patterns.append(pattern) + return fallback -def load_pattern_file(fileobj, roots, patterns): +def load_pattern_file(fileobj, roots, patterns, fallback=None): + if fallback is None: + fallback = ShellPattern # ShellPattern is defined later in this module for patternstr in clean_lines(fileobj): - parse_add_pattern(patternstr, roots, patterns) + fallback = parse_add_pattern(patternstr, roots, patterns, fallback) def load_exclude_file(fileobj, patterns): @@ -415,7 +420,7 @@ class ArgparsePatternAction(argparse.Action): super().__init__(nargs=nargs, **kw) def __call__(self, parser, args, values, option_string=None): - parse_add_pattern(values[0], args.paths, args.patterns) + parse_add_pattern(values[0], args.paths, args.patterns, ShellPattern) class ArgparsePatternFileAction(argparse.Action): @@ -614,6 +619,14 @@ _PATTERN_STYLE_BY_PREFIX = dict((i.PREFIX, i) for i in _PATTERN_STYLES) InclExclPattern = namedtuple('InclExclPattern', 'pattern ptype') RootPath = object() +PatternStyle = object() + + +def get_pattern_style(prefix): + try: + return _PATTERN_STYLE_BY_PREFIX[prefix] + except KeyError: + raise ValueError("Unknown pattern style: {}".format(prefix)) from None def parse_pattern(pattern, fallback=FnmatchPattern): @@ -621,14 +634,9 @@ def parse_pattern(pattern, fallback=FnmatchPattern): """ if len(pattern) > 2 and pattern[2] == ":" and pattern[:2].isalnum(): (style, pattern) = (pattern[:2], pattern[3:]) - - cls = _PATTERN_STYLE_BY_PREFIX.get(style, None) - - if cls is None: - raise ValueError("Unknown pattern style: {}".format(style)) + cls = get_pattern_style(style) else: cls = fallback - return cls(pattern) @@ -646,6 +654,8 @@ def parse_inclexcl_pattern(pattern, fallback=ShellPattern): '+': True, 'R': RootPath, 'r': RootPath, + 'P': PatternStyle, + 'p': PatternStyle, } try: ptype = type_prefix_map[pattern[0]] @@ -656,6 +666,11 @@ def parse_inclexcl_pattern(pattern, fallback=ShellPattern): raise argparse.ArgumentTypeError("Unable to parse pattern: {}".format(pattern)) if ptype is RootPath: pobj = pattern + elif ptype is PatternStyle: + try: + pobj = get_pattern_style(pattern) + except ValueError: + raise argparse.ArgumentTypeError("Unable to parse pattern: {}".format(pattern)) else: pobj = parse_pattern(pattern, fallback) return InclExclPattern(pobj, ptype) diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 57bf0175..4210cddd 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -1,5 +1,6 @@ import argparse import hashlib +import io import os import sys from datetime import datetime, timezone, timedelta @@ -496,6 +497,32 @@ def test_load_patterns_from_file(tmpdir, lines, expected_roots, expected_numpatt assert numpatterns == expected_numpatterns +def test_switch_patterns_style(): + patterns = """\ + +0_initial_default_is_shell + p fm + +1_fnmatch + P re + +2_regex + +3_more_regex + P pp + +4_pathprefix + p fm + p sh + +5_shell + """ + pattern_file = io.StringIO(patterns) + roots, patterns = [], [] + load_pattern_file(pattern_file, roots, patterns) + assert len(patterns) == 6 + assert isinstance(patterns[0].pattern, ShellPattern) + assert isinstance(patterns[1].pattern, FnmatchPattern) + assert isinstance(patterns[2].pattern, RegexPattern) + assert isinstance(patterns[3].pattern, RegexPattern) + assert isinstance(patterns[4].pattern, PathPrefixPattern) + assert isinstance(patterns[5].pattern, ShellPattern) + + @pytest.mark.parametrize("lines", [ (["X /data"]), # illegal pattern type prefix (["/data"]), # need a pattern type prefix From b7a17a6db76283c9d9d6f246831c3010fbb35fe2 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 15 Mar 2017 18:54:34 +0100 Subject: [PATCH 0730/1387] clamp (nano)second values to unproblematic range, fixes #2304 filesystem -> clamp -> archive (create) --- src/borg/archive.py | 7 ++++--- src/borg/cache.py | 6 ++++-- src/borg/helpers.py | 34 ++++++++++++++++++++++++++++------ src/borg/testsuite/helpers.py | 16 ++++++++++++++++ 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 7752f516..fbcfe523 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -33,6 +33,7 @@ from .helpers import format_time, format_timedelta, format_file_size, file_statu from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates from .helpers import StableDict from .helpers import bin_to_hex +from .helpers import safe_ns from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi from .helpers import PathPrefixPattern, FnmatchPattern from .helpers import CompressionDecider1, CompressionDecider2, CompressionSpec @@ -784,15 +785,15 @@ Utilization of max. archive size: {csize_max:.0%} mode=st.st_mode, uid=st.st_uid, gid=st.st_gid, - mtime=st.st_mtime_ns, + mtime=safe_ns(st.st_mtime_ns), ) # borg can work with archives only having mtime (older attic archives do not have # atime/ctime). it can be useful to omit atime/ctime, if they change without the # file content changing - e.g. to get better metadata deduplication. if not self.noatime: - attrs['atime'] = st.st_atime_ns + attrs['atime'] = safe_ns(st.st_atime_ns) if not self.noctime: - attrs['ctime'] = st.st_ctime_ns + attrs['ctime'] = safe_ns(st.st_ctime_ns) if self.numeric_owner: attrs['user'] = attrs['group'] = None else: diff --git a/src/borg/cache.py b/src/borg/cache.py index 56e16560..2b6082b3 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -17,6 +17,7 @@ from .helpers import Error from .helpers import get_cache_dir, get_security_dir from .helpers import bin_to_hex from .helpers import format_file_size +from .helpers import safe_ns from .helpers import yes from .helpers import remove_surrogates from .helpers import ProgressIndicatorPercent, ProgressIndicatorMessage @@ -591,6 +592,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def memorize_file(self, path_hash, st, ids): if not (self.do_files and stat.S_ISREG(st.st_mode)): return - entry = FileCacheEntry(age=0, inode=st.st_ino, size=st.st_size, mtime=st.st_mtime_ns, chunk_ids=ids) + mtime_ns = safe_ns(st.st_mtime_ns) + entry = FileCacheEntry(age=0, inode=st.st_ino, size=st.st_size, mtime=mtime_ns, chunk_ids=ids) self.files[path_hash] = msgpack.packb(entry) - self._newest_mtime = max(self._newest_mtime or 0, st.st_mtime_ns) + self._newest_mtime = max(self._newest_mtime or 0, mtime_ns) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 9c112d11..a491ec65 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -665,7 +665,7 @@ def timestamp(s): """Convert a --timestamp=s argument to a datetime object""" try: # is it pointing to a file / directory? - ts = os.stat(s).st_mtime + ts = safe_s(os.stat(s).st_mtime) return datetime.utcfromtimestamp(ts) except OSError: # didn't work, try parsing as timestamp. UTC, no TZ, no microsecs support. @@ -818,12 +818,34 @@ def SortBySpec(text): return text.replace('timestamp', 'ts') +# Not too rarely, we get crappy timestamps from the fs, that overflow some computations. +# As they are crap anyway, nothing is lost if we just clamp them to the max valid value. +# msgpack can only pack uint64. datetime is limited to year 9999. +MAX_NS = 18446744073000000000 # less than 2**64 - 1 ns. also less than y9999. +MAX_S = MAX_NS // 1000000000 + + +def safe_s(ts): + if 0 <= ts <= MAX_S: + return ts + elif ts < 0: + return 0 + else: + return MAX_S + + +def safe_ns(ts): + if 0 <= ts <= MAX_NS: + return ts + elif ts < 0: + return 0 + else: + return MAX_NS + + def safe_timestamp(item_timestamp_ns): - try: - return datetime.fromtimestamp(item_timestamp_ns / 1e9) - except OverflowError: - # likely a broken file time and datetime did not want to go beyond year 9999 - return datetime(9999, 12, 31, 23, 59, 59) + t_ns = safe_ns(item_timestamp_ns) + return datetime.fromtimestamp(t_ns / 1e9) def format_time(t): diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 57bf0175..17ca66de 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -27,6 +27,7 @@ from ..helpers import CompressionSpec, CompressionDecider1, CompressionDecider2 from ..helpers import parse_pattern, PatternMatcher, RegexPattern, PathPrefixPattern, FnmatchPattern, ShellPattern from ..helpers import swidth_slice from ..helpers import chunkit +from ..helpers import safe_ns, safe_s from . import BaseTestCase, FakeInputs @@ -1221,3 +1222,18 @@ def test_swidth_slice_mixed_characters(): string = '나윤a선나윤선나윤선나윤선나윤선' assert swidth_slice(string, 5) == '나윤a' assert swidth_slice(string, 6) == '나윤a' + + +def test_safe_timestamps(): + # ns fit into uint64 + assert safe_ns(2 ** 64) < 2 ** 64 + assert safe_ns(-1) == 0 + # s are so that their ns conversion fits into uint64 + assert safe_s(2 ** 64) * 1000000000 < 2 ** 64 + assert safe_s(-1) == 0 + # datetime won't fall over its y10k problem + beyond_y10k = 2 ** 100 + with pytest.raises(OverflowError): + datetime.utcfromtimestamp(beyond_y10k) + assert datetime.utcfromtimestamp(safe_s(beyond_y10k)) > datetime(2500, 12, 31) + assert datetime.utcfromtimestamp(safe_ns(beyond_y10k) / 1000000000) > datetime(2500, 12, 31) From 04dba76fc9761cc71f57a47ff6fdb45808611208 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 21 Mar 2017 02:05:38 +0100 Subject: [PATCH 0731/1387] Mostly revert "clean imports, remove unused code" This reverts commit b7eaeee26631f145a2b7d73f73f20fb56314c3a8. We still need the bigint stuff for compatibility to borg 1.0 archives. # Conflicts: # src/borg/archive.py # src/borg/archiver.py # src/borg/helpers.py # src/borg/key.py --- src/borg/archive.py | 1 + src/borg/helpers.py | 20 +++++++++++++++++++- src/borg/testsuite/helpers.py | 14 +++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 94cd482a..c1d4d809 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -34,6 +34,7 @@ from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates from .helpers import StableDict from .helpers import bin_to_hex from .helpers import safe_ns +from .helpers import int_to_bigint, bigint_to_int from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi from .helpers import PathPrefixPattern, FnmatchPattern from .helpers import CompressionDecider1, CompressionDecider2, CompressionSpec diff --git a/src/borg/helpers.py b/src/borg/helpers.py index c04da8b6..e2949c9d 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -859,7 +859,7 @@ def safe_ns(ts): def safe_timestamp(item_timestamp_ns): - t_ns = safe_ns(item_timestamp_ns) + t_ns = safe_ns(bigint_to_int(item_timestamp_ns)) return datetime.fromtimestamp(t_ns / 1e9) @@ -1332,6 +1332,24 @@ class StableDict(dict): return sorted(super().items()) +def bigint_to_int(mtime): + """Convert bytearray to int + """ + if isinstance(mtime, bytes): + return int.from_bytes(mtime, 'little', signed=True) + return mtime + + +def int_to_bigint(value): + """Convert integers larger than 64 bits to bytearray + + Smaller integers are left alone + """ + if value.bit_length() > 63: + return value.to_bytes((value.bit_length() + 9) // 8, 'little', signed=True) + return value + + def is_slow_msgpack(): return msgpack.Packer is msgpack.fallback.Packer diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 02ed0b38..2dcf287a 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -20,7 +20,7 @@ from ..helpers import prune_within, prune_split from ..helpers import get_cache_dir, get_keys_dir, get_security_dir from ..helpers import is_slow_msgpack from ..helpers import yes, TRUISH, FALSISH, DEFAULTISH -from ..helpers import StableDict, bin_to_hex +from ..helpers import StableDict, int_to_bigint, bigint_to_int, bin_to_hex from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams, Chunk from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless from ..helpers import load_exclude_file, load_pattern_file @@ -33,6 +33,18 @@ from ..helpers import safe_ns, safe_s from . import BaseTestCase, FakeInputs +class BigIntTestCase(BaseTestCase): + + def test_bigint(self): + self.assert_equal(int_to_bigint(0), 0) + self.assert_equal(int_to_bigint(2**63-1), 2**63-1) + self.assert_equal(int_to_bigint(-2**63+1), -2**63+1) + self.assert_equal(int_to_bigint(2**63), b'\x00\x00\x00\x00\x00\x00\x00\x80\x00') + self.assert_equal(int_to_bigint(-2**63), b'\x00\x00\x00\x00\x00\x00\x00\x80\xff') + self.assert_equal(bigint_to_int(int_to_bigint(-2**70)), -2**70) + self.assert_equal(bigint_to_int(int_to_bigint(2**70)), 2**70) + + def test_bin_to_hex(): assert bin_to_hex(b'') == '' assert bin_to_hex(b'\x00\x01\xff') == '0001ff' From f7081837438da6652ed1c1056303ecf6ae6625da Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 21 Mar 2017 02:21:32 +0100 Subject: [PATCH 0732/1387] Revert "don't do "bigint" conversion for nanosecond mtime" This reverts commit 8b2e7ec68099fc85ac7298d462db14b5f0ee7486. We still need the bigint stuff for borg 1.0 compatibility. # Conflicts: # src/borg/cache.py --- src/borg/cache.py | 8 ++++---- src/borg/item.pyx | 7 ++++--- src/borg/testsuite/item.py | 11 +++++++++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 2b6082b3..79c0a497 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -15,7 +15,7 @@ from .hashindex import ChunkIndex, ChunkIndexEntry from .helpers import Location from .helpers import Error from .helpers import get_cache_dir, get_security_dir -from .helpers import bin_to_hex +from .helpers import int_to_bigint, bigint_to_int, bin_to_hex from .helpers import format_file_size from .helpers import safe_ns from .helpers import yes @@ -354,7 +354,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" # this is to avoid issues with filesystem snapshots and mtime granularity. # Also keep files from older backups that have not reached BORG_FILES_CACHE_TTL yet. entry = FileCacheEntry(*msgpack.unpackb(item)) - if entry.age == 0 and entry.mtime < self._newest_mtime or \ + if entry.age == 0 and bigint_to_int(entry.mtime) < self._newest_mtime or \ entry.age > 0 and entry.age < ttl: msgpack.pack((path_hash, entry), fd) pi.output('Saving cache config') @@ -574,7 +574,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if not entry: return None entry = FileCacheEntry(*msgpack.unpackb(entry)) - if (entry.size == st.st_size and entry.mtime == st.st_mtime_ns and + if (entry.size == st.st_size and bigint_to_int(entry.mtime) == st.st_mtime_ns and (ignore_inode or entry.inode == st.st_ino)): # we ignored the inode number in the comparison above or it is still same. # if it is still the same, replacing it in the tuple doesn't change it. @@ -593,6 +593,6 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if not (self.do_files and stat.S_ISREG(st.st_mode)): return mtime_ns = safe_ns(st.st_mtime_ns) - entry = FileCacheEntry(age=0, inode=st.st_ino, size=st.st_size, mtime=mtime_ns, chunk_ids=ids) + entry = FileCacheEntry(age=0, inode=st.st_ino, size=st.st_size, mtime=int_to_bigint(mtime_ns), chunk_ids=ids) self.files[path_hash] = msgpack.packb(entry) self._newest_mtime = max(self._newest_mtime or 0, mtime_ns) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 627ffd1f..6404a130 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -2,6 +2,7 @@ from collections import namedtuple from .constants import ITEM_KEYS from .helpers import safe_encode, safe_decode +from .helpers import bigint_to_int, int_to_bigint from .helpers import StableDict API_VERSION = '1.1_02' @@ -156,9 +157,9 @@ class Item(PropDict): rdev = PropDict._make_property('rdev', int) bsdflags = PropDict._make_property('bsdflags', int) - atime = PropDict._make_property('atime', int) - ctime = PropDict._make_property('ctime', int) - mtime = PropDict._make_property('mtime', int) + atime = PropDict._make_property('atime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int) + ctime = PropDict._make_property('ctime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int) + mtime = PropDict._make_property('mtime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int) # size is only present for items with a chunk list and then it is sum(chunk_sizes) # compatibility note: this is a new feature, in old archives size will be missing. diff --git a/src/borg/testsuite/item.py b/src/borg/testsuite/item.py index 9c66b6a6..4dedcdc1 100644 --- a/src/borg/testsuite/item.py +++ b/src/borg/testsuite/item.py @@ -77,6 +77,17 @@ def test_item_int_property(): item.mode = "invalid" +def test_item_bigint_property(): + item = Item() + small, big = 42, 2 ** 65 + item.atime = small + assert item.atime == small + assert item.as_dict() == {'atime': small} + item.atime = big + assert item.atime == big + assert item.as_dict() == {'atime': b'\0' * 8 + b'\x02'} + + def test_item_user_group_none(): item = Item() item.user = None From 3665cc3024dd2e9f288b2313dbd6839c6db04150 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 21 Mar 2017 02:27:20 +0100 Subject: [PATCH 0733/1387] bigint conversion: add compatibility note --- src/borg/item.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 6404a130..642b7b26 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -157,6 +157,7 @@ class Item(PropDict): rdev = PropDict._make_property('rdev', int) bsdflags = PropDict._make_property('bsdflags', int) + # note: we need to keep the bigint conversion for compatibility with borg 1.0 archives. atime = PropDict._make_property('atime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int) ctime = PropDict._make_property('ctime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int) mtime = PropDict._make_property('mtime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int) From b27cc37e85ca0511a20799c40ca89b339c2d2a33 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 21 Mar 2017 02:40:50 +0100 Subject: [PATCH 0734/1387] safe_timestamp: arg is always an int the Item object already does the bigint_to_int decode when accessing .mtime/.atime/.ctime --- src/borg/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index e2949c9d..dcbe49bb 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -859,7 +859,7 @@ def safe_ns(ts): def safe_timestamp(item_timestamp_ns): - t_ns = safe_ns(bigint_to_int(item_timestamp_ns)) + t_ns = safe_ns(item_timestamp_ns) return datetime.fromtimestamp(t_ns / 1e9) From 1b008f725cb0e08844608bf11f61bcbbb49a40ee Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 21 Mar 2017 02:42:51 +0100 Subject: [PATCH 0735/1387] fixup: remove unneeded imports --- src/borg/archive.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index c1d4d809..94cd482a 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -34,7 +34,6 @@ from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates from .helpers import StableDict from .helpers import bin_to_hex from .helpers import safe_ns -from .helpers import int_to_bigint, bigint_to_int from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi from .helpers import PathPrefixPattern, FnmatchPattern from .helpers import CompressionDecider1, CompressionDecider2, CompressionSpec From 2414cd4df794ccfde55f294e568711e56e629284 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 23 Mar 2017 23:57:30 +0100 Subject: [PATCH 0736/1387] use immutable data structure for the compression spec, fixes #2331 the bug was compr_args.update(compr_spec), helpers.py:2168 - that mutated the compression spec dict (and not just some local one, but the compr spec dict parsed from the commandline args). so a change that was intended just for 1 chunk changed the desired compression level on the archive scope. I refactored the stuff to use a namedtuple (which is immutable, so such effects can not happen again). --- src/borg/archive.py | 4 ++-- src/borg/archiver.py | 4 ++-- src/borg/helpers.py | 17 ++++++++++------- src/borg/key.py | 2 +- src/borg/testsuite/helpers.py | 34 +++++++++++++++++----------------- 5 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 94cd482a..6efc304b 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -953,7 +953,7 @@ Utilization of max. archive size: {csize_max:.0%} item.chunks = chunks else: compress = self.compression_decider1.decide(path) - self.file_compression_logger.debug('%s -> compression %s', path, compress['name']) + self.file_compression_logger.debug('%s -> compression %s', path, compress.name) with backup_io('open'): fh = Archive._open_rb(path) with os.fdopen(fh, 'rb') as fd: @@ -1651,7 +1651,7 @@ class ArchiveRecreater: if self.recompress and not self.always_recompress and chunk_id in self.cache.chunks: # Check if this chunk is already compressed the way we want it old_chunk = self.key.decrypt(None, self.repository.get(chunk_id), decompress=False) - if Compressor.detect(old_chunk.data).name == compression_spec['name']: + if Compressor.detect(old_chunk.data).name == compression_spec.name: # Stored chunk has the same compression we wanted overwrite = False chunk_entry = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index ce6c2bf2..ff448bd6 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -37,7 +37,7 @@ from .constants import * # NOQA from .crc32 import crc32 from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from .helpers import Error, NoManifestError, set_ec -from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec +from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec, ComprSpec from .helpers import PrefixSpec, SortBySpec, HUMAN_SORT_KEYS from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter from .helpers import format_time, format_timedelta, format_file_size, format_archive @@ -2394,7 +2394,7 @@ class Archiver: help='specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, ' 'HASH_MASK_BITS, HASH_WINDOW_SIZE). default: %d,%d,%d,%d' % CHUNKER_PARAMS) archive_group.add_argument('-C', '--compression', dest='compression', - type=CompressionSpec, default=dict(name='lz4'), metavar='COMPRESSION', + type=CompressionSpec, default=ComprSpec(name='lz4', spec=None), metavar='COMPRESSION', help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') archive_group.add_argument('--compression-from', dest='compression_files', diff --git a/src/borg/helpers.py b/src/borg/helpers.py index dcbe49bb..fc472965 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -705,6 +705,9 @@ def ChunkerParams(s): return int(chunk_min), int(chunk_max), int(chunk_mask), int(window_size) +ComprSpec = namedtuple('ComprSpec', ('name', 'spec')) + + def CompressionSpec(s): values = s.split(',') count = len(values) @@ -713,7 +716,7 @@ def CompressionSpec(s): # --compression algo[,level] name = values[0] if name in ('none', 'lz4', ): - return dict(name=name) + return ComprSpec(name=name, spec=None) if name in ('zlib', 'lzma', ): if count < 2: level = 6 # default compression level in py stdlib @@ -723,13 +726,13 @@ def CompressionSpec(s): raise ValueError else: raise ValueError - return dict(name=name, level=level) + return ComprSpec(name=name, spec=level) if name == 'auto': if 2 <= count <= 3: compression = ','.join(values[1:]) else: raise ValueError - return dict(name=name, spec=CompressionSpec(compression)) + return ComprSpec(name=name, spec=CompressionSpec(compression)) raise ValueError @@ -2147,7 +2150,7 @@ class CompressionDecider2: # if we compress the data here to decide, we can even update the chunk data # and modify the metadata as desired. compr_spec = chunk.meta.get('compress', self.compression) - if compr_spec['name'] == 'auto': + if compr_spec.name == 'auto': # we did not decide yet, use heuristic: compr_spec, chunk = self.heuristic_lz4(compr_spec, chunk) return compr_spec, chunk @@ -2160,14 +2163,14 @@ class CompressionDecider2: data_len = len(data) cdata_len = len(cdata) if cdata_len < data_len: - compr_spec = compr_args['spec'] + compr_spec = compr_args.spec else: # uncompressible - we could have a special "uncompressible compressor" # that marks such data as uncompressible via compression-type metadata. compr_spec = CompressionSpec('none') - compr_args.update(compr_spec) self.logger.debug("len(data) == %d, len(lz4(data)) == %d, choosing %s", data_len, cdata_len, compr_spec) - return compr_args, Chunk(data, **meta) + meta['compress'] = compr_spec + return compr_spec, Chunk(data, **meta) class ErrorIgnoringTextIOWrapper(io.TextIOWrapper): diff --git a/src/borg/key.py b/src/borg/key.py index 2b8d8d64..3d3cfc53 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -153,7 +153,7 @@ class KeyBase: def compress(self, chunk): compr_args, chunk = self.compression_decider2.decide(chunk) - compressor = Compressor(**compr_args) + compressor = Compressor(name=compr_args.name, level=compr_args.spec) meta, data = chunk data = compressor.compress(data) return Chunk(data, **meta) diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 2dcf287a..727f1628 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -24,7 +24,7 @@ from ..helpers import StableDict, int_to_bigint, bigint_to_int, bin_to_hex from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams, Chunk from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless from ..helpers import load_exclude_file, load_pattern_file -from ..helpers import CompressionSpec, CompressionDecider1, CompressionDecider2 +from ..helpers import CompressionSpec, ComprSpec, CompressionDecider1, CompressionDecider2 from ..helpers import parse_pattern, PatternMatcher, RegexPattern, PathPrefixPattern, FnmatchPattern, ShellPattern from ..helpers import swidth_slice from ..helpers import chunkit @@ -671,16 +671,16 @@ def test_pattern_matcher(): def test_compression_specs(): with pytest.raises(ValueError): CompressionSpec('') - assert CompressionSpec('none') == dict(name='none') - assert CompressionSpec('lz4') == dict(name='lz4') - assert CompressionSpec('zlib') == dict(name='zlib', level=6) - assert CompressionSpec('zlib,0') == dict(name='zlib', level=0) - assert CompressionSpec('zlib,9') == dict(name='zlib', level=9) + assert CompressionSpec('none') == ComprSpec(name='none', spec=None) + assert CompressionSpec('lz4') == ComprSpec(name='lz4', spec=None) + assert CompressionSpec('zlib') == ComprSpec(name='zlib', spec=6) + assert CompressionSpec('zlib,0') == ComprSpec(name='zlib', spec=0) + assert CompressionSpec('zlib,9') == ComprSpec(name='zlib', spec=9) with pytest.raises(ValueError): CompressionSpec('zlib,9,invalid') - assert CompressionSpec('lzma') == dict(name='lzma', level=6) - assert CompressionSpec('lzma,0') == dict(name='lzma', level=0) - assert CompressionSpec('lzma,9') == dict(name='lzma', level=9) + assert CompressionSpec('lzma') == ComprSpec(name='lzma', spec=6) + assert CompressionSpec('lzma,0') == ComprSpec(name='lzma', spec=0) + assert CompressionSpec('lzma,9') == ComprSpec(name='lzma', spec=9) with pytest.raises(ValueError): CompressionSpec('lzma,9,invalid') with pytest.raises(ValueError): @@ -1202,14 +1202,14 @@ none:*.zip """.splitlines() cd = CompressionDecider1(default, []) # no conf, always use default - assert cd.decide('/srv/vm_disks/linux')['name'] == 'zlib' - assert cd.decide('test.zip')['name'] == 'zlib' - assert cd.decide('test')['name'] == 'zlib' + assert cd.decide('/srv/vm_disks/linux').name == 'zlib' + assert cd.decide('test.zip').name == 'zlib' + assert cd.decide('test').name == 'zlib' cd = CompressionDecider1(default, [conf, ]) - assert cd.decide('/srv/vm_disks/linux')['name'] == 'lz4' - assert cd.decide('test.zip')['name'] == 'none' - assert cd.decide('test')['name'] == 'zlib' # no match in conf, use default + assert cd.decide('/srv/vm_disks/linux').name == 'lz4' + assert cd.decide('test.zip').name == 'none' + assert cd.decide('test').name == 'zlib' # no match in conf, use default def test_compression_decider2(): @@ -1217,9 +1217,9 @@ def test_compression_decider2(): cd = CompressionDecider2(default) compr_spec, chunk = cd.decide(Chunk(None)) - assert compr_spec['name'] == 'zlib' + assert compr_spec.name == 'zlib' compr_spec, chunk = cd.decide(Chunk(None, compress=CompressionSpec('lzma'))) - assert compr_spec['name'] == 'lzma' + assert compr_spec.name == 'lzma' def test_format_line(): From d01a8f54b6bbc524021e5a370b97ea870af5d46d Mon Sep 17 00:00:00 2001 From: Fredrik Mikker Date: Fri, 24 Mar 2017 00:49:08 +0100 Subject: [PATCH 0737/1387] Added FAQ section about backing up root partition Using borgbackup to backup the root partition works fine, just remember to exclude non-essential directories. Signed-off-by: Fredrik Mikker --- docs/faq.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index 48596066..835983b6 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -353,6 +353,10 @@ How can I restore huge file(s) over a unstable connection? If you can not manage to extract the whole big file in one go, you can extract all the part files (see above) and manually concatenate them together. +Can i backup my root partition (/) with borg? +-------------------------------------------- +Backing up your entire root partition works just fine, but remember to exclude directories that make no sense to backup, such as /dev, /proc, /sys, /tmp and /run. + If it crashes with a UnicodeError, what can I do? ------------------------------------------------- From 475d53d9ef8e3dc6c91b3aada499c4196b366f20 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 25 Mar 2017 15:41:11 +0100 Subject: [PATCH 0738/1387] docs: kill api page --- docs/api.rst | 99 -------------------------------------------- docs/development.rst | 12 ++---- setup.py | 35 ---------------- 3 files changed, 3 insertions(+), 143 deletions(-) delete mode 100644 docs/api.rst diff --git a/docs/api.rst b/docs/api.rst deleted file mode 100644 index 9fd1a492..00000000 --- a/docs/api.rst +++ /dev/null @@ -1,99 +0,0 @@ - -API Documentation -================= - -.. automodule:: borg.archiver - :members: - :undoc-members: - -.. automodule:: borg.archive - :members: - :undoc-members: - -.. automodule:: borg.repository - :members: - :undoc-members: - -.. automodule:: borg.remote - :members: - :undoc-members: - -.. automodule:: borg.cache - :members: - :undoc-members: - -.. automodule:: borg.key - :members: - :undoc-members: - -.. automodule:: borg.keymanager - :members: - :undoc-members: - -.. automodule:: borg.nonces - :members: - :undoc-members: - -.. automodule:: borg.item - :members: - :undoc-members: - -.. automodule:: borg.constants - :members: - :undoc-members: - -.. automodule:: borg.logger - :members: - :undoc-members: - -.. automodule:: borg.helpers - :members: - :undoc-members: - -.. automodule:: borg.locking - :members: - :undoc-members: - -.. automodule:: borg.shellpattern - :members: - :undoc-members: - -.. automodule:: borg.lrucache - :members: - :undoc-members: - -.. automodule:: borg.fuse - :members: - :undoc-members: - -.. automodule:: borg.selftest - :members: - :undoc-members: - -.. automodule:: borg.xattr - :members: - :undoc-members: - -.. automodule:: borg.platform.base - :members: - :undoc-members: - -.. automodule:: borg.platform.linux - :members: - :undoc-members: - -.. automodule:: borg.hashindex - :members: - :undoc-members: - -.. automodule:: borg.compress - :members: get_compressor, Compressor, CompressorBase - :undoc-members: - -.. automodule:: borg.chunker - :members: - :undoc-members: - -.. automodule:: borg.crypto - :members: - :undoc-members: diff --git a/docs/development.rst b/docs/development.rst index ca138fda..f8277aaa 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -217,12 +217,6 @@ However, we prefer to do this as part of our :ref:`releasing` preparations, so it is generally not necessary to update these when submitting patches that change something about the command line. -The code documentation (which is currently not part of the released -docs) also uses a generated file (``docs/api.rst``), that needs to be -updated when a module is added or removed:: - - python setup.py build_api - Building the docs with Sphinx ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -300,9 +294,9 @@ Checklist: - update ``CHANGES.rst``, based on ``git log $PREVIOUS_RELEASE..`` - check version number of upcoming release in ``CHANGES.rst`` - verify that ``MANIFEST.in`` and ``setup.py`` are complete -- ``python setup.py build_api ; python setup.py build_usage ; python - setup.py build_man`` and commit (be sure to build with Python 3.4 as - Python 3.6 added `more guaranteed hashing algorithms +- ``python setup.py build_usage ; python setup.py build_man`` and + commit (be sure to build with Python 3.4 or 3.5 as Python 3.6 added `more + guaranteed hashing algorithms `_) - tag the release:: diff --git a/setup.py b/setup.py index a8da0bf0..15ebb8ed 100644 --- a/setup.py +++ b/setup.py @@ -567,43 +567,8 @@ class build_man(Command): write(option.ljust(padding), desc) -class build_api(Command): - description = "generate a basic api.rst file based on the modules available" - - user_options = [ - ('output=', 'O', 'output directory'), - ] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - print("auto-generating API documentation") - with open("docs/api.rst", "w") as doc: - doc.write(""" -.. IMPORTANT: this file is auto-generated by "setup.py build_api", do not edit! - - -API Documentation -================= -""") - for mod in sorted(glob('src/borg/*.py') + glob('src/borg/*.pyx')): - print("examining module %s" % mod) - mod = mod.replace('.pyx', '').replace('.py', '').replace('/', '.') - if "._" not in mod: - doc.write(""" -.. automodule:: %s - :members: - :undoc-members: -""" % mod) - - cmdclass = { 'build_ext': build_ext, - 'build_api': build_api, 'build_usage': build_usage, 'build_man': build_man, 'sdist': Sdist From dfdf590445beb5ff66a19d599ee51b25472c3724 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 25 Mar 2017 15:42:00 +0100 Subject: [PATCH 0739/1387] docs: typos --- docs/faq.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 835983b6..6f7c39ed 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -353,9 +353,12 @@ How can I restore huge file(s) over a unstable connection? If you can not manage to extract the whole big file in one go, you can extract all the part files (see above) and manually concatenate them together. -Can i backup my root partition (/) with borg? --------------------------------------------- -Backing up your entire root partition works just fine, but remember to exclude directories that make no sense to backup, such as /dev, /proc, /sys, /tmp and /run. +Can I backup my root partition (/) with Borg? +--------------------------------------------- + +Backing up your entire root partition works just fine, but remember to +exclude directories that make no sense to backup, such as /dev, /proc, +/sys, /tmp and /run. If it crashes with a UnicodeError, what can I do? ------------------------------------------------- From 126e7829989ddda6a01eab61112eddb3a45379b3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 25 Mar 2017 22:49:39 +0100 Subject: [PATCH 0740/1387] path normalization: rather use function than decorator less and less complex code, more flexible usage. --- src/borg/helpers.py | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index fc472965..ee388324 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -474,22 +474,11 @@ class PatternMatcher: return self.fallback -def normalized(func): - """ Decorator for the Pattern match methods, returning a wrapper that - normalizes OSX paths to match the normalized pattern on OSX, and - returning the original method on other platforms""" - @wraps(func) - def normalize_wrapper(self, path): - return func(self, unicodedata.normalize("NFD", path)) - - if sys.platform in ('darwin',): - # HFS+ converts paths to a canonical form, so users shouldn't be - # required to enter an exact match - return normalize_wrapper - else: - # Windows and Unix filesystems allow different forms, so users - # always have to enter an exact match - return func +def normalize_path(path): + """normalize paths for MacOS (but do nothing on other platforms)""" + # HFS+ converts paths to a canonical form, so users shouldn't be required to enter an exact match. + # Windows and Unix filesystems allow different forms, so users always have to enter an exact match. + return unicodedata.normalize('NFD', path) if sys.platform == 'darwin' else path class PatternBase: @@ -500,19 +489,14 @@ class PatternBase: def __init__(self, pattern): self.pattern_orig = pattern self.match_count = 0 - - if sys.platform in ('darwin',): - pattern = unicodedata.normalize("NFD", pattern) - + pattern = normalize_path(pattern) self._prepare(pattern) - @normalized def match(self, path): + path = normalize_path(path) matches = self._match(path) - if matches: self.match_count += 1 - return matches def __repr__(self): From 1a376ae1f11307b6de41552e73e72f70492b43bd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 25 Mar 2017 23:16:05 +0100 Subject: [PATCH 0741/1387] PatternMatcher: only normalize the path once not N times for N patterns. --- src/borg/helpers.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index ee388324..2a3a1b9b 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -467,10 +467,10 @@ class PatternMatcher: self._items.extend(patterns) def match(self, path): + path = normalize_path(path) for (pattern, value) in self._items: - if pattern.match(path): + if pattern.match(path, normalize=False): return value - return self.fallback @@ -492,8 +492,14 @@ class PatternBase: pattern = normalize_path(pattern) self._prepare(pattern) - def match(self, path): - path = normalize_path(path) + def match(self, path, normalize=True): + """match the given path against this pattern. + + If normalize is True (default), the path will get normalized using normalize_path(), + otherwise it is assumed that it already is normalized using that function. + """ + if normalize: + path = normalize_path(path) matches = self._match(path) if matches: self.match_count += 1 From 48652a65a6a875ad59bcc08fd4737341f2001558 Mon Sep 17 00:00:00 2001 From: Dan Christensen Date: Sat, 25 Mar 2017 19:44:19 -0400 Subject: [PATCH 0742/1387] With --compression auto,C, only use C if lz4 achieves at least 3% compression --- src/borg/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index fc472965..2a07ac42 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -2162,13 +2162,13 @@ class CompressionDecider2: cdata = lz4.compress(data) data_len = len(data) cdata_len = len(cdata) - if cdata_len < data_len: + if cdata_len < 0.97 * data_len: compr_spec = compr_args.spec else: # uncompressible - we could have a special "uncompressible compressor" # that marks such data as uncompressible via compression-type metadata. compr_spec = CompressionSpec('none') - self.logger.debug("len(data) == %d, len(lz4(data)) == %d, choosing %s", data_len, cdata_len, compr_spec) + self.logger.debug("len(data) == %d, len(lz4(data)) == %d, ratio == %.3f, choosing %s", data_len, cdata_len, cdata_len/data_len, compr_spec) meta['compress'] = compr_spec return compr_spec, Chunk(data, **meta) From 945880af47a3222db4bca32d8a347a09e72188ee Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 5 Mar 2017 05:19:32 +0100 Subject: [PATCH 0743/1387] implement async_response, add wait=True for add_chunk/chunk_decref Before this changeset, async responses were: - if not an error: ignored - if an error: raised as response to the arbitrary/unrelated next command Now, after sending async commands, the async_response command must be used to process outstanding responses / exceptions. We are avoiding to pile up lots of stuff in cases of high latency, because we do NOT first wait until ALL responses have arrived, but we just can begin to process responses. Calls with wait=False will just return what we already have received. Repeated calls with wait=True until None is returned will fetch all responses. Async commands now actually could have non-exception non-None results, but this is not used yet. None responses are still dropped. The motivation for this is to have a clear separation between a request blowing up because it (itself) failed and failures unrelated to that request / to that line in the sourcecode. also: fix processing for async repo obj deletes exception_ignored is a special object used that is "not None" (as None is used to signal "finished with processing async results") but also not a potential async response result value. Also: added wait=True to chunk_decref() and add_chunk() this makes async processing explicit - the default is synchronous and you only need to be careful and do extra steps for async processing if you explicitly request async by calling with wait=False (usually for speed reasons). to process async results, use async_response, see above. --- src/borg/archive.py | 37 +++++++++++++++++++++++++--------- src/borg/cache.py | 8 ++++---- src/borg/remote.py | 35 ++++++++++++++++++++++++++++---- src/borg/repository.py | 21 +++++++++++++++++++ src/borg/testsuite/archive.py | 7 ++++++- src/borg/testsuite/archiver.py | 3 ++- 6 files changed, 92 insertions(+), 19 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 6efc304b..bfc2d533 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -260,7 +260,8 @@ class CacheChunkBuffer(ChunkBuffer): self.stats = stats def write_chunk(self, chunk): - id_, _, _ = self.cache.add_chunk(self.key.id_hash(chunk.data), chunk, self.stats) + id_, _, _ = self.cache.add_chunk(self.key.id_hash(chunk.data), chunk, self.stats, wait=False) + self.cache.repository.async_response(wait=False) return id_ @@ -469,6 +470,8 @@ Utilization of max. archive size: {csize_max:.0%} data = self.key.pack_and_authenticate_metadata(metadata.as_dict(), context=b'archive') self.id = self.key.id_hash(data) self.cache.add_chunk(self.id, Chunk(data), self.stats) + while self.repository.async_response(wait=True) is not None: + pass self.manifest.archives[name] = (self.id, metadata.time) self.manifest.write() self.repository.commit() @@ -730,18 +733,27 @@ Utilization of max. archive size: {csize_max:.0%} class ChunksIndexError(Error): """Chunk ID {} missing from chunks index, corrupted chunks index - aborting transaction.""" - def chunk_decref(id, stats): - nonlocal error + exception_ignored = object() + + def fetch_async_response(wait=True): try: - self.cache.chunk_decref(id, stats) - except KeyError: - cid = bin_to_hex(id) - raise ChunksIndexError(cid) + return self.repository.async_response(wait=wait) except Repository.ObjectNotFound as e: + nonlocal error # object not in repo - strange, but we wanted to delete it anyway. if forced == 0: raise error = True + return exception_ignored # must not return None here + + def chunk_decref(id, stats): + try: + self.cache.chunk_decref(id, stats, wait=False) + except KeyError: + cid = bin_to_hex(id) + raise ChunksIndexError(cid) + else: + fetch_async_response(wait=False) error = False try: @@ -778,6 +790,10 @@ Utilization of max. archive size: {csize_max:.0%} # some harmless exception. chunk_decref(self.id, stats) del self.manifest.archives[self.name] + while fetch_async_response(wait=True) is not None: + # we did async deletes, process outstanding results (== exceptions), + # so there is nothing pending when we return and our caller wants to commit. + pass if error: logger.warning('forced deletion succeeded, but the deleted archive was corrupted.') logger.warning('borg check --repair is required to free all space.') @@ -865,7 +881,9 @@ Utilization of max. archive size: {csize_max:.0%} def chunk_file(self, item, cache, stats, chunk_iter, chunk_processor=None, **chunk_kw): if not chunk_processor: def chunk_processor(data): - return cache.add_chunk(self.key.id_hash(data), Chunk(data, **chunk_kw), stats) + chunk_entry = cache.add_chunk(self.key.id_hash(data), Chunk(data, **chunk_kw), stats, wait=False) + self.cache.repository.async_response(wait=False) + return chunk_entry item.chunks = [] from_chunk = 0 @@ -1654,7 +1672,8 @@ class ArchiveRecreater: if Compressor.detect(old_chunk.data).name == compression_spec.name: # Stored chunk has the same compression we wanted overwrite = False - chunk_entry = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite) + chunk_entry = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite, wait=False) + self.cache.repository.async_response(wait=False) self.seen_chunks.add(chunk_entry.id) return chunk_entry diff --git a/src/borg/cache.py b/src/borg/cache.py index 79c0a497..b3d7e12f 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -524,7 +524,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" self.do_cache = os.path.isdir(archive_path) self.chunks = create_master_idx(self.chunks) - def add_chunk(self, id, chunk, stats, overwrite=False): + def add_chunk(self, id, chunk, stats, overwrite=False, wait=True): if not self.txn_active: self.begin_txn() size = len(chunk.data) @@ -533,7 +533,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" return self.chunk_incref(id, stats) data = self.key.encrypt(chunk) csize = len(data) - self.repository.put(id, data, wait=False) + self.repository.put(id, data, wait=wait) self.chunks.add(id, 1, size, csize) stats.update(size, csize, not refcount) return ChunkListEntry(id, size, csize) @@ -554,13 +554,13 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" stats.update(size, csize, False) return ChunkListEntry(id, size, csize) - def chunk_decref(self, id, stats): + def chunk_decref(self, id, stats, wait=True): if not self.txn_active: self.begin_txn() count, size, csize = self.chunks.decref(id) if count == 0: del self.chunks[id] - self.repository.delete(id, wait=False) + self.repository.delete(id, wait=wait) stats.update(-size, -csize, True) else: stats.update(-size, -csize, False) diff --git a/src/borg/remote.py b/src/borg/remote.py index 7bb9ece0..d38a66e5 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -513,6 +513,7 @@ class RemoteRepository: self.chunkid_to_msgids = {} self.ignore_responses = set() self.responses = {} + self.async_responses = {} self.ratelimit = SleepingBandwidthLimiter(args.remote_ratelimit * 1024 if args and args.remote_ratelimit else 0) self.unpacker = get_limited_unpacker('client') self.server_version = parse_version('1.0.8') # fallback version if server is too old to send version information @@ -670,8 +671,8 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. for resp in self.call_many(cmd, [args], **kw): return resp - def call_many(self, cmd, calls, wait=True, is_preloaded=False): - if not calls: + def call_many(self, cmd, calls, wait=True, is_preloaded=False, async_wait=True): + if not calls and cmd != 'async_responses': return def pop_preload_msgid(chunkid): @@ -726,6 +727,22 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. return except KeyError: break + if cmd == 'async_responses': + while True: + try: + msgid, unpacked = self.async_responses.popitem() + except KeyError: + # there is nothing left what we already have received + if async_wait and self.ignore_responses: + # but do not return if we shall wait and there is something left to wait for: + break + else: + return + else: + if b'exception_class' in unpacked: + handle_error(unpacked) + else: + yield unpacked[RESULT] if self.to_send or ((calls or self.preload_ids) and len(waiting_for) < MAX_INFLIGHT): w_fds = [self.stdin_fd] else: @@ -755,8 +772,14 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. raise UnexpectedRPCDataFormatFromServer(data) if msgid in self.ignore_responses: self.ignore_responses.remove(msgid) + # async methods never return values, but may raise exceptions. if b'exception_class' in unpacked: - handle_error(unpacked) + self.async_responses[msgid] = unpacked + else: + # we currently do not have async result values except "None", + # so we do not add them into async_responses. + if unpacked[RESULT] is not None: + self.async_responses[msgid] = unpacked else: self.responses[msgid] = unpacked elif fd is self.stderr_fd: @@ -805,7 +828,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. # that the fd should be writable if e.errno != errno.EAGAIN: raise - self.ignore_responses |= set(waiting_for) + self.ignore_responses |= set(waiting_for) # we lose order here @api(since=parse_version('1.0.0'), append_only={'since': parse_version('1.0.7'), 'previously': False}) @@ -883,6 +906,10 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. self.p.wait() self.p = None + def async_response(self, wait=True): + for resp in self.call_many('async_responses', calls=[], wait=True, async_wait=wait): + return resp + def preload(self, ids): self.preload_ids += ids diff --git a/src/borg/repository.py b/src/borg/repository.py index d7b84ab3..862cd056 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -862,6 +862,11 @@ class Repository: yield self.get(id_) def put(self, id, data, wait=True): + """put a repo object + + Note: when doing calls with wait=False this gets async and caller must + deal with async results / exceptions later. + """ if not self._active_txn: self.prepare_txn(self.get_transaction_id()) try: @@ -881,6 +886,11 @@ class Repository: self.index[id] = segment, offset def delete(self, id, wait=True): + """delete a repo object + + Note: when doing calls with wait=False this gets async and caller must + deal with async results / exceptions later. + """ if not self._active_txn: self.prepare_txn(self.get_transaction_id()) try: @@ -895,6 +905,17 @@ class Repository: self.compact[segment] += size self.segments.setdefault(segment, 0) + def async_response(self, wait=True): + """Get one async result (only applies to remote repositories). + + async commands (== calls with wait=False, e.g. delete and put) have no results, + but may raise exceptions. These async exceptions must get collected later via + async_response() calls. Repeat the call until it returns None. + The previous calls might either return one (non-None) result or raise an exception. + If wait=True is given and there are outstanding responses, it will wait for them + to arrive. With wait=False, it will only return already received responses. + """ + def preload(self, ids): """Preload objects (only applies to remote repositories) """ diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index 7bf3f5b6..dc172d43 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -63,10 +63,15 @@ This archive: 20 B 10 B 10 B"" class MockCache: + class MockRepo: + def async_response(self, wait=True): + pass + def __init__(self): self.objects = {} + self.repository = self.MockRepo() - def add_chunk(self, id, chunk, stats=None): + def add_chunk(self, id, chunk, stats=None, wait=True): self.objects[id] = chunk.data return id, len(chunk.data), len(chunk.data) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 52cb05a6..e44f2753 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1255,7 +1255,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): repository.delete(first_chunk_id) repository.commit() break - self.cmd('delete', '--force', self.repository_location + '::test') + output = self.cmd('delete', '--force', self.repository_location + '::test') + self.assert_in('deleted archive was corrupted', output) self.cmd('check', '--repair', self.repository_location) output = self.cmd('list', self.repository_location) self.assert_not_in('test', output) From a9088135aa020311ac4f854e501c9ffbcaf8b002 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 21 Mar 2017 23:44:47 +0100 Subject: [PATCH 0744/1387] RemoteRepository: shutdown with timeout --- src/borg/remote.py | 8 ++++++++ src/borg/repository.py | 1 + 2 files changed, 9 insertions(+) diff --git a/src/borg/remote.py b/src/borg/remote.py index d38a66e5..d3af8270 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -514,6 +514,7 @@ class RemoteRepository: self.ignore_responses = set() self.responses = {} self.async_responses = {} + self.shutdown_time = None self.ratelimit = SleepingBandwidthLimiter(args.remote_ratelimit * 1024 if args and args.remote_ratelimit else 0) self.unpacker = get_limited_unpacker('client') self.server_version = parse_version('1.0.8') # fallback version if server is too old to send version information @@ -605,6 +606,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. def __exit__(self, exc_type, exc_val, exc_tb): try: if exc_type is not None: + self.shutdown_time = time.monotonic() + 30 self.rollback() finally: # in any case, we want to cleanly close the repo, even if the @@ -715,6 +717,12 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. calls = list(calls) waiting_for = [] while wait or calls: + if self.shutdown_time and time.monotonic() > self.shutdown_time: + # we are shutting this RemoteRepository down already, make sure we do not waste + # a lot of time in case a lot of async stuff is coming in or remote is gone or slow. + logger.debug('shutdown_time reached, shutting down with %d waiting_for and %d async_responses.', + len(waiting_for), len(self.async_responses)) + return while waiting_for: try: unpacked = self.responses.pop(waiting_for[0]) diff --git a/src/borg/repository.py b/src/borg/repository.py index 862cd056..2073eec0 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -785,6 +785,7 @@ class Repository: self._active_txn = False def rollback(self): + # note: when used in remote mode, this is time limited, see RemoteRepository.shutdown_time. self._rollback(cleanup=False) def __len__(self): From 90dd0e8eca1dceb66066835d937766012e53b863 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Mar 2017 16:05:22 +0200 Subject: [PATCH 0745/1387] fix symlink item fs size computation a symlink has a 'source' attribute, so it was confused with a hardlink slave here. see also issue #2343. also, a symlink's fs size is defined as the length of the target path. --- src/borg/item.pyx | 5 +++++ src/borg/testsuite/archiver.py | 2 ++ src/borg/testsuite/item.py | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 642b7b26..5ca93404 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -1,3 +1,4 @@ +import stat from collections import namedtuple from .constants import ITEM_KEYS @@ -193,6 +194,10 @@ class Item(PropDict): raise AttributeError size = getattr(self, attr) except AttributeError: + if stat.S_ISLNK(self.mode): + # get out of here quickly. symlinks have no own chunks, their fs size is the length of the target name. + # also, there is the dual-use issue of .source (#2343), so don't confuse it with a hardlink slave. + return len(self.source) # no precomputed (c)size value available, compute it: try: chunks = getattr(self, 'chunks') diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index e44f2753..87498726 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1748,6 +1748,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): out_fn = os.path.join(mountpoint, 'input', 'link1') sti = os.stat(in_fn, follow_symlinks=False) sto = os.stat(out_fn, follow_symlinks=False) + assert sti.st_size == len('somewhere') + assert sto.st_size == len('somewhere') assert stat.S_ISLNK(sti.st_mode) assert stat.S_ISLNK(sto.st_mode) assert os.readlink(in_fn) == os.readlink(out_fn) diff --git a/src/borg/testsuite/item.py b/src/borg/testsuite/item.py index 4dedcdc1..785a962c 100644 --- a/src/borg/testsuite/item.py +++ b/src/borg/testsuite/item.py @@ -149,7 +149,7 @@ def test_unknown_property(): def test_item_file_size(): - item = Item(chunks=[ + item = Item(mode=0o100666, chunks=[ ChunkListEntry(csize=1, size=1000, id=None), ChunkListEntry(csize=1, size=2000, id=None), ]) @@ -157,5 +157,5 @@ def test_item_file_size(): def test_item_file_size_no_chunks(): - item = Item() + item = Item(mode=0o100666) assert item.get_size() == 0 From ebd928795e8a45f82d571a56a202eb6462ada6ca Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 24 Mar 2017 04:30:03 +0100 Subject: [PATCH 0746/1387] add PathFullPattern not really a pattern (as in potentially having any variable parts) - it just does a full, precise match, after the usual normalizations. the reason for adding this is mainly for later optimizations, e.g. via set membership check, so that a lot of such PathFullPatterns can be "matched" within O(1) time. --- src/borg/helpers.py | 12 ++++++++++++ src/borg/testsuite/helpers.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 43ae2d26..6252a5e7 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -518,6 +518,17 @@ class PatternBase: raise NotImplementedError +class PathFullPattern(PatternBase): + """Full match of a path.""" + PREFIX = "pf" + + def _prepare(self, pattern): + self.pattern = os.path.normpath(pattern) + + def _match(self, path): + return path == self.pattern + + # For PathPrefixPattern, FnmatchPattern and ShellPattern, we require that the pattern either match the whole path # or an initial segment of the path up to but not including a path separator. To unify the two cases, we add a path # separator to the end of the path before matching. @@ -600,6 +611,7 @@ class RegexPattern(PatternBase): _PATTERN_STYLES = set([ FnmatchPattern, + PathFullPattern, PathPrefixPattern, RegexPattern, ShellPattern, diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 727f1628..19c5e9c5 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -25,7 +25,8 @@ from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams, from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless from ..helpers import load_exclude_file, load_pattern_file from ..helpers import CompressionSpec, ComprSpec, CompressionDecider1, CompressionDecider2 -from ..helpers import parse_pattern, PatternMatcher, RegexPattern, PathPrefixPattern, FnmatchPattern, ShellPattern +from ..helpers import parse_pattern, PatternMatcher +from ..helpers import PathFullPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, RegexPattern from ..helpers import swidth_slice from ..helpers import chunkit from ..helpers import safe_ns, safe_s @@ -254,6 +255,35 @@ def check_patterns(files, pattern, expected): assert matched == (files if expected is None else expected) +@pytest.mark.parametrize("pattern, expected", [ + # "None" means all files, i.e. all match the given pattern + ("/", []), + ("/home", ["/home"]), + ("/home///", ["/home"]), + ("/./home", ["/home"]), + ("/home/user", ["/home/user"]), + ("/home/user2", ["/home/user2"]), + ("/home/user/.bashrc", ["/home/user/.bashrc"]), + ]) +def test_patterns_full(pattern, expected): + files = ["/home", "/home/user", "/home/user2", "/home/user/.bashrc", ] + + check_patterns(files, PathFullPattern(pattern), expected) + + +@pytest.mark.parametrize("pattern, expected", [ + # "None" means all files, i.e. all match the given pattern + ("", []), + ("relative", []), + ("relative/path/", ["relative/path"]), + ("relative/path", ["relative/path"]), + ]) +def test_patterns_full_relative(pattern, expected): + files = ["relative/path", "relative/path2", ] + + check_patterns(files, PathFullPattern(pattern), expected) + + @pytest.mark.parametrize("pattern, expected", [ # "None" means all files, i.e. all match the given pattern ("/", None), From 93feb754117d1d24aa0db7738b777de966308032 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 24 Mar 2017 06:06:02 +0100 Subject: [PATCH 0747/1387] optimize PathFullPattern matching for O(1) time For a borg create run using a patterns file with 15.000 PathFullPattern excludes that excluded almost all files in the input data set: - before this optimization: ~60s - after this optimization: ~1s --- src/borg/helpers.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 6252a5e7..2e343e4e 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -451,23 +451,42 @@ class PatternMatcher: # Value to return from match function when none of the patterns match. self.fallback = fallback + # optimizations + self._path_full_patterns = {} # full path -> return value + def empty(self): - return not len(self._items) + return not len(self._items) and not len(self._path_full_patterns) + + def _add(self, pattern, value): + if isinstance(pattern, PathFullPattern): + key = pattern.pattern # full, normalized path + self._path_full_patterns[key] = value + else: + self._items.append((pattern, value)) def add(self, patterns, value): """Add list of patterns to internal list. The given value is returned from the match function when one of the given patterns matches. """ - self._items.extend((i, value) for i in patterns) + for pattern in patterns: + self._add(pattern, value) def add_inclexcl(self, patterns): """Add list of patterns (of type InclExclPattern) to internal list. The patterns ptype member is returned from the match function when one of the given patterns matches. """ - self._items.extend(patterns) + for pattern, pattern_type in patterns: + self._add(pattern, pattern_type) def match(self, path): path = normalize_path(path) + # do a fast lookup for full path matches (note: we do not count such matches): + non_existent = object() + value = self._path_full_patterns.get(path, non_existent) + if value is not non_existent: + # we have a full path match! + return value + # this is the slow way, if we have many patterns in self._items: for (pattern, value) in self._items: if pattern.match(path, normalize=False): return value From cb6bfdf4d656d1db37c2c7ffdb0bbbcb345b9dfa Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Mar 2017 00:26:57 +0100 Subject: [PATCH 0748/1387] add docs for path full-match patterns --- src/borg/archiver.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index ff448bd6..15c6dd9b 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1604,11 +1604,27 @@ class Archiver: regular expression syntax is described in the `Python documentation for the re module `_. - Prefix path, selector `pp:` + Path prefix, selector `pp:` This pattern style is useful to match whole sub-directories. The pattern `pp:/data/bar` matches `/data/bar` and everything therein. + Path full-match, selector `pf:` + + This pattern style is useful to match whole paths. + This is kind of a pseudo pattern as it can not have any variable or + unspecified parts - the full, precise path must be given. + `pf:/data/foo.txt` matches `/data/foo.txt` only. + + Implementation note: this is implemented via very time-efficient O(1) + hashtable lookups (this means you can have huge amounts of such patterns + without impacting performance much). + Due to that, this kind of pattern does not respect any context or order. + If you use such a pattern to include a file, it will always be included + (if the directory recursion encounters it). + Other include/exclude patterns that would normally match will be ignored. + Same logic applies for exclude. + Exclusions can be passed via the command line option `--exclude`. When used from within a shell the patterns should be quoted to protect them from expansion. From 60ae95801d546bb8475cf4cc5b61ec411240987d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 22 Mar 2017 05:08:14 +0100 Subject: [PATCH 0749/1387] update CHANGES (master / 1.1.0b4) --- docs/changes.rst | 139 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 133 insertions(+), 6 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index d8d9a351..beca3a62 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -133,16 +133,143 @@ Version 1.1.0b4 (not released yet) Compatibility notes: -- Moved "borg migrate-to-repokey" to "borg key migrate-to-repokey". -- "borg change-passphrase" is deprecated, use "borg key change-passphrase" instead. - -New features: - +- init: the --encryption argument is mandatory now (there are several choices) +- moved "borg migrate-to-repokey" to "borg key migrate-to-repokey". +- "borg change-passphrase" is deprecated, use "borg key change-passphrase" + instead. - the --exclude-if-present option now supports tagging a folder with any filesystem object type (file, folder, etc), instead of expecting only files as tags, #1999 -- the --keep-tag-files option has been deprecated in favor of the new +- the --keep-tag-files option has been deprecated in favor of the new --keep-exclude-tags, to account for the change mentioned above. +- use lz4 compression by default, #2179 + +New features: + +- JSON API to make developing frontends and automation easier + (see :ref:`json_output`) + + - add JSON output to commands: `borg create/list/info --json ...`. + - add --log-json option for structured logging output. + - add JSON progress information, JSON support for confirmations (yes()). +- add two new options --pattern and --patterns-from as discussed in #1406 +- new path full match pattern style (pf:) for very fast matching, #2334 +- add 'debug dump-manifest' and 'debug dump-archive' commands +- add 'borg benchmark crud' command, #1788 +- new 'borg delete --force --force' to delete severely corrupted archives, #1975 +- info: show utilization of maximum archive size, #1452 +- list: add dsize and dcsize keys, #2164 +- paperkey.html: Add interactive html template for printing key backups. +- key export: add qr html export mode +- securely erase config file (which might have old encryption key), #2257 +- archived file items: add size to metadata, 'borg extract' and 'borg check' do + check the file size for consistency, FUSE uses precomputed size from Item. + +Fixes: + +- fix remote speed regression introduced in 1.1.0b3, #2185 +- fix regression handling timestamps beyond 2262 (revert bigint removal), + introduced in 1.1.0b3, #2321 +- clamp (nano)second values to unproblematic range, #2304 +- hashindex: rebuild hashtable if we have too little empty buckets + (performance fix), #2246 +- Location regex: fix bad parsing of wrong syntax +- ignore posix_fadvise errors in repository.py, #2095 +- borg rpc: use limited msgpack.Unpacker (security precaution), #2139 +- Manifest: Make sure manifest timestamp is strictly monotonically increasing. +- create: handle BackupOSError on a per-path level in one spot +- create: clarify -x option / meaning of "same filesystem" +- create: don't create hard link refs to failed files +- archive check: detect and fix missing all-zero replacement chunks, #2180 +- files cache: update inode number when --ignore-inode is used, #2226 +- fix decompression exceptions crashing ``check --verify-data`` and others + instead of reporting integrity error, #2224 #2221 +- extract: warning for unextracted big extended attributes, #2258, #2161 +- mount: umount on SIGINT/^C when in foreground +- mount: handle invalid hard link refs +- mount: fix huge RAM consumption when mounting a repository (saves number of + archives * 8 MiB), #2308 +- hashindex: detect mingw byte order #2073 +- hashindex: fix wrong skip_hint on hashindex_set when encountering tombstones, + the regression was introduced in #1748 +- fix ChunkIndex.__contains__ assertion for big-endian archs +- fix borg key/debug/benchmark crashing without subcommand, #2240 +- Location: accept //servername/share/path +- correct/refactor calculation of unique/non-unique chunks +- extract: fix missing call to ProgressIndicator.finish +- prune: fix error msg, it is --keep-within, not --within +- fix "auto" compression mode bug (not compressing), #2331 +- fix symlink item fs size computation, #2344 + +Other changes: + +- remote repository: improved async exception processing, #2255 #2225 +- with --compression auto,C, only use C if lz4 achieves at least 3% compression +- PatternMatcher: only normalize path once, #2338 +- hashindex: separate endian-dependent defs from endian detection +- migrate-to-repokey: ask using canonical_path() as we do everywhere else. +- SyncFile: fix use of fd object after close +- make LoggedIO.close_segment reentrant +- creating a new segment: use "xb" mode, #2099 +- redo key_creator, key_factory, centralise key knowledge, #2272 +- add return code functions, #2199 +- list: only load cache if needed +- list: files->items, clarifications +- list: add "name" key for consistency with info cmd +- ArchiveFormatter: add "start" key for compatibility with "info" +- RemoteRepository: account rx/tx bytes +- setup.py build_usage/build_man/build_api fixes +- Manifest.in: simplify, exclude *.{so,dll,orig}, #2066 +- FUSE: get rid of chunk accounting, st_blocks = ceil(size / blocksize). +- tests: + + - help python development by testing 3.6-dev + - test for borg delete --force +- vagrant: + + - freebsd: some fixes, #2067 + - darwin64: use osxfuse 3.5.4 for tests / to build binaries + - darwin64: improve VM settings + - use python 3.5.3 to build binaries, #2078 + - upgrade pyinstaller from 3.1.1+ to 3.2.1 + - pyinstaller: use fixed AND freshly compiled bootloader, #2002 + - pyinstaller: automatically builds bootloader if missing +- docs: + + - create really nice man pages + - faq: mention --remote-ratelimit in bandwidth limit question + - fix caskroom link, #2299 + - docs/security: reiterate that RPC in Borg does no networking + - docs/security: counter tracking, #2266 + - docs/development: update merge remarks + - address SSH batch mode in docs, #2202 #2270 + - add warning about running build_usage on Python >3.4, #2123 + - one link per distro in the installation page + - improve --exclude-if-present and --keep-exclude-tags, #2268 + - improve automated backup script in doc, #2214 + - improve remote-path description + - update docs for create -C default change (lz4) + - document relative path usage, #1868 + - document snapshot usage, #2178 + - corrected some stuff in internals+security + - internals: move toctree to after the introduction text + - clarify metadata kind, manifest ops + - key enc: correct / clarify some stuff, link to internals/security + - datas: enc: 1.1.x mas different MACs + - datas: enc: correct factual error -- no nonce involved there. + - make internals.rst an index page and edit it a bit + - add "Cryptography in Borg" and "Remote RPC protocol security" sections + - document BORG_HOSTNAME_IS_UNIQUE, #2087 + - FAQ by categories as proposed by @anarcat in #1802 + - FAQ: update Which file types, attributes, etc. are *not* preserved? + - development: new branching model for git repository + - development: define "ours" merge strategy for auto-generated files + - create: move --exclude note to main doc + - create: move item flags to main doc + - fix examples using borg init without -e/--encryption + - list: don't print key listings in fat (html + man) + - remove Python API docs (were very incomplete, build problems on RTFD) + - added FAQ section about backing up root partition Version 1.0.10 (2017-02-13) From 72abd12e3cc5b4ec3516b1f7d73d431af75909fd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 27 Mar 2017 01:35:32 +0200 Subject: [PATCH 0750/1387] update CHANGES with release date --- docs/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index beca3a62..4548856a 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -128,8 +128,8 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= -Version 1.1.0b4 (not released yet) ----------------------------------- +Version 1.1.0b4 (2017-03-27) +---------------------------- Compatibility notes: From 5bc17148e127ae21e2591725422fbd04ba80f6ae Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 27 Mar 2017 01:45:04 +0200 Subject: [PATCH 0751/1387] patterns help: mention path full-match in intro --- src/borg/archiver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 15c6dd9b..f1bfc67f 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1561,8 +1561,8 @@ class Archiver: helptext = collections.OrderedDict() helptext['patterns'] = textwrap.dedent(''' - File patterns support four separate styles: fnmatch, shell, regular - expressions and path prefixes. By default, fnmatch is used for + File patterns support these styles: fnmatch, shell, regular expressions, + path prefixes and path full-matches. By default, fnmatch is used for `--exclude` patterns and shell-style is used for `--pattern`. If followed by a colon (':') the first two characters of a pattern are used as a style selector. Explicit style selection is necessary when a From 251983a7411f2da8d4315a26e6b8071c907345ff Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 27 Mar 2017 01:45:45 +0200 Subject: [PATCH 0752/1387] ran setup.py build_usage --- docs/usage/break-lock.rst.inc | 2 +- docs/usage/change-passphrase.rst.inc | 2 +- docs/usage/check.rst.inc | 2 +- docs/usage/common-options.rst.inc | 4 +- docs/usage/create.rst.inc | 33 ++++++++-- docs/usage/delete.rst.inc | 4 +- docs/usage/diff.rst.inc | 22 +++++-- docs/usage/extract.rst.inc | 6 +- docs/usage/help.rst.inc | 76 +++++++++++++++++++---- docs/usage/info.rst.inc | 6 +- docs/usage/init.rst.inc | 2 +- docs/usage/key_change-passphrase.rst.inc | 2 +- docs/usage/key_export.rst.inc | 4 +- docs/usage/key_import.rst.inc | 2 +- docs/usage/key_migrate-to-repokey.rst.inc | 2 +- docs/usage/list.rst.inc | 28 +++++++-- docs/usage/mount.rst.inc | 2 +- docs/usage/prune.rst.inc | 2 +- docs/usage/recreate.rst.inc | 16 +++-- docs/usage/rename.rst.inc | 2 +- docs/usage/serve.rst.inc | 2 +- docs/usage/umount.rst.inc | 2 +- docs/usage/with-lock.rst.inc | 2 +- 23 files changed, 173 insertions(+), 52 deletions(-) diff --git a/docs/usage/break-lock.rst.inc b/docs/usage/break-lock.rst.inc index 5fa1cda5..756b0d39 100644 --- a/docs/usage/break-lock.rst.inc +++ b/docs/usage/break-lock.rst.inc @@ -20,4 +20,4 @@ Description This command breaks the repository and cache locks. Please use carefully and only while no borg process (on any machine) is -trying to access the Cache or the Repository. +trying to access the Cache or the Repository. \ No newline at end of file diff --git a/docs/usage/change-passphrase.rst.inc b/docs/usage/change-passphrase.rst.inc index 3bb827a4..92a82c0d 100644 --- a/docs/usage/change-passphrase.rst.inc +++ b/docs/usage/change-passphrase.rst.inc @@ -19,4 +19,4 @@ Description ~~~~~~~~~~~ The key files used for repository encryption are optionally passphrase -protected. This command can be used to change this passphrase. +protected. This command can be used to change this passphrase. \ No newline at end of file diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index 7705471b..9958603e 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -86,4 +86,4 @@ repository, decrypting and decompressing it. This is a cryptographic verificatio which will detect (accidental) corruption. For encrypted repositories it is tamper-resistant as well, unless the attacker has access to the keys. -It is also very slow. +It is also very slow. \ No newline at end of file diff --git a/docs/usage/common-options.rst.inc b/docs/usage/common-options.rst.inc index fb235b52..5dadfd52 100644 --- a/docs/usage/common-options.rst.inc +++ b/docs/usage/common-options.rst.inc @@ -12,6 +12,8 @@ | enable debug output, work on log level DEBUG ``--debug-topic TOPIC`` | enable TOPIC debugging (can be specified multiple times). The logger path is borg.debug. if TOPIC is not fully qualified. + ``--log-json`` + | Output one JSON object per log line instead of formatted text. ``--lock-wait N`` | wait for the lock, but max. N seconds (default: 1). ``--show-version`` @@ -23,7 +25,7 @@ ``--umask M`` | set umask to M (local and remote, default: 0077) ``--remote-path PATH`` - | set remote path to executable (default: "borg") + | use PATH as borg executable on the remote (default: "borg") ``--remote-ratelimit rate`` | set remote network upload rate limit in kiByte/s (default: 0=unlimited) ``--consider-part-files`` diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 6b34278d..97e05306 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -25,6 +25,8 @@ optional arguments | output verbose list of items (files, dirs, ...) ``--filter STATUSCHARS`` | only display items with the given status characters + ``--json`` + | output stats as JSON (implies --stats) `Common options`_ | @@ -39,11 +41,15 @@ Exclusion options ``--exclude-if-present NAME`` | exclude directories that are tagged by containing a filesystem object with the given NAME ``--keep-exclude-tags``, ``--keep-tag-files`` - | keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise excluded caches/directories + | if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive + ``--pattern PATTERN`` + | include/exclude paths matching PATTERN + ``--patterns-from PATTERNFILE`` + | read include/exclude patterns from PATTERNFILE, one per line Filesystem options ``-x``, ``--one-file-system`` - | stay in same file system, do not cross mount points + | stay in the same file system and do not store mount points of other file systems ``--numeric-owner`` | only store numeric user and group identifiers ``--noatime`` @@ -73,9 +79,12 @@ Description ~~~~~~~~~~~ This command creates a backup archive containing all files found while recursively -traversing all paths specified. When giving '-' as path, borg will read data -from standard input and create a file 'stdin' in the created archive from that -data. +traversing all paths specified. Paths are added to the archive as they are given, +that means if relative paths are desired, the command has to be run from the correct +directory. + +When giving '-' as path, borg will read data from standard input and create a +file 'stdin' in the created archive from that data. The archive will consume almost no disk space for files or parts of files that have already been stored in other archives. @@ -92,6 +101,11 @@ not provide correct inode information the --ignore-inode flag can be used. This potentially decreases reliability of change detection, while avoiding always reading all files on these file systems. +The mount points of filesystems or filesystem snapshots should be the same for every +creation of a new archive to ensure fast operation. This is because the file cache that +is used to determine changed files quickly uses absolute filenames. +If this is not possible, consider creating a bind mount to a stable location. + See the output of the "borg help patterns" command for more help on exclude patterns. See the output of the "borg help placeholders" command for more help on placeholders. @@ -102,6 +116,15 @@ exclude foo/.bundler/gems. In borg it will not, you need to use --exclude '\*/.bundler/gems' to get the same effect. See ``borg help patterns`` for more information. +In addition to using ``--exclude`` patterns, it is possible to use +``--exclude-if-present`` to specify the name of a filesystem object (e.g. a file +or folder name) which, when contained within another folder, will prevent the +containing folder from being backed up. By default, the containing folder and +all of its contents will be omitted from the backup. If, however, you wish to +only include the objects specified by ``--exclude-if-present`` in your backup, +and not include any other contents of the containing folder, this can be enabled +through using the ``--keep-exclude-tags`` option. + Item flags ++++++++++ diff --git a/docs/usage/delete.rst.inc b/docs/usage/delete.rst.inc index 2a685e88..5c1361a2 100644 --- a/docs/usage/delete.rst.inc +++ b/docs/usage/delete.rst.inc @@ -20,7 +20,7 @@ optional arguments ``-c``, ``--cache-only`` | delete only the local cache for the given repository ``--force`` - | force deletion of corrupted archives + | force deletion of corrupted archives, use --force --force in case --force does not work. ``--save-space`` | work slower, but using less space @@ -42,4 +42,4 @@ Description This command deletes an archive from the repository or the complete repository. Disk space is reclaimed accordingly. If you delete the complete repository, the -local cache for it (if any) is also deleted. +local cache for it (if any) is also deleted. \ No newline at end of file diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index 1c245cf2..65d5afe6 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -17,10 +17,6 @@ positional arguments paths of items inside the archives to compare; patterns are supported optional arguments - ``-e PATTERN``, ``--exclude PATTERN`` - | exclude paths matching PATTERN - ``--exclude-from EXCLUDEFILE`` - | read exclude patterns from EXCLUDEFILE, one per line ``--numeric-owner`` | only consider numeric user and group identifiers ``--same-chunker-params`` @@ -31,6 +27,22 @@ optional arguments `Common options`_ | +Exclusion options + ``-e PATTERN``, ``--exclude PATTERN`` + | exclude paths matching PATTERN + ``--exclude-from EXCLUDEFILE`` + | read exclude patterns from EXCLUDEFILE, one per line + ``--exclude-caches`` + | exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) + ``--exclude-if-present NAME`` + | exclude directories that are tagged by containing a filesystem object with the given NAME + ``--keep-exclude-tags``, ``--keep-tag-files`` + | if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive + ``--pattern PATTERN`` + | include/exclude paths matching PATTERN + ``--patterns-from PATTERNFILE`` + | read include/exclude patterns from PATTERNFILE, one per line + Description ~~~~~~~~~~~ @@ -49,4 +61,4 @@ If you did not create the archives with different chunker params, pass --same-chunker-params. Note that the chunker params changed from Borg 0.xx to 1.0. -See the output of the "borg help patterns" command for more help on exclude patterns. +See the output of the "borg help patterns" command for more help on exclude patterns. \ No newline at end of file diff --git a/docs/usage/extract.rst.inc b/docs/usage/extract.rst.inc index 682eaa3a..704c7c64 100644 --- a/docs/usage/extract.rst.inc +++ b/docs/usage/extract.rst.inc @@ -25,6 +25,10 @@ optional arguments | exclude paths matching PATTERN ``--exclude-from EXCLUDEFILE`` | read exclude patterns from EXCLUDEFILE, one per line + ``--pattern PATTERN`` + | include/exclude paths matching PATTERN + ``--patterns-from PATTERNFILE`` + | read include/exclude patterns from PATTERNFILE, one per line ``--numeric-owner`` | only obey numeric user and group identifiers ``--strip-components NUMBER`` @@ -49,4 +53,4 @@ See the output of the "borg help patterns" command for more help on exclude patt By using ``--dry-run``, you can do all extraction steps except actually writing the output data: reading metadata and data chunks from the repo, checking the hash/hmac, -decrypting, decompressing. +decrypting, decompressing. \ No newline at end of file diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index a4a11c4f..dc2072d6 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -6,8 +6,9 @@ borg help patterns ~~~~~~~~~~~~~~~~~~ -Exclusion patterns support four separate styles, fnmatch, shell, regular -expressions and path prefixes. By default, fnmatch is used. If followed +File patterns support these styles: fnmatch, shell, regular expressions, +path prefixes and path full-matches. By default, fnmatch is used for +`--exclude` patterns and shell-style is used for `--pattern`. If followed by a colon (':') the first two characters of a pattern are used as a style selector. Explicit style selection is necessary when a non-default style is desired or when the desired pattern starts with @@ -15,12 +16,12 @@ two alphanumeric characters followed by a colon (i.e. `aa:something/*`). `Fnmatch `_, selector `fm:` - This is the default style. These patterns use a variant of shell - pattern syntax, with '*' matching any number of characters, '?' - matching any single character, '[...]' matching any single - character specified, including ranges, and '[!...]' matching any - character not specified. For the purpose of these patterns, the - path separator ('\' for Windows and '/' on other systems) is not + This is the default style for --exclude and --exclude-from. + These patterns use a variant of shell pattern syntax, with '*' matching + any number of characters, '?' matching any single character, '[...]' + matching any single character specified, including ranges, and '[!...]' + matching any character not specified. For the purpose of these patterns, + the path separator ('\' for Windows and '/' on other systems) is not treated specially. Wrap meta-characters in brackets for a literal match (i.e. `[?]` to match the literal character `?`). For a path to match a pattern, it must completely match from start to end, or @@ -31,6 +32,7 @@ two alphanumeric characters followed by a colon (i.e. `aa:something/*`). Shell-style patterns, selector `sh:` + This is the default style for --pattern and --patterns-from. Like fnmatch patterns these are similar to shell patterns. The difference is that the pattern may include `**/` for matching zero or more directory levels, `*` for matching zero or more arbitrary characters with the @@ -47,11 +49,27 @@ Regular expressions, selector `re:` regular expression syntax is described in the `Python documentation for the re module `_. -Prefix path, selector `pp:` +Path prefix, selector `pp:` This pattern style is useful to match whole sub-directories. The pattern `pp:/data/bar` matches `/data/bar` and everything therein. +Path full-match, selector `pf:` + + This pattern style is useful to match whole paths. + This is kind of a pseudo pattern as it can not have any variable or + unspecified parts - the full, precise path must be given. + `pf:/data/foo.txt` matches `/data/foo.txt` only. + + Implementation note: this is implemented via very time-efficient O(1) + hashtable lookups (this means you can have huge amounts of such patterns + without impacting performance much). + Due to that, this kind of pattern does not respect any context or order. + If you use such a pattern to include a file, it will always be included + (if the directory recursion encounters it). + Other include/exclude patterns that would normally match will be ignored. + Same logic applies for exclude. + Exclusions can be passed via the command line option `--exclude`. When used from within a shell the patterns should be quoted to protect them from expansion. @@ -93,6 +111,40 @@ Examples:: EOF $ borg create --exclude-from exclude.txt backup / + +A more general and easier to use way to define filename matching patterns exists +with the `--pattern` and `--patterns-from` options. Using these, you may specify +the backup roots (starting points) and patterns for inclusion/exclusion. A +root path starts with the prefix `R`, followed by a path (a plain path, not a +file pattern). An include rule starts with the prefix +, an exclude rule starts +with the prefix -, both followed by a pattern. +Inclusion patterns are useful to include pathes that are contained in an excluded +path. The first matching pattern is used so if an include pattern matches before +an exclude pattern, the file is backed up. + +Note that the default pattern style for `--pattern` and `--patterns-from` is +shell style (`sh:`), so those patterns behave similar to rsync include/exclude +patterns. The pattern style can be set via the `P` prefix. + +Patterns (`--pattern`) and excludes (`--exclude`) from the command line are +considered first (in the order of appearance). Then patterns from `--patterns-from` +are added. Exclusion patterns from `--exclude-from` files are appended last. + +An example `--patterns-from` file could look like that:: + + # "sh:" pattern style is the default, so the following line is not needed: + P sh + R / + # can be rebuild + - /home/*/.cache + # they're downloads for a reason + - /home/*/Downloads + # susan is a nice person + # include susans home + + /home/susan + # don't backup the other home directories + - /home/* + .. _borg_placeholders: borg help placeholders @@ -156,17 +208,17 @@ borg help compression ~~~~~~~~~~~~~~~~~~~~~ -Compression is off by default, if you want some, you have to specify what you want. +Compression is lz4 by default. If you want something else, you have to specify what you want. Valid compression specifiers are: none - Do not compress. (default) + Do not compress. lz4 - Use lz4 compression. High speed, low compression. + Use lz4 compression. High speed, low compression. (default) zlib[,L] diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index be61637e..4926b640 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -12,6 +12,10 @@ positional arguments REPOSITORY_OR_ARCHIVE archive or repository to display information about +optional arguments + ``--json`` + | format output as JSON + `Common options`_ | @@ -37,4 +41,4 @@ are meaning different things: This archive / deduplicated size = amount of data stored ONLY for this archive = unique chunks of this archive. All archives / deduplicated size = amount of data stored in the repo - = all chunks in the repository. + = all chunks in the repository. \ No newline at end of file diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index 9fb3264a..9e858d22 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -97,4 +97,4 @@ hardware-accelerated. BLAKE2b is faster than SHA256 on Intel/AMD 64bit CPUs, which makes `authenticated` faster than `none`. On modern ARM CPUs, NEON provides hardware acceleration for SHA256 making it faster -than BLAKE2b-256 there. +than BLAKE2b-256 there. \ No newline at end of file diff --git a/docs/usage/key_change-passphrase.rst.inc b/docs/usage/key_change-passphrase.rst.inc index cbc0d9e6..c34fd002 100644 --- a/docs/usage/key_change-passphrase.rst.inc +++ b/docs/usage/key_change-passphrase.rst.inc @@ -19,4 +19,4 @@ Description ~~~~~~~~~~~ The key files used for repository encryption are optionally passphrase -protected. This command can be used to change this passphrase. +protected. This command can be used to change this passphrase. \ No newline at end of file diff --git a/docs/usage/key_export.rst.inc b/docs/usage/key_export.rst.inc index f251d430..3013b8d2 100644 --- a/docs/usage/key_export.rst.inc +++ b/docs/usage/key_export.rst.inc @@ -17,6 +17,8 @@ positional arguments optional arguments ``--paper`` | Create an export suitable for printing and later type-in + ``--qr-html`` + | Create an html file suitable for printing and later type-in or qr scan `Common options`_ | @@ -40,4 +42,4 @@ data backup. For repositories using the repokey encryption the key is saved in the repository in the config file. A backup is thus not strictly needed, but guards against the repository becoming inaccessible if the file -is damaged for some reason. +is damaged for some reason. \ No newline at end of file diff --git a/docs/usage/key_import.rst.inc b/docs/usage/key_import.rst.inc index c8c2a0a8..c92bc9cd 100644 --- a/docs/usage/key_import.rst.inc +++ b/docs/usage/key_import.rst.inc @@ -29,4 +29,4 @@ export command. If the ``--paper`` option is given, the import will be an interactive process in which each line is checked for plausibility before -proceeding to the next line. For this format PATH must not be given. +proceeding to the next line. For this format PATH must not be given. \ No newline at end of file diff --git a/docs/usage/key_migrate-to-repokey.rst.inc b/docs/usage/key_migrate-to-repokey.rst.inc index 0e82f28c..66629bdb 100644 --- a/docs/usage/key_migrate-to-repokey.rst.inc +++ b/docs/usage/key_migrate-to-repokey.rst.inc @@ -33,4 +33,4 @@ use, but you may also use a different one. After migrating to repokey mode, you can change the passphrase at any time. But please note: the secrets will always stay the same and they could always -be derived from your (old) passphrase-mode passphrase. +be derived from your (old) passphrase-mode passphrase. \ No newline at end of file diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index d3a4485f..ee14a108 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -20,10 +20,8 @@ optional arguments ``--format``, ``--list-format`` | specify format for file listing | (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") - ``-e PATTERN``, ``--exclude PATTERN`` - | exclude paths matching PATTERN - ``--exclude-from EXCLUDEFILE`` - | read exclude patterns from EXCLUDEFILE, one per line + ``--json`` + | format output as JSON. The form of --format is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. `Common options`_ | @@ -38,6 +36,22 @@ filters ``--last N`` | consider last N archives after other filters were applied +Exclusion options + ``-e PATTERN``, ``--exclude PATTERN`` + | exclude paths matching PATTERN + ``--exclude-from EXCLUDEFILE`` + | read exclude patterns from EXCLUDEFILE, one per line + ``--exclude-caches`` + | exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) + ``--exclude-if-present NAME`` + | exclude directories that are tagged by containing a filesystem object with the given NAME + ``--keep-exclude-tags``, ``--keep-tag-files`` + | if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive + ``--pattern PATTERN`` + | include/exclude paths matching PATTERN + ``--patterns-from PATTERNFILE`` + | read include/exclude patterns from PATTERNFILE, one per line + Description ~~~~~~~~~~~ @@ -45,6 +59,8 @@ This command lists the contents of a repository or an archive. See the "borg help patterns" command for more help on exclude patterns. +.. man NOTES + The following keys are available for --format: - NEWLINE: OS dependent line separator @@ -57,7 +73,7 @@ The following keys are available for --format: Keys for listing repository archives: - - archive: archive name interpreted as text (might be missing non-text characters, see barchive) + - archive, name: archive name interpreted as text (might be missing non-text characters, see barchive) - barchive: verbatim archive name, can contain any character except NUL - time: time of creation of the archive - id: internal ID of the archive @@ -78,6 +94,8 @@ Keys for listing archive files: - size - csize: compressed size + - dsize: deduplicated size + - dcsize: deduplicated compressed size - num_chunks: number of chunks in this file - unique_chunks: number of unique chunks in this file diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index e15f25af..e7e60ce2 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -67,4 +67,4 @@ Unmounting in these cases could cause an active rsync or similar process to unintentionally delete data. When running in the foreground ^C/SIGINT unmounts cleanly, but other -signals or crashes do not. +signals or crashes do not. \ No newline at end of file diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index b6a0052c..c09b9885 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -87,4 +87,4 @@ negative number of archives to keep means that there is no limit. The "--keep-last N" option is doing the same as "--keep-secondly N" (and it will keep the last N archives under the assumption that you do not create more than one -backup archive in the same second). +backup archive in the same second). \ No newline at end of file diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index 93f7f414..0e37696b 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -37,9 +37,13 @@ Exclusion options ``--exclude-caches`` | exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) ``--exclude-if-present NAME`` - | exclude directories that are tagged by containing a filesystem object with the given NAME + | exclude directories that are tagged by containing a filesystem object with the given NAME ``--keep-exclude-tags``, ``--keep-tag-files`` - | keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise excluded caches/directories + | if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive + ``--pattern PATTERN`` + | include/exclude paths matching PATTERN + ``--patterns-from PATTERNFILE`` + | read include/exclude patterns from PATTERNFILE, one per line Archive options ``--target TARGET`` @@ -66,9 +70,9 @@ Recreate the contents of existing archives. This is an *experimental* feature. Do *not* use this on your only backup. ---exclude, --exclude-from and PATH have the exact same semantics -as in "borg create". If PATHs are specified the resulting archive -will only contain files from these PATHs. +--exclude, --exclude-from, --exclude-if-present, --keep-exclude-tags, and PATH +have the exact same semantics as in "borg create". If PATHs are specified the +resulting archive will only contain files from these PATHs. Note that all paths in an archive are relative, therefore absolute patterns/paths will *not* match (--exclude, --exclude-from, --compression-from, PATHs). @@ -97,4 +101,4 @@ With --target the original archive is not replaced, instead a new archive is cre When rechunking space usage can be substantial, expect at least the entire deduplicated size of the archives using the previous chunker params. When recompressing expect approx. (throughput / checkpoint-interval) in space usage, -assuming all chunks are recompressed. +assuming all chunks are recompressed. \ No newline at end of file diff --git a/docs/usage/rename.rst.inc b/docs/usage/rename.rst.inc index 3cff5a8a..6e53d245 100644 --- a/docs/usage/rename.rst.inc +++ b/docs/usage/rename.rst.inc @@ -22,4 +22,4 @@ Description This command renames an archive in the repository. -This results in a different archive ID. +This results in a different archive ID. \ No newline at end of file diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index 351af5e4..628ff399 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -20,4 +20,4 @@ optional arguments Description ~~~~~~~~~~~ -This command starts a repository server process. This command is usually not used manually. +This command starts a repository server process. This command is usually not used manually. \ No newline at end of file diff --git a/docs/usage/umount.rst.inc b/docs/usage/umount.rst.inc index 28c5f8f0..f99c1d46 100644 --- a/docs/usage/umount.rst.inc +++ b/docs/usage/umount.rst.inc @@ -21,4 +21,4 @@ Description This command un-mounts a FUSE filesystem that was mounted with ``borg mount``. This is a convenience wrapper that just calls the platform-specific shell -command - usually this is either umount or fusermount -u. +command - usually this is either umount or fusermount -u. \ No newline at end of file diff --git a/docs/usage/with-lock.rst.inc b/docs/usage/with-lock.rst.inc index c77eb2f6..407bda72 100644 --- a/docs/usage/with-lock.rst.inc +++ b/docs/usage/with-lock.rst.inc @@ -31,4 +31,4 @@ code as borg's return code. Note: if you copy a repository with the lock held, the lock will be present in the copy, obviously. Thus, before using borg on the copy, you need to - use "borg break-lock" on it. + use "borg break-lock" on it. \ No newline at end of file From acd3da62f4a764721e1c5e2f40e8b2659ed4d989 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 27 Mar 2017 01:57:52 +0200 Subject: [PATCH 0753/1387] add docstring to do_benchmark_crud --- src/borg/archiver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f1bfc67f..e241002e 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -325,6 +325,7 @@ class Archiver: return EXIT_SUCCESS def do_benchmark_crud(self, args): + """Benchmark Create, Read, Update, Delete for archives.""" def measurement_run(repo, path): archive = repo + '::borg-benchmark-crud' compression = '--compression=none' From 85bfcd439c3d8d3e674b013db8146343c902361d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 27 Mar 2017 01:58:19 +0200 Subject: [PATCH 0754/1387] ran setup.py build_man --- docs/man/borg-break-lock.1 | 2 +- docs/man/borg-change-passphrase.1 | 2 +- docs/man/borg-check.1 | 2 +- docs/man/borg-common.1 | 7 ++- docs/man/borg-compression.1 | 8 +-- docs/man/borg-create.1 | 45 +++++++++++--- docs/man/borg-delete.1 | 4 +- docs/man/borg-diff.1 | 32 +++++++--- docs/man/borg-extract.1 | 8 ++- docs/man/borg-info.1 | 8 ++- docs/man/borg-init.1 | 2 +- docs/man/borg-key-change-passphrase.1 | 2 +- docs/man/borg-key-export.1 | 5 +- docs/man/borg-key-import.1 | 2 +- docs/man/borg-key-migrate-to-repokey.1 | 2 +- docs/man/borg-key.1 | 2 +- docs/man/borg-list.1 | 55 +++++++++-------- docs/man/borg-mount.1 | 2 +- docs/man/borg-patterns.1 | 82 ++++++++++++++++++++++---- docs/man/borg-placeholders.1 | 2 +- docs/man/borg-prune.1 | 2 +- docs/man/borg-recreate.1 | 18 ++++-- docs/man/borg-rename.1 | 2 +- docs/man/borg-serve.1 | 2 +- docs/man/borg-umount.1 | 2 +- docs/man/borg-upgrade.1 | 2 +- docs/man/borg-with-lock.1 | 2 +- docs/man/borg.1 | 6 +- 28 files changed, 229 insertions(+), 81 deletions(-) diff --git a/docs/man/borg-break-lock.1 b/docs/man/borg-break-lock.1 index b8162436..6f9f73e0 100644 --- a/docs/man/borg-break-lock.1 +++ b/docs/man/borg-break-lock.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-BREAK-LOCK 1 "2017-02-11" "" "borg backup tool" +.TH BORG-BREAK-LOCK 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-break-lock \- Break the repository lock (e.g. in case it was left by a dead borg. . diff --git a/docs/man/borg-change-passphrase.1 b/docs/man/borg-change-passphrase.1 index b74dd336..2c8f90c5 100644 --- a/docs/man/borg-change-passphrase.1 +++ b/docs/man/borg-change-passphrase.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CHANGE-PASSPHRASE 1 "2017-02-11" "" "borg backup tool" +.TH BORG-CHANGE-PASSPHRASE 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-change-passphrase \- Change repository key file passphrase . diff --git a/docs/man/borg-check.1 b/docs/man/borg-check.1 index 8a0ec36b..3545a4d6 100644 --- a/docs/man/borg-check.1 +++ b/docs/man/borg-check.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CHECK 1 "2017-02-11" "" "borg backup tool" +.TH BORG-CHECK 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-check \- Check repository consistency . diff --git a/docs/man/borg-common.1 b/docs/man/borg-common.1 index e480c1f8..a10838f4 100644 --- a/docs/man/borg-common.1 +++ b/docs/man/borg-common.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-COMMON 1 "2017-02-11" "" "borg backup tool" +.TH BORG-COMMON 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-common \- Common options of Borg commands . @@ -54,6 +54,9 @@ enable debug output, work on log level DEBUG .BI \-\-debug\-topic \ TOPIC enable TOPIC debugging (can be specified multiple times). The logger path is borg.debug. if TOPIC is not fully qualified. .TP +.B \-\-log\-json +Output one JSON object per log line instead of formatted text. +.TP .BI \-\-lock\-wait \ N wait for the lock, but max. N seconds (default: 1). .TP @@ -70,7 +73,7 @@ do not load/update the file metadata cache used to detect unchanged files set umask to M (local and remote, default: 0077) .TP .BI \-\-remote\-path \ PATH -set remote path to executable (default: "borg") +use PATH as borg executable on the remote (default: "borg") .TP .BI \-\-remote\-ratelimit \ rate set remote network upload rate limit in kiByte/s (default: 0=unlimited) diff --git a/docs/man/borg-compression.1 b/docs/man/borg-compression.1 index 51124fea..e0a98ed1 100644 --- a/docs/man/borg-compression.1 +++ b/docs/man/borg-compression.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-COMPRESSION 1 "2017-02-11" "" "borg backup tool" +.TH BORG-COMPRESSION 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-compression \- Details regarding compression . @@ -32,21 +32,21 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH DESCRIPTION .sp -Compression is off by default, if you want some, you have to specify what you want. +Compression is lz4 by default. If you want something else, you have to specify what you want. .sp Valid compression specifiers are: .sp none .INDENT 0.0 .INDENT 3.5 -Do not compress. (default) +Do not compress. .UNINDENT .UNINDENT .sp lz4 .INDENT 0.0 .INDENT 3.5 -Use lz4 compression. High speed, low compression. +Use lz4 compression. High speed, low compression. (default) .UNINDENT .UNINDENT .sp diff --git a/docs/man/borg-create.1 b/docs/man/borg-create.1 index d01d77e2..c0a7ab96 100644 --- a/docs/man/borg-create.1 +++ b/docs/man/borg-create.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CREATE 1 "2017-02-12" "" "borg backup tool" +.TH BORG-CREATE 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-create \- Create new archive . @@ -36,9 +36,12 @@ borg create ARCHIVE PATH .SH DESCRIPTION .sp This command creates a backup archive containing all files found while recursively -traversing all paths specified. When giving \(aq\-\(aq as path, borg will read data -from standard input and create a file \(aqstdin\(aq in the created archive from that -data. +traversing all paths specified. Paths are added to the archive as they are given, +that means if relative paths are desired, the command has to be run from the correct +directory. +.sp +When giving \(aq\-\(aq as path, borg will read data from standard input and create a +file \(aqstdin\(aq in the created archive from that data. .sp The archive will consume almost no disk space for files or parts of files that have already been stored in other archives. @@ -55,6 +58,11 @@ not provide correct inode information the \-\-ignore\-inode flag can be used. Th potentially decreases reliability of change detection, while avoiding always reading all files on these file systems. .sp +The mount points of filesystems or filesystem snapshots should be the same for every +creation of a new archive to ensure fast operation. This is because the file cache that +is used to determine changed files quickly uses absolute filenames. +If this is not possible, consider creating a bind mount to a stable location. +.sp See the output of the "borg help patterns" command for more help on exclude patterns. See the output of the "borg help placeholders" command for more help on placeholders. .SH OPTIONS @@ -86,6 +94,9 @@ output verbose list of items (files, dirs, ...) .TP .BI \-\-filter \ STATUSCHARS only display items with the given status characters +.TP +.B \-\-json +output stats as JSON (implies \-\-stats) .UNINDENT .SS Exclusion options .INDENT 0.0 @@ -103,13 +114,19 @@ exclude directories that contain a CACHEDIR.TAG file (\fI\%http://www.brynosauru exclude directories that are tagged by containing a filesystem object with the given NAME .TP .B \-\-keep\-exclude\-tags\fP,\fB \-\-keep\-tag\-files -keep tag objects (i.e.: arguments to \-\-exclude\-if\-present) in otherwise excluded caches/directories +if tag objects are specified with \-\-exclude\-if\-present, don\(aqt omit the tag objects themselves from the backup archive +.TP +.BI \-\-pattern \ PATTERN +include/exclude paths matching PATTERN +.TP +.BI \-\-patterns\-from \ PATTERNFILE +read include/exclude patterns from PATTERNFILE, one per line .UNINDENT .SS Filesystem options .INDENT 0.0 .TP .B \-x\fP,\fB \-\-one\-file\-system -stay in same file system, do not cross mount points +stay in the same file system and do not store mount points of other file systems .TP .B \-\-numeric\-owner only store numeric user and group identifiers @@ -175,7 +192,7 @@ $ borg create /path/to/repo::my\-files /home \e \-\-exclude \(aqsh:/home/*/.thumbnails\(aq # Backup the root filesystem into an archive named "root\-YYYY\-MM\-DD" -# use zlib compression (good, but slow) \- default is no compression +# use zlib compression (good, but slow) \- default is lz4 (fast, low compression ratio) $ borg create \-C zlib,6 /path/to/repo::root\-{now:%Y\-%m\-%d} / \-\-one\-file\-system # Backup a remote host locally ("pull" style) using sshfs @@ -212,6 +229,11 @@ $ borg create /path/to/repo::{hostname}\-{user}\-{now} ~ $ borg create /path/to/repo::{hostname}\-{user}\-{now:%Y\-%m\-%dT%H:%M:%S} ~ # As above, but add nanoseconds $ borg create /path/to/repo::{hostname}\-{user}\-{now:%Y\-%m\-%dT%H:%M:%S.%f} ~ + +# Backing up relative paths by moving into the correct directory first +$ cd /home/user/Documents +# The root directory of the archive will be "projectA" +$ borg create /path/to/repo::daily\-projectA\-{now:%Y\-%m\-%d} projectA .ft P .fi .UNINDENT @@ -222,6 +244,15 @@ The \-\-exclude patterns are not like tar. In tar \-\-exclude .bundler/gems will exclude foo/.bundler/gems. In borg it will not, you need to use \-\-exclude \(aq*/.bundler/gems\(aq to get the same effect. See \fBborg help patterns\fP for more information. +.sp +In addition to using \fB\-\-exclude\fP patterns, it is possible to use +\fB\-\-exclude\-if\-present\fP to specify the name of a filesystem object (e.g. a file +or folder name) which, when contained within another folder, will prevent the +containing folder from being backed up. By default, the containing folder and +all of its contents will be omitted from the backup. If, however, you wish to +only include the objects specified by \fB\-\-exclude\-if\-present\fP in your backup, +and not include any other contents of the containing folder, this can be enabled +through using the \fB\-\-keep\-exclude\-tags\fP option. .SS Item flags .sp \fB\-\-list\fP outputs a list of all files, directories and other diff --git a/docs/man/borg-delete.1 b/docs/man/borg-delete.1 index 5b0ec529..01e2167d 100644 --- a/docs/man/borg-delete.1 +++ b/docs/man/borg-delete.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-DELETE 1 "2017-02-11" "" "borg backup tool" +.TH BORG-DELETE 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-delete \- Delete an existing repository or archives . @@ -60,7 +60,7 @@ print statistics for the deleted archive delete only the local cache for the given repository .TP .B \-\-force -force deletion of corrupted archives +force deletion of corrupted archives, use \-\-force \-\-force in case \-\-force does not work. .TP .B \-\-save\-space work slower, but using less space diff --git a/docs/man/borg-diff.1 b/docs/man/borg-diff.1 index f488a72a..dd0ec88c 100644 --- a/docs/man/borg-diff.1 +++ b/docs/man/borg-diff.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-DIFF 1 "2017-02-11" "" "borg backup tool" +.TH BORG-DIFF 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-diff \- Diff contents of two archives . @@ -69,12 +69,6 @@ paths of items inside the archives to compare; patterns are supported .SS optional arguments .INDENT 0.0 .TP -.BI \-e \ PATTERN\fP,\fB \ \-\-exclude \ PATTERN -exclude paths matching PATTERN -.TP -.BI \-\-exclude\-from \ EXCLUDEFILE -read exclude patterns from EXCLUDEFILE, one per line -.TP .B \-\-numeric\-owner only consider numeric user and group identifiers .TP @@ -84,6 +78,30 @@ Override check of chunker parameters. .B \-\-sort Sort the output lines by file path. .UNINDENT +.SS Exclusion options +.INDENT 0.0 +.TP +.BI \-e \ PATTERN\fP,\fB \ \-\-exclude \ PATTERN +exclude paths matching PATTERN +.TP +.BI \-\-exclude\-from \ EXCLUDEFILE +read exclude patterns from EXCLUDEFILE, one per line +.TP +.B \-\-exclude\-caches +exclude directories that contain a CACHEDIR.TAG file (\fI\%http://www.brynosaurus.com/cachedir/spec.html\fP) +.TP +.BI \-\-exclude\-if\-present \ NAME +exclude directories that are tagged by containing a filesystem object with the given NAME +.TP +.B \-\-keep\-exclude\-tags\fP,\fB \-\-keep\-tag\-files +if tag objects are specified with \-\-exclude\-if\-present, don\(aqt omit the tag objects themselves from the backup archive +.TP +.BI \-\-pattern \ PATTERN +include/exclude paths matching PATTERN +.TP +.BI \-\-patterns\-from \ PATTERNFILE +read include/exclude patterns from PATTERNFILE, one per line +.UNINDENT .SH EXAMPLES .INDENT 0.0 .INDENT 3.5 diff --git a/docs/man/borg-extract.1 b/docs/man/borg-extract.1 index 426be31e..089635c8 100644 --- a/docs/man/borg-extract.1 +++ b/docs/man/borg-extract.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-EXTRACT 1 "2017-02-11" "" "borg backup tool" +.TH BORG-EXTRACT 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-extract \- Extract archive contents . @@ -75,6 +75,12 @@ exclude paths matching PATTERN .BI \-\-exclude\-from \ EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line .TP +.BI \-\-pattern \ PATTERN +include/exclude paths matching PATTERN +.TP +.BI \-\-patterns\-from \ PATTERNFILE +read include/exclude patterns from PATTERNFILE, one per line +.TP .B \-\-numeric\-owner only obey numeric user and group identifiers .TP diff --git a/docs/man/borg-info.1 b/docs/man/borg-info.1 index 305f18fa..b75e44ff 100644 --- a/docs/man/borg-info.1 +++ b/docs/man/borg-info.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-INFO 1 "2017-02-11" "" "borg backup tool" +.TH BORG-INFO 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-info \- Show archive details such as disk space used . @@ -57,6 +57,12 @@ See \fIborg\-common(1)\fP for common options of Borg commands. .B REPOSITORY_OR_ARCHIVE archive or repository to display information about .UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-\-json +format output as JSON +.UNINDENT .SS filters .INDENT 0.0 .TP diff --git a/docs/man/borg-init.1 b/docs/man/borg-init.1 index bbe020c4..2b489ff9 100644 --- a/docs/man/borg-init.1 +++ b/docs/man/borg-init.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-INIT 1 "2017-02-11" "" "borg backup tool" +.TH BORG-INIT 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-init \- Initialize an empty repository . diff --git a/docs/man/borg-key-change-passphrase.1 b/docs/man/borg-key-change-passphrase.1 index 86a30ebf..7b1a71fa 100644 --- a/docs/man/borg-key-change-passphrase.1 +++ b/docs/man/borg-key-change-passphrase.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-CHANGE-PASSPHRASE 1 "2017-02-11" "" "borg backup tool" +.TH BORG-KEY-CHANGE-PASSPHRASE 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-key-change-passphrase \- Change repository key file passphrase . diff --git a/docs/man/borg-key-export.1 b/docs/man/borg-key-export.1 index 67202303..8cfe1505 100644 --- a/docs/man/borg-key-export.1 +++ b/docs/man/borg-key-export.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-EXPORT 1 "2017-02-11" "" "borg backup tool" +.TH BORG-KEY-EXPORT 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-key-export \- Export the repository key for backup . @@ -68,6 +68,9 @@ where to store the backup .TP .B \-\-paper Create an export suitable for printing and later type\-in +.TP +.B \-\-qr\-html +Create an html file suitable for printing and later type\-in or qr scan .UNINDENT .SH SEE ALSO .sp diff --git a/docs/man/borg-key-import.1 b/docs/man/borg-key-import.1 index 91ce569d..0a5a3245 100644 --- a/docs/man/borg-key-import.1 +++ b/docs/man/borg-key-import.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-IMPORT 1 "2017-02-11" "" "borg backup tool" +.TH BORG-KEY-IMPORT 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-key-import \- Import the repository key from backup . diff --git a/docs/man/borg-key-migrate-to-repokey.1 b/docs/man/borg-key-migrate-to-repokey.1 index 774c9199..efda66e1 100644 --- a/docs/man/borg-key-migrate-to-repokey.1 +++ b/docs/man/borg-key-migrate-to-repokey.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-MIGRATE-TO-REPOKEY 1 "2017-02-11" "" "borg backup tool" +.TH BORG-KEY-MIGRATE-TO-REPOKEY 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-key-migrate-to-repokey \- Migrate passphrase -> repokey . diff --git a/docs/man/borg-key.1 b/docs/man/borg-key.1 index ca1ec2eb..e0c878d6 100644 --- a/docs/man/borg-key.1 +++ b/docs/man/borg-key.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY 1 "2017-02-12" "" "borg backup tool" +.TH BORG-KEY 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-key \- Manage a keyfile or repokey of a repository . diff --git a/docs/man/borg-list.1 b/docs/man/borg-list.1 index 74e5a278..be02614e 100644 --- a/docs/man/borg-list.1 +++ b/docs/man/borg-list.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-LIST 1 "2017-02-12" "" "borg backup tool" +.TH BORG-LIST 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-list \- List archive or repository contents . @@ -60,11 +60,8 @@ only print file/directory names, nothing else specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") .TP -.BI \-e \ PATTERN\fP,\fB \ \-\-exclude \ PATTERN -exclude paths matching PATTERN -.TP -.BI \-\-exclude\-from \ EXCLUDEFILE -read exclude patterns from EXCLUDEFILE, one per line +.B \-\-json +format output as JSON. The form of \-\-format is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. .UNINDENT .SS filters .INDENT 0.0 @@ -81,6 +78,30 @@ consider first N archives after other filters were applied .BI \-\-last \ N consider last N archives after other filters were applied .UNINDENT +.SS Exclusion options +.INDENT 0.0 +.TP +.BI \-e \ PATTERN\fP,\fB \ \-\-exclude \ PATTERN +exclude paths matching PATTERN +.TP +.BI \-\-exclude\-from \ EXCLUDEFILE +read exclude patterns from EXCLUDEFILE, one per line +.TP +.B \-\-exclude\-caches +exclude directories that contain a CACHEDIR.TAG file (\fI\%http://www.brynosaurus.com/cachedir/spec.html\fP) +.TP +.BI \-\-exclude\-if\-present \ NAME +exclude directories that are tagged by containing a filesystem object with the given NAME +.TP +.B \-\-keep\-exclude\-tags\fP,\fB \-\-keep\-tag\-files +if tag objects are specified with \-\-exclude\-if\-present, don\(aqt omit the tag objects themselves from the backup archive +.TP +.BI \-\-pattern \ PATTERN +include/exclude paths matching PATTERN +.TP +.BI \-\-patterns\-from \ PATTERNFILE +read include/exclude patterns from PATTERNFILE, one per line +.UNINDENT .SH EXAMPLES .INDENT 0.0 .INDENT 3.5 @@ -141,7 +162,7 @@ Keys for listing repository archives: .INDENT 3.5 .INDENT 0.0 .IP \(bu 2 -archive: archive name interpreted as text (might be missing non\-text characters, see barchive) +archive, name: archive name interpreted as text (might be missing non\-text characters, see barchive) .IP \(bu 2 barchive: verbatim archive name, can contain any character except NUL .IP \(bu 2 @@ -183,6 +204,10 @@ size .IP \(bu 2 csize: compressed size .IP \(bu 2 +dsize: deduplicated size +.IP \(bu 2 +dcsize: deduplicated compressed size +.IP \(bu 2 num_chunks: number of chunks in this file .IP \(bu 2 unique_chunks: number of unique chunks in this file @@ -199,10 +224,6 @@ isoctime .IP \(bu 2 isoatime .IP \(bu 2 -blake2b -.IP \(bu 2 -blake2s -.IP \(bu 2 md5 .IP \(bu 2 sha1 @@ -213,20 +234,8 @@ sha256 .IP \(bu 2 sha384 .IP \(bu 2 -sha3_224 -.IP \(bu 2 -sha3_256 -.IP \(bu 2 -sha3_384 -.IP \(bu 2 -sha3_512 -.IP \(bu 2 sha512 .IP \(bu 2 -shake_128 -.IP \(bu 2 -shake_256 -.IP \(bu 2 archiveid .IP \(bu 2 archivename diff --git a/docs/man/borg-mount.1 b/docs/man/borg-mount.1 index 63a12cee..cb8ad26b 100644 --- a/docs/man/borg-mount.1 +++ b/docs/man/borg-mount.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-MOUNT 1 "2017-02-11" "" "borg backup tool" +.TH BORG-MOUNT 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-mount \- Mount archive or an entire repository as a FUSE filesystem . diff --git a/docs/man/borg-patterns.1 b/docs/man/borg-patterns.1 index 646694bf..c250a4b9 100644 --- a/docs/man/borg-patterns.1 +++ b/docs/man/borg-patterns.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PATTERNS 1 "2017-02-11" "" "borg backup tool" +.TH BORG-PATTERNS 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-patterns \- Details regarding patterns . @@ -32,8 +32,9 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH DESCRIPTION .sp -Exclusion patterns support four separate styles, fnmatch, shell, regular -expressions and path prefixes. By default, fnmatch is used. If followed +File patterns support these styles: fnmatch, shell, regular expressions, +path prefixes and path full\-matches. By default, fnmatch is used for +\fI\-\-exclude\fP patterns and shell\-style is used for \fI\-\-pattern\fP\&. If followed by a colon (\(aq:\(aq) the first two characters of a pattern are used as a style selector. Explicit style selection is necessary when a non\-default style is desired or when the desired pattern starts with @@ -42,12 +43,12 @@ two alphanumeric characters followed by a colon (i.e. \fIaa:something/*\fP). \fI\%Fnmatch\fP, selector \fIfm:\fP .INDENT 0.0 .INDENT 3.5 -This is the default style. These patterns use a variant of shell -pattern syntax, with \(aq*\(aq matching any number of characters, \(aq?\(aq -matching any single character, \(aq[...]\(aq matching any single -character specified, including ranges, and \(aq[!...]\(aq matching any -character not specified. For the purpose of these patterns, the -path separator (\(aq\(aq for Windows and \(aq/\(aq on other systems) is not +This is the default style for \-\-exclude and \-\-exclude\-from. +These patterns use a variant of shell pattern syntax, with \(aq*\(aq matching +any number of characters, \(aq?\(aq matching any single character, \(aq[...]\(aq +matching any single character specified, including ranges, and \(aq[!...]\(aq +matching any character not specified. For the purpose of these patterns, +the path separator (\(aq\(aq for Windows and \(aq/\(aq on other systems) is not treated specially. Wrap meta\-characters in brackets for a literal match (i.e. \fI[?]\fP to match the literal character \fI?\fP). For a path to match a pattern, it must completely match from start to end, or @@ -61,6 +62,7 @@ separator, a \(aq*\(aq is appended before matching is attempted. Shell\-style patterns, selector \fIsh:\fP .INDENT 0.0 .INDENT 3.5 +This is the default style for \-\-pattern and \-\-patterns\-from. Like fnmatch patterns these are similar to shell patterns. The difference is that the pattern may include \fI**/\fP for matching zero or more directory levels, \fI*\fP for matching zero or more arbitrary characters with the @@ -82,7 +84,7 @@ the re module\fP\&. .UNINDENT .UNINDENT .sp -Prefix path, selector \fIpp:\fP +Path prefix, selector \fIpp:\fP .INDENT 0.0 .INDENT 3.5 This pattern style is useful to match whole sub\-directories. The pattern @@ -90,6 +92,25 @@ This pattern style is useful to match whole sub\-directories. The pattern .UNINDENT .UNINDENT .sp +Path full\-match, selector \fIpf:\fP +.INDENT 0.0 +.INDENT 3.5 +This pattern style is useful to match whole paths. +This is kind of a pseudo pattern as it can not have any variable or +unspecified parts \- the full, precise path must be given. +\fIpf:/data/foo.txt\fP matches \fI/data/foo.txt\fP only. +.sp +Implementation note: this is implemented via very time\-efficient O(1) +hashtable lookups (this means you can have huge amounts of such patterns +without impacting performance much). +Due to that, this kind of pattern does not respect any context or order. +If you use such a pattern to include a file, it will always be included +(if the directory recursion encounters it). +Other include/exclude patterns that would normally match will be ignored. +Same logic applies for exclude. +.UNINDENT +.UNINDENT +.sp Exclusions can be passed via the command line option \fI\-\-exclude\fP\&. When used from within a shell the patterns should be quoted to protect them from expansion. @@ -138,6 +159,47 @@ $ borg create \-\-exclude\-from exclude.txt backup / .fi .UNINDENT .UNINDENT +.sp +A more general and easier to use way to define filename matching patterns exists +with the \fI\-\-pattern\fP and \fI\-\-patterns\-from\fP options. Using these, you may specify +the backup roots (starting points) and patterns for inclusion/exclusion. A +root path starts with the prefix \fIR\fP, followed by a path (a plain path, not a +file pattern). An include rule starts with the prefix +, an exclude rule starts +with the prefix \-, both followed by a pattern. +Inclusion patterns are useful to include pathes that are contained in an excluded +path. The first matching pattern is used so if an include pattern matches before +an exclude pattern, the file is backed up. +.sp +Note that the default pattern style for \fI\-\-pattern\fP and \fI\-\-patterns\-from\fP is +shell style (\fIsh:\fP), so those patterns behave similar to rsync include/exclude +patterns. The pattern style can be set via the \fIP\fP prefix. +.sp +Patterns (\fI\-\-pattern\fP) and excludes (\fI\-\-exclude\fP) from the command line are +considered first (in the order of appearance). Then patterns from \fI\-\-patterns\-from\fP +are added. Exclusion patterns from \fI\-\-exclude\-from\fP files are appended last. +.sp +An example \fI\-\-patterns\-from\fP file could look like that: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# "sh:" pattern style is the default, so the following line is not needed: +P sh +R / +# can be rebuild +\- /home/*/.cache +# they\(aqre downloads for a reason +\- /home/*/Downloads +# susan is a nice person +# include susans home ++ /home/susan +# don\(aqt backup the other home directories +\- /home/* +.ft P +.fi +.UNINDENT +.UNINDENT .SH AUTHOR The Borg Collective .\" Generated by docutils manpage writer. diff --git a/docs/man/borg-placeholders.1 b/docs/man/borg-placeholders.1 index 49cfb6dc..aa62bb9b 100644 --- a/docs/man/borg-placeholders.1 +++ b/docs/man/borg-placeholders.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PLACEHOLDERS 1 "2017-02-11" "" "borg backup tool" +.TH BORG-PLACEHOLDERS 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-placeholders \- Details regarding placeholders . diff --git a/docs/man/borg-prune.1 b/docs/man/borg-prune.1 index ff8afa1d..13541544 100644 --- a/docs/man/borg-prune.1 +++ b/docs/man/borg-prune.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PRUNE 1 "2017-02-11" "" "borg backup tool" +.TH BORG-PRUNE 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-prune \- Prune repository archives according to specified rules . diff --git a/docs/man/borg-recreate.1 b/docs/man/borg-recreate.1 index db822491..c7e30420 100644 --- a/docs/man/borg-recreate.1 +++ b/docs/man/borg-recreate.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-RECREATE 1 "2017-02-11" "" "borg backup tool" +.TH BORG-RECREATE 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-recreate \- Re-create archives . @@ -39,9 +39,9 @@ Recreate the contents of existing archives. .sp This is an \fIexperimental\fP feature. Do \fInot\fP use this on your only backup. .sp -\-\-exclude, \-\-exclude\-from and PATH have the exact same semantics -as in "borg create". If PATHs are specified the resulting archive -will only contain files from these PATHs. +\-\-exclude, \-\-exclude\-from, \-\-exclude\-if\-present, \-\-keep\-exclude\-tags, and PATH +have the exact same semantics as in "borg create". If PATHs are specified the +resulting archive will only contain files from these PATHs. .sp Note that all paths in an archive are relative, therefore absolute patterns/paths will \fInot\fP match (\-\-exclude, \-\-exclude\-from, \-\-compression\-from, PATHs). @@ -114,10 +114,16 @@ read exclude patterns from EXCLUDEFILE, one per line exclude directories that contain a CACHEDIR.TAG file (\fI\%http://www.brynosaurus.com/cachedir/spec.html\fP) .TP .BI \-\-exclude\-if\-present \ NAME -exclude directories that are tagged by containing a filesystem object with the given NAME +exclude directories that are tagged by containing a filesystem object with the given NAME .TP .B \-\-keep\-exclude\-tags\fP,\fB \-\-keep\-tag\-files -keep tag objects (i.e.: arguments to \-\-exclude\-if\-present) in otherwise excluded caches/directories +if tag objects are specified with \-\-exclude\-if\-present, don\(aqt omit the tag objects themselves from the backup archive +.TP +.BI \-\-pattern \ PATTERN +include/exclude paths matching PATTERN +.TP +.BI \-\-patterns\-from \ PATTERNFILE +read include/exclude patterns from PATTERNFILE, one per line .UNINDENT .SS Archive options .INDENT 0.0 diff --git a/docs/man/borg-rename.1 b/docs/man/borg-rename.1 index e3c1ee77..764ec91e 100644 --- a/docs/man/borg-rename.1 +++ b/docs/man/borg-rename.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-RENAME 1 "2017-02-11" "" "borg backup tool" +.TH BORG-RENAME 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-rename \- Rename an existing archive . diff --git a/docs/man/borg-serve.1 b/docs/man/borg-serve.1 index ee62ee7f..d6a6fbcd 100644 --- a/docs/man/borg-serve.1 +++ b/docs/man/borg-serve.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-SERVE 1 "2017-02-11" "" "borg backup tool" +.TH BORG-SERVE 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-serve \- Start in server mode. This command is usually not used manually. . diff --git a/docs/man/borg-umount.1 b/docs/man/borg-umount.1 index 7d8af587..c8cea6f4 100644 --- a/docs/man/borg-umount.1 +++ b/docs/man/borg-umount.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-UMOUNT 1 "2017-02-11" "" "borg backup tool" +.TH BORG-UMOUNT 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-umount \- un-mount the FUSE filesystem . diff --git a/docs/man/borg-upgrade.1 b/docs/man/borg-upgrade.1 index 0c0df0d5..c8111a4e 100644 --- a/docs/man/borg-upgrade.1 +++ b/docs/man/borg-upgrade.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-UPGRADE 1 "2017-02-11" "" "borg backup tool" +.TH BORG-UPGRADE 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-upgrade \- upgrade a repository from a previous version . diff --git a/docs/man/borg-with-lock.1 b/docs/man/borg-with-lock.1 index 2db9c3fc..7ed9d53e 100644 --- a/docs/man/borg-with-lock.1 +++ b/docs/man/borg-with-lock.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-WITH-LOCK 1 "2017-02-11" "" "borg backup tool" +.TH BORG-WITH-LOCK 1 "2017-03-26" "" "borg backup tool" .SH NAME borg-with-lock \- run a user specified command with the repository lock held . diff --git a/docs/man/borg.1 b/docs/man/borg.1 index b720d71c..7dffe320 100644 --- a/docs/man/borg.1 +++ b/docs/man/borg.1 @@ -336,6 +336,10 @@ Main usecase for this is to fully automate \fBborg change\-passphrase\fP\&. .B BORG_DISPLAY_PASSPHRASE When set, use the value to answer the "display the passphrase for verification" question when defining a new passphrase for encrypted repositories. .TP +.B BORG_HOSTNAME_IS_UNIQUE=yes +Use this to assert that your hostname is unique. +Borg will then automatically remove locks that it could determine to be stale. +.TP .B BORG_LOGGING_CONF When set, use the given filename as \fI\%INI\fP\-style logging configuration. .TP @@ -344,7 +348,7 @@ When set, use this command instead of \fBssh\fP\&. This can be used to specify s a custom identity file \fBssh \-i /path/to/private/key\fP\&. See \fBman ssh\fP for other options. .TP .B BORG_REMOTE_PATH -When set, use the given path/filename as remote path (default is "borg"). +When set, use the given path as borg executable on the remote (defaults to "borg" if unset). Using \fB\-\-remote\-path PATH\fP commandline option overrides the environment variable. .TP .B BORG_FILES_CACHE_TTL From 38860b3f53407b7202f0b8464573c833eaa015db Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 27 Mar 2017 12:08:54 +0200 Subject: [PATCH 0755/1387] lz4 compress: lower max. buffer size, exception handling on the wheezy32 test machine, a test testing with corrupted data crashed with a MemoryError when it tried to get a ~800MB large buffer. MemoryError is now transformed to DecompressionError, so it gets handled better. Also, the bound for giving up is now much lower: 1GiB -> 128MiB. --- src/borg/compress.pyx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index 91616725..786e19fd 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -104,13 +104,16 @@ class LZ4(CompressorBase): # allocate more if isize * 3 is already bigger, to avoid having to resize often. osize = max(int(1.1 * 2**23), isize * 3) while True: - buf = buffer.get(osize) + try: + buf = buffer.get(osize) + except MemoryError: + raise DecompressionError('MemoryError') dest = buf with nogil: rsize = LZ4_decompress_safe(source, dest, isize, osize) if rsize >= 0: break - if osize > 2 ** 30: + if osize > 2 ** 27: # 128MiB (should be enough, considering max. repo obj size and very good compression) # this is insane, get out of here raise DecompressionError('lz4 decompress failed') # likely the buffer was too small, get a bigger one: From 963949812d2e82e34d2de8ec2cab47ea8e8c3f72 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 27 Mar 2017 12:38:19 +0200 Subject: [PATCH 0756/1387] vagrant: increase memory for parallel testing considering that we run pytest-xdist with 4 parallel processes, we need a bit more RAM to avoid the OOM killer and MemoryError. so, 1GiB for 32bit, 1.5GiB for 64bit VMs. --- Vagrantfile | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index babbe432..0a3e4af9 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -355,7 +355,7 @@ Vagrant.configure(2) do |config| config.vm.define "centos7_64" do |b| b.vm.box = "centos/7" b.vm.provider :virtualbox do |v| - v.memory = 768 + v.memory = 1536 end b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos7_64") @@ -367,6 +367,9 @@ Vagrant.configure(2) do |config| config.vm.define "centos6_32" do |b| b.vm.box = "centos6-32" + b.vm.provider :virtualbox do |v| + v.memory = 1024 + end b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos6_32") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("centos6_32") @@ -378,7 +381,7 @@ Vagrant.configure(2) do |config| config.vm.define "centos6_64" do |b| b.vm.box = "centos6-64" b.vm.provider :virtualbox do |v| - v.memory = 768 + v.memory = 1536 end b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos6_64") @@ -391,7 +394,7 @@ Vagrant.configure(2) do |config| config.vm.define "xenial64" do |b| b.vm.box = "ubuntu/xenial64" b.vm.provider :virtualbox do |v| - v.memory = 768 + v.memory = 1536 end b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("xenial64") @@ -402,7 +405,7 @@ Vagrant.configure(2) do |config| config.vm.define "trusty64" do |b| b.vm.box = "ubuntu/trusty64" b.vm.provider :virtualbox do |v| - v.memory = 768 + v.memory = 1536 end b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("trusty64") @@ -413,7 +416,7 @@ Vagrant.configure(2) do |config| config.vm.define "jessie64" do |b| b.vm.box = "debian/jessie64" b.vm.provider :virtualbox do |v| - v.memory = 768 + v.memory = 1536 end b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("jessie64") @@ -423,6 +426,9 @@ Vagrant.configure(2) do |config| config.vm.define "wheezy32" do |b| b.vm.box = "boxcutter/debian7-i386" + b.vm.provider :virtualbox do |v| + v.memory = 1024 + end b.vm.provision "packages prepare wheezy", :type => :shell, :inline => packages_prepare_wheezy b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("wheezy32") @@ -492,7 +498,7 @@ Vagrant.configure(2) do |config| config.vm.define "openbsd64" do |b| b.vm.box = "openbsd60-64" # note: basic openbsd install for vagrant WITH sudo and rsync pre-installed b.vm.provider :virtualbox do |v| - v.memory = 768 + v.memory = 1536 end b.vm.provision "packages openbsd", :type => :shell, :inline => packages_openbsd b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("openbsd64") @@ -503,7 +509,7 @@ Vagrant.configure(2) do |config| config.vm.define "netbsd64" do |b| b.vm.box = "netbsd70-64" b.vm.provider :virtualbox do |v| - v.memory = 768 + v.memory = 1536 end b.vm.provision "packages netbsd", :type => :shell, :inline => packages_netbsd b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("netbsd64") From 9d8b7ca0f5b37ff55d1c442a07fe02b01b763da6 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 28 Mar 2017 14:11:35 +0200 Subject: [PATCH 0757/1387] LICENSE: use canonical formulation ("copyright holders and contributors" instead of "author") --- LICENSE | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/LICENSE b/LICENSE index e9940124..1928806f 100644 --- a/LICENSE +++ b/LICENSE @@ -16,14 +16,14 @@ are met: products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS -OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From ceaf4a8fcfdee348428c945617ac77d9623e3cad Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 28 Mar 2017 22:02:54 +0200 Subject: [PATCH 0758/1387] extract: small bugfix and optimization for hardlink masters if a hardlink master is not in the to-be-extracted subset, the "x" status was not displayed for it. also, the matcher was called twice for matching items. --- src/borg/archive.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index bfc2d533..6070f3d5 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1614,15 +1614,13 @@ class ArchiveRecreater: return (target_is_subset and stat.S_ISREG(item.mode) and item.get('hardlink_master', True) and - 'source' not in item and - not matcher.match(item.path)) + 'source' not in item) for item in archive.iter_items(): - if item_is_hardlink_master(item): - hardlink_masters[item.path] = (item.get('chunks'), None) - continue if not matcher.match(item.path): self.print_file_status('x', item.path) + if item_is_hardlink_master(item): + hardlink_masters[item.path] = (item.get('chunks'), None) continue if target_is_subset and stat.S_ISREG(item.mode) and item.get('source') in hardlink_masters: # master of this hard link is outside the target subset From d4e27e2952888d547c54d5b0f4be22892aa9e2f1 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 28 Mar 2017 23:22:25 +0200 Subject: [PATCH 0759/1387] extract: small bugfix and refactoring for parent dir creation make_parent(path) helper to reduce code duplication. also use it for directories although makedirs can also do it. bugfix: also create parent dir for device files, if needed. --- src/borg/archive.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 6070f3d5..c24c9fae 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -559,11 +559,16 @@ Utilization of max. archive size: {csize_max:.0%} raise self.IncompatibleFilesystemEncodingError(path, sys.getfilesystemencoding()) from None except OSError: pass + + def make_parent(path): + parent_dir = os.path.dirname(path) + if not os.path.exists(parent_dir): + os.makedirs(parent_dir) + mode = item.mode if stat.S_ISREG(mode): with backup_io('makedirs'): - if not os.path.exists(os.path.dirname(path)): - os.makedirs(os.path.dirname(path)) + make_parent(path) # Hard link? if 'source' in item: source = os.path.join(dest, *item.source.split(os.sep)[stripped_components:]) @@ -615,13 +620,13 @@ Utilization of max. archive size: {csize_max:.0%} with backup_io: # No repository access beyond this point. if stat.S_ISDIR(mode): + make_parent(path) if not os.path.exists(path): - os.makedirs(path) + os.mkdir(path) if restore_attrs: self.restore_attrs(path, item) elif stat.S_ISLNK(mode): - if not os.path.exists(os.path.dirname(path)): - os.makedirs(os.path.dirname(path)) + make_parent(path) source = item.source if os.path.exists(path): os.unlink(path) @@ -631,11 +636,11 @@ Utilization of max. archive size: {csize_max:.0%} raise self.IncompatibleFilesystemEncodingError(source, sys.getfilesystemencoding()) from None self.restore_attrs(path, item, symlink=True) elif stat.S_ISFIFO(mode): - if not os.path.exists(os.path.dirname(path)): - os.makedirs(os.path.dirname(path)) + make_parent(path) os.mkfifo(path) self.restore_attrs(path, item) elif stat.S_ISCHR(mode) or stat.S_ISBLK(mode): + make_parent(path) os.mknod(path, item.mode, item.rdev) self.restore_attrs(path, item) else: From bdbcbf7bb8b10b3e255075430b71f00e67a9f5a0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 1 Apr 2017 16:56:21 +0200 Subject: [PATCH 0760/1387] extract: remove duplicate code anything at gets nuked already a few lines above, if possible. --- src/borg/archive.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index c24c9fae..5ed60570 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -573,8 +573,6 @@ Utilization of max. archive size: {csize_max:.0%} if 'source' in item: source = os.path.join(dest, *item.source.split(os.sep)[stripped_components:]) with backup_io('link'): - if os.path.exists(path): - os.unlink(path) if item.source not in hardlink_masters: os.link(source, path) return @@ -628,8 +626,6 @@ Utilization of max. archive size: {csize_max:.0%} elif stat.S_ISLNK(mode): make_parent(path) source = item.source - if os.path.exists(path): - os.unlink(path) try: os.symlink(source, path) except UnicodeEncodeError: From 132f0006d374ef668b59a0eec0a8f2a178af6a1c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 1 Apr 2017 18:25:45 +0200 Subject: [PATCH 0761/1387] enhance travis setuptools_scm situation * add setuptools_scm to the development requirements * print the own version at install time * unshallow the repo and fetch all tags --- .travis.yml | 1 + .travis/install.sh | 1 + requirements.d/development.txt | 1 + 3 files changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 1c9dbcaf..bfebcf67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,6 +43,7 @@ matrix: env: TOXENV=py36 install: + - git fetch --unshallow --tags - ./.travis/install.sh script: diff --git a/.travis/install.sh b/.travis/install.sh index fdfe0181..573cba08 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -48,4 +48,5 @@ python -m virtualenv ~/.venv source ~/.venv/bin/activate pip install -r requirements.d/development.txt pip install codecov +python setup.py --version pip install -e .[fuse] diff --git a/requirements.d/development.txt b/requirements.d/development.txt index de242810..5805cb4a 100644 --- a/requirements.d/development.txt +++ b/requirements.d/development.txt @@ -1,4 +1,5 @@ setuptools +setuptools_scm pip virtualenv tox From bb6b4fde93aec2b8f036ce416b6ba9da8b53e7c0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 1 Apr 2017 21:28:41 +0200 Subject: [PATCH 0762/1387] BORG_HOSTNAME_IS_UNIQUE=yes by default. --- docs/changes.rst | 7 +++++++ docs/usage_general.rst.inc | 7 ++++--- src/borg/cache.py | 7 ++----- src/borg/helpers.py | 4 ++++ src/borg/remote.py | 7 +++---- src/borg/repository.py | 7 ++----- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 4548856a..686ac9b7 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -128,6 +128,13 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= +Version 1.1.0b5 (not released) +------------------------------ + +Compatibility notes: + +- BORG_HOSTNAME_IS_UNIQUE is now on by default. + Version 1.1.0b4 (2017-03-27) ---------------------------- diff --git a/docs/usage_general.rst.inc b/docs/usage_general.rst.inc index 160a4649..addadc5b 100644 --- a/docs/usage_general.rst.inc +++ b/docs/usage_general.rst.inc @@ -140,9 +140,10 @@ General: Main usecase for this is to fully automate ``borg change-passphrase``. BORG_DISPLAY_PASSPHRASE When set, use the value to answer the "display the passphrase for verification" question when defining a new passphrase for encrypted repositories. - BORG_HOSTNAME_IS_UNIQUE=yes - Use this to assert that your hostname is unique. - Borg will then automatically remove locks that it could determine to be stale. + BORG_HOSTNAME_IS_UNIQUE=no + Borg assumes that it can derive a unique hostname / identity (see ``borg debug info``). + If this is not the case or you do not want Borg to automatically remove stale locks, + set this to *no*. BORG_LOGGING_CONF When set, use the given filename as INI_-style logging configuration. BORG_RSH diff --git a/src/borg/cache.py b/src/borg/cache.py index b3d7e12f..0eeb7b96 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -18,7 +18,7 @@ from .helpers import get_cache_dir, get_security_dir from .helpers import int_to_bigint, bigint_to_int, bin_to_hex from .helpers import format_file_size from .helpers import safe_ns -from .helpers import yes +from .helpers import yes, hostname_is_unique from .helpers import remove_surrogates from .helpers import ProgressIndicatorPercent, ProgressIndicatorMessage from .item import Item, ArchiveItem, ChunkListEntry @@ -187,9 +187,6 @@ class Cache: self.progress = progress self.path = path or os.path.join(get_cache_dir(), repository.id_str) self.security_manager = SecurityManager(repository) - self.hostname_is_unique = yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', prompt=False, env_msg=None) - if self.hostname_is_unique: - logger.info('Enabled removal of stale cache locks') self.do_files = do_files # Warn user before sending data to a never seen before unencrypted repository if not os.path.exists(self.path): @@ -295,7 +292,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def open(self, lock_wait=None): if not os.path.isdir(self.path): raise Exception('%s Does not look like a Borg cache' % self.path) - self.lock = Lock(os.path.join(self.path, 'lock'), exclusive=True, timeout=lock_wait, kill_stale_locks=self.hostname_is_unique).acquire() + self.lock = Lock(os.path.join(self.path, 'lock'), exclusive=True, timeout=lock_wait, kill_stale_locks=hostname_is_unique()).acquire() self.rollback() def close(self): diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 2e343e4e..2892139d 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1478,6 +1478,10 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, env_var_override = None +def hostname_is_unique(): + return yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', prompt=False, env_msg=None, default=True) + + def ellipsis_truncate(msg, space): """ shorten a long string by adding ellipsis between it and return it, example: diff --git a/src/borg/remote.py b/src/borg/remote.py index d3af8270..b2a9938c 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -8,7 +8,6 @@ import select import shlex import sys import tempfile -import time import traceback import textwrap import time @@ -22,7 +21,7 @@ from .helpers import get_home_dir from .helpers import sysinfo from .helpers import bin_to_hex from .helpers import replace_placeholders -from .helpers import yes +from .helpers import hostname_is_unique from .repository import Repository, MAX_OBJECT_SIZE, LIST_SCAN_LIMIT from .version import parse_version, format_version from .logger import create_logger @@ -646,8 +645,8 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. except AttributeError: pass env_vars = [] - if yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', env_msg=None, prompt=False): - env_vars.append('BORG_HOSTNAME_IS_UNIQUE=yes') + if not hostname_is_unique(): + env_vars.append('BORG_HOSTNAME_IS_UNIQUE=no') if testing: return env_vars + [sys.executable, '-m', 'borg.archiver', 'serve'] + opts + self.extra_test_args else: # pragma: no cover diff --git a/src/borg/repository.py b/src/borg/repository.py index 2073eec0..47a52015 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -17,7 +17,7 @@ from .helpers import Error, ErrorWithTraceback, IntegrityError, format_file_size from .helpers import Location from .helpers import ProgressIndicatorPercent from .helpers import bin_to_hex -from .helpers import yes +from .helpers import hostname_is_unique from .helpers import secure_erase from .locking import Lock, LockError, LockErrorT from .logger import create_logger @@ -124,9 +124,6 @@ class Repository: self.created = False self.exclusive = exclusive self.append_only = append_only - self.hostname_is_unique = yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', env_msg=None, prompt=False) - if self.hostname_is_unique: - logger.info('Enabled removal of stale repository locks') def __del__(self): if self.lock: @@ -279,7 +276,7 @@ class Repository: if not os.path.isdir(path): raise self.DoesNotExist(path) if lock: - self.lock = Lock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait, kill_stale_locks=self.hostname_is_unique).acquire() + self.lock = Lock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait, kill_stale_locks=hostname_is_unique()).acquire() else: self.lock = None self.config = ConfigParser(interpolation=None) From a27f585eaabd94f1ed76bc82416696326af128b8 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 31 Mar 2017 12:02:30 +0200 Subject: [PATCH 0763/1387] refactor CompressionDecider2 into a meta Compressor --- src/borg/archive.py | 9 ++-- src/borg/archiver.py | 7 +++- src/borg/compress.pyx | 75 ++++++++++++++++++++++++++++++++++ src/borg/helpers.py | 75 +++------------------------------- src/borg/key.py | 15 +++---- src/borg/testsuite/compress.py | 47 ++++++++++++++++++++- src/borg/testsuite/helpers.py | 32 +-------------- 7 files changed, 142 insertions(+), 118 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 5ed60570..392efce2 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -21,7 +21,7 @@ logger = create_logger() from . import xattr from .cache import ChunkListEntry from .chunker import Chunker -from .compress import Compressor +from .compress import Compressor, CompressionSpec from .constants import * # NOQA from .hashindex import ChunkIndex, ChunkIndexEntry from .helpers import Manifest @@ -36,7 +36,7 @@ from .helpers import bin_to_hex from .helpers import safe_ns from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi from .helpers import PathPrefixPattern, FnmatchPattern -from .helpers import CompressionDecider1, CompressionDecider2, CompressionSpec +from .helpers import CompressionDecider1 from .item import Item, ArchiveItem from .key import key_factory from .platform import acl_get, acl_set, set_flags, get_flags, swidth @@ -312,7 +312,6 @@ class Archive: self.chunker = Chunker(self.key.chunk_seed, *chunker_params) self.compression_decider1 = CompressionDecider1(compression or CompressionSpec('none'), compression_files or []) - key.compression_decider2 = CompressionDecider2(compression or CompressionSpec('none')) if name in manifest.archives: raise self.AlreadyExists(name) self.last_checkpoint = time.monotonic() @@ -1585,7 +1584,6 @@ class ArchiveRecreater: self.seen_chunks = set() self.compression_decider1 = CompressionDecider1(compression or CompressionSpec('none'), compression_files or []) - key.compression_decider2 = CompressionDecider2(compression or CompressionSpec('none')) self.dry_run = dry_run self.stats = stats @@ -1663,12 +1661,11 @@ class ArchiveRecreater: if chunk_id in self.seen_chunks: return self.cache.chunk_incref(chunk_id, target.stats) chunk = Chunk(data, compress=compress) - compression_spec, chunk = self.key.compression_decider2.decide(chunk) overwrite = self.recompress if self.recompress and not self.always_recompress and chunk_id in self.cache.chunks: # Check if this chunk is already compressed the way we want it old_chunk = self.key.decrypt(None, self.repository.get(chunk_id), decompress=False) - if Compressor.detect(old_chunk.data).name == compression_spec.name: + if Compressor.detect(old_chunk.data).name == compress.name: # Stored chunk has the same compression we wanted overwrite = False chunk_entry = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite, wait=False) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e241002e..43aa24c1 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -34,10 +34,11 @@ from .archive import Archive, ArchiveChecker, ArchiveRecreater, Statistics, is_s from .archive import BackupOSError, backup_io from .cache import Cache from .constants import * # NOQA +from .compress import CompressionSpec from .crc32 import crc32 from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from .helpers import Error, NoManifestError, set_ec -from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec, ComprSpec +from .helpers import location_validator, archivename_validator, ChunkerParams from .helpers import PrefixSpec, SortBySpec, HUMAN_SORT_KEYS from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter from .helpers import format_time, format_timedelta, format_file_size, format_archive @@ -107,6 +108,8 @@ def with_repository(fake=False, invert_fake=False, create=False, lock=True, excl with repository: if manifest or cache: kwargs['manifest'], kwargs['key'] = Manifest.load(repository) + if args.__dict__.get('compression'): + kwargs['key'].compressor = args.compression.compressor if cache: with Cache(repository, kwargs['key'], kwargs['manifest'], do_files=getattr(args, 'cache_files', False), @@ -2411,7 +2414,7 @@ class Archiver: help='specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, ' 'HASH_MASK_BITS, HASH_WINDOW_SIZE). default: %d,%d,%d,%d' % CHUNKER_PARAMS) archive_group.add_argument('-C', '--compression', dest='compression', - type=CompressionSpec, default=ComprSpec(name='lz4', spec=None), metavar='COMPRESSION', + type=CompressionSpec, default=CompressionSpec('lz4'), metavar='COMPRESSION', help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') archive_group.add_argument('--compression-from', dest='compression_files', diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index 786e19fd..2da2389e 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -1,9 +1,12 @@ import zlib +from collections import namedtuple + try: import lzma except ImportError: lzma = None +from .logger import create_logger from .helpers import Buffer, DecompressionError API_VERSION = '1.1_02' @@ -179,12 +182,50 @@ class ZLIB(CompressorBase): raise DecompressionError(str(e)) from None +class Auto(CompressorBase): + """ + Meta-Compressor that decides which compression to use based on LZ4's ratio. + + As a meta-Compressor the actual compression is deferred to other Compressors, + therefore this Compressor has no ID, no detect() and no decompress(). + """ + + ID = None + name = 'auto' + + logger = create_logger('borg.debug.file-compression') + + def __init__(self, compressor): + super().__init__() + self.compressor = compressor + self.lz4 = get_compressor('lz4') + self.none = get_compressor('none') + + def compress(self, data): + lz4_data = self.lz4.compress(data) + if len(lz4_data) < 0.97 * len(data): + return self.compressor.compress(data) + elif len(lz4_data) < len(data): + return lz4_data + else: + return self.none.compress(data) + + def decompress(self, data): + raise NotImplementedError + + def detect(cls, data): + raise NotImplementedError + + +# Maps valid compressor names to their class COMPRESSOR_TABLE = { CNONE.name: CNONE, LZ4.name: LZ4, ZLIB.name: ZLIB, LZMA.name: LZMA, + Auto.name: Auto, } +# List of possible compression types. Does not include Auto, since it is a meta-Compressor. COMPRESSOR_LIST = [LZ4, CNONE, ZLIB, LZMA, ] # check fast stuff first def get_compressor(name, **kwargs): @@ -216,3 +257,37 @@ class Compressor: return cls else: raise ValueError('No decompressor for this data found: %r.', data[:2]) + + +ComprSpec = namedtuple('ComprSpec', ('name', 'spec', 'compressor')) + + +def CompressionSpec(s): + values = s.split(',') + count = len(values) + if count < 1: + raise ValueError + # --compression algo[,level] + name = values[0] + if name == 'none': + return ComprSpec(name=name, spec=None, compressor=CNONE()) + elif name == 'lz4': + return ComprSpec(name=name, spec=None, compressor=LZ4()) + if name in ('zlib', 'lzma', ): + if count < 2: + level = 6 # default compression level in py stdlib + elif count == 2: + level = int(values[1]) + if not 0 <= level <= 9: + raise ValueError + else: + raise ValueError + return ComprSpec(name=name, spec=level, compressor=get_compressor(name, level=level)) + if name == 'auto': + if 2 <= count <= 3: + compression = ','.join(values[1:]) + else: + raise ValueError + inner = CompressionSpec(compression) + return ComprSpec(name=name, spec=inner, compressor=Auto(inner.compressor)) + raise ValueError diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 2e343e4e..685d9f43 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -726,37 +726,6 @@ def ChunkerParams(s): return int(chunk_min), int(chunk_max), int(chunk_mask), int(window_size) -ComprSpec = namedtuple('ComprSpec', ('name', 'spec')) - - -def CompressionSpec(s): - values = s.split(',') - count = len(values) - if count < 1: - raise ValueError - # --compression algo[,level] - name = values[0] - if name in ('none', 'lz4', ): - return ComprSpec(name=name, spec=None) - if name in ('zlib', 'lzma', ): - if count < 2: - level = 6 # default compression level in py stdlib - elif count == 2: - level = int(values[1]) - if not 0 <= level <= 9: - raise ValueError - else: - raise ValueError - return ComprSpec(name=name, spec=level) - if name == 'auto': - if 2 <= count <= 3: - compression = ','.join(values[1:]) - else: - raise ValueError - return ComprSpec(name=name, spec=CompressionSpec(compression)) - raise ValueError - - def dir_is_cachedir(path): """Determines whether the specified path is a cache directory (and therefore should potentially be excluded from the backup) according to @@ -2136,11 +2105,12 @@ class CompressionDecider1: :param compression_files: list of compression config files (e.g. from --compression-from) or a list of other line iterators """ - self.compression = compression + from .compress import CompressionSpec + self.compressor = compression.compressor if not compression_files: self.matcher = None else: - self.matcher = PatternMatcher(fallback=compression) + self.matcher = PatternMatcher(fallback=compression.compressor) for file in compression_files: try: for line in clean_lines(file): @@ -2148,7 +2118,7 @@ class CompressionDecider1: compr_spec, fn_pattern = line.split(':', 1) except: continue - self.matcher.add([parse_pattern(fn_pattern)], CompressionSpec(compr_spec)) + self.matcher.add([parse_pattern(fn_pattern)], CompressionSpec(compr_spec).compressor) finally: if hasattr(file, 'close'): file.close() @@ -2156,42 +2126,7 @@ class CompressionDecider1: def decide(self, path): if self.matcher is not None: return self.matcher.match(path) - return self.compression - - -class CompressionDecider2: - logger = create_logger('borg.debug.file-compression') - - def __init__(self, compression): - self.compression = compression - - def decide(self, chunk): - # nothing fancy here yet: we either use what the metadata says or the default - # later, we can decide based on the chunk data also. - # if we compress the data here to decide, we can even update the chunk data - # and modify the metadata as desired. - compr_spec = chunk.meta.get('compress', self.compression) - if compr_spec.name == 'auto': - # we did not decide yet, use heuristic: - compr_spec, chunk = self.heuristic_lz4(compr_spec, chunk) - return compr_spec, chunk - - def heuristic_lz4(self, compr_args, chunk): - from .compress import get_compressor - meta, data = chunk - lz4 = get_compressor('lz4') - cdata = lz4.compress(data) - data_len = len(data) - cdata_len = len(cdata) - if cdata_len < 0.97 * data_len: - compr_spec = compr_args.spec - else: - # uncompressible - we could have a special "uncompressible compressor" - # that marks such data as uncompressible via compression-type metadata. - compr_spec = CompressionSpec('none') - self.logger.debug("len(data) == %d, len(lz4(data)) == %d, ratio == %.3f, choosing %s", data_len, cdata_len, cdata_len/data_len, compr_spec) - meta['compress'] = compr_spec - return compr_spec, Chunk(data, **meta) + return self.compressor class ErrorIgnoringTextIOWrapper(io.TextIOWrapper): diff --git a/src/borg/key.py b/src/borg/key.py index 3d3cfc53..d3bf22b1 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -13,14 +13,13 @@ from .logger import create_logger logger = create_logger() from .constants import * # NOQA -from .compress import Compressor, get_compressor +from .compress import Compressor from .crypto import AES, bytes_to_long, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512 from .helpers import Chunk, StableDict from .helpers import Error, IntegrityError from .helpers import yes from .helpers import get_keys_dir, get_security_dir from .helpers import bin_to_hex -from .helpers import CompressionDecider2, CompressionSpec from .item import Key, EncryptedKey from .platform import SaveFile from .nonces import NonceManager @@ -143,8 +142,8 @@ class KeyBase: self.TYPE_STR = bytes([self.TYPE]) self.repository = repository self.target = None # key location file path / repo obj - self.compression_decider2 = CompressionDecider2(CompressionSpec('none')) self.compressor = Compressor('none') # for decompression + self.decompress = self.compressor.decompress self.tam_required = True def id_hash(self, data): @@ -152,10 +151,8 @@ class KeyBase: """ def compress(self, chunk): - compr_args, chunk = self.compression_decider2.decide(chunk) - compressor = Compressor(name=compr_args.name, level=compr_args.spec) meta, data = chunk - data = compressor.compress(data) + data = meta.get('compress', self.compressor).compress(data) return Chunk(data, **meta) def encrypt(self, chunk): @@ -268,7 +265,7 @@ class PlaintextKey(KeyBase): payload = memoryview(data)[1:] if not decompress: return Chunk(payload) - data = self.compressor.decompress(payload) + data = self.decompress(payload) self.assert_id(id, data) return Chunk(data) @@ -362,7 +359,7 @@ class AESKeyBase(KeyBase): payload = self.dec_cipher.decrypt(data_view[41:]) if not decompress: return Chunk(payload) - data = self.compressor.decompress(payload) + data = self.decompress(payload) self.assert_id(id, data) return Chunk(data) @@ -757,7 +754,7 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): payload = memoryview(data)[1:] if not decompress: return Chunk(payload) - data = self.compressor.decompress(payload) + data = self.decompress(payload) self.assert_id(id, data) return Chunk(data) diff --git a/src/borg/testsuite/compress.py b/src/borg/testsuite/compress.py index ff9d4271..9bcf595c 100644 --- a/src/borg/testsuite/compress.py +++ b/src/borg/testsuite/compress.py @@ -7,7 +7,7 @@ except ImportError: import pytest -from ..compress import get_compressor, Compressor, CNONE, ZLIB, LZ4 +from ..compress import get_compressor, Compressor, CompressionSpec, ComprSpec, CNONE, ZLIB, LZ4, LZMA, Auto buffer = bytes(2**16) @@ -107,3 +107,48 @@ def test_compressor(): for params in params_list: c = Compressor(**params) assert data == c.decompress(c.compress(data)) + + +def test_auto(): + compressor = CompressionSpec('auto,zlib,9').compressor + + compressed = compressor.compress(bytes(500)) + assert Compressor.detect(compressed) == ZLIB + + compressed = compressor.compress(b'\x00\xb8\xa3\xa2-O\xe1i\xb6\x12\x03\xc21\xf3\x8a\xf78\\\x01\xa5b\x07\x95\xbeE\xf8\xa3\x9ahm\xb1~') + assert Compressor.detect(compressed) == CNONE + + +def test_compression_specs(): + with pytest.raises(ValueError): + CompressionSpec('') + + assert isinstance(CompressionSpec('none').compressor, CNONE) + assert isinstance(CompressionSpec('lz4').compressor, LZ4) + + zlib = CompressionSpec('zlib').compressor + assert isinstance(zlib, ZLIB) + assert zlib.level == 6 + zlib = CompressionSpec('zlib,0').compressor + assert isinstance(zlib, ZLIB) + assert zlib.level == 0 + zlib = CompressionSpec('zlib,9').compressor + assert isinstance(zlib, ZLIB) + assert zlib.level == 9 + with pytest.raises(ValueError): + CompressionSpec('zlib,9,invalid') + + lzma = CompressionSpec('lzma').compressor + assert isinstance(lzma, LZMA) + assert lzma.level == 6 + lzma = CompressionSpec('lzma,0').compressor + assert isinstance(lzma, LZMA) + assert lzma.level == 0 + lzma = CompressionSpec('lzma,9').compressor + assert isinstance(lzma, LZMA) + assert lzma.level == 9 + + with pytest.raises(ValueError): + CompressionSpec('lzma,9,invalid') + with pytest.raises(ValueError): + CompressionSpec('invalid') diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 19c5e9c5..b905a18d 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -12,6 +12,7 @@ import msgpack import msgpack.fallback from .. import platform +from ..compress import CompressionSpec from ..helpers import Location from ..helpers import Buffer from ..helpers import partial_format, format_file_size, parse_file_size, format_timedelta, format_line, PlaceholderError, replace_placeholders @@ -24,7 +25,7 @@ from ..helpers import StableDict, int_to_bigint, bigint_to_int, bin_to_hex from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams, Chunk from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless from ..helpers import load_exclude_file, load_pattern_file -from ..helpers import CompressionSpec, ComprSpec, CompressionDecider1, CompressionDecider2 +from ..helpers import CompressionDecider1 from ..helpers import parse_pattern, PatternMatcher from ..helpers import PathFullPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, RegexPattern from ..helpers import swidth_slice @@ -698,25 +699,6 @@ def test_pattern_matcher(): assert PatternMatcher(fallback="hey!").fallback == "hey!" -def test_compression_specs(): - with pytest.raises(ValueError): - CompressionSpec('') - assert CompressionSpec('none') == ComprSpec(name='none', spec=None) - assert CompressionSpec('lz4') == ComprSpec(name='lz4', spec=None) - assert CompressionSpec('zlib') == ComprSpec(name='zlib', spec=6) - assert CompressionSpec('zlib,0') == ComprSpec(name='zlib', spec=0) - assert CompressionSpec('zlib,9') == ComprSpec(name='zlib', spec=9) - with pytest.raises(ValueError): - CompressionSpec('zlib,9,invalid') - assert CompressionSpec('lzma') == ComprSpec(name='lzma', spec=6) - assert CompressionSpec('lzma,0') == ComprSpec(name='lzma', spec=0) - assert CompressionSpec('lzma,9') == ComprSpec(name='lzma', spec=9) - with pytest.raises(ValueError): - CompressionSpec('lzma,9,invalid') - with pytest.raises(ValueError): - CompressionSpec('invalid') - - def test_chunkerparams(): assert ChunkerParams('19,23,21,4095') == (19, 23, 21, 4095) assert ChunkerParams('10,23,16,4095') == (10, 23, 16, 4095) @@ -1242,16 +1224,6 @@ none:*.zip assert cd.decide('test').name == 'zlib' # no match in conf, use default -def test_compression_decider2(): - default = CompressionSpec('zlib') - - cd = CompressionDecider2(default) - compr_spec, chunk = cd.decide(Chunk(None)) - assert compr_spec.name == 'zlib' - compr_spec, chunk = cd.decide(Chunk(None, compress=CompressionSpec('lzma'))) - assert compr_spec.name == 'lzma' - - def test_format_line(): data = dict(foo='bar baz') assert format_line('', data) == '' From 5a20fc08de19e9191c58b06cb7180fee1af1c2d4 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 31 Mar 2017 12:17:58 +0200 Subject: [PATCH 0764/1387] key: compress(chunk) return data --- src/borg/key.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/borg/key.py b/src/borg/key.py index d3bf22b1..b7d2cf60 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -152,8 +152,7 @@ class KeyBase: def compress(self, chunk): meta, data = chunk - data = meta.get('compress', self.compressor).compress(data) - return Chunk(data, **meta) + return meta.get('compress', self.compressor).compress(data) def encrypt(self, chunk): pass @@ -255,8 +254,8 @@ class PlaintextKey(KeyBase): return sha256(data).digest() def encrypt(self, chunk): - chunk = self.compress(chunk) - return b''.join([self.TYPE_STR, chunk.data]) + data = self.compress(chunk) + return b''.join([self.TYPE_STR, data]) def decrypt(self, id, data, decompress=True): if data[0] != self.TYPE: @@ -333,10 +332,10 @@ class AESKeyBase(KeyBase): MAC = hmac_sha256 def encrypt(self, chunk): - chunk = self.compress(chunk) - self.nonce_manager.ensure_reservation(num_aes_blocks(len(chunk.data))) + data = self.compress(chunk) + self.nonce_manager.ensure_reservation(num_aes_blocks(len(data))) self.enc_cipher.reset() - data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(chunk.data))) + data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(data))) assert (self.MAC is blake2b_256 and len(self.enc_hmac_key) == 128 or self.MAC is hmac_sha256 and len(self.enc_hmac_key) == 32) hmac = self.MAC(self.enc_hmac_key, data) @@ -745,8 +744,8 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): STORAGE = KeyBlobStorage.REPO def encrypt(self, chunk): - chunk = self.compress(chunk) - return b''.join([self.TYPE_STR, chunk.data]) + data = self.compress(chunk) + return b''.join([self.TYPE_STR, data]) def decrypt(self, id, data, decompress=True): if data[0] != self.TYPE: From 88647595ac52b2301b62f2893fbd1b5c94ae3ffb Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 31 Mar 2017 13:39:54 +0200 Subject: [PATCH 0765/1387] compress: docs --- src/borg/compress.pyx | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index 2da2389e..8cd09450 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -1,3 +1,28 @@ +""" +borg.compress +============= + +Compression is applied to chunks after ID hashing (so the ID is a direct function of the +plain chunk, compression is irrelevant to it), and of course before encryption. + +Borg has a flexible scheme for deciding which compression to use for chunks. + +First, there is a global default set by the --compression command line option, +which sets the .compressor attribute on the Key. + +For chunks that emanate from files CompressionDecider1 may set a specific +Compressor based on patterns (this is the --compression-from option). This is stored +as a Compressor instance in the "compress" key in the Chunk's meta dictionary. + +When compressing either the Compressor specified in the Chunk's meta dictionary +is used, or the default Compressor of the key. + +The "auto" mode (e.g. --compression auto,lzma,4) is implemented as a meta Compressor, +meaning that Auto acts like a Compressor, but defers actual work to others (namely +LZ4 as a heuristic whether compression is worth it, and the specified Compressor +for the actual compression). +""" + import zlib from collections import namedtuple From 0c7410104cd5d71f761ae31bb28bb6a35f6e8063 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 31 Mar 2017 13:43:48 +0200 Subject: [PATCH 0766/1387] Rename Chunk.meta[compress] => Chunk.meta[compressor] --- src/borg/archive.py | 16 ++++++++-------- src/borg/compress.pyx | 10 +++++++--- src/borg/key.py | 2 +- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 392efce2..da383e50 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -970,12 +970,12 @@ Utilization of max. archive size: {csize_max:.0%} if chunks is not None: item.chunks = chunks else: - compress = self.compression_decider1.decide(path) - self.file_compression_logger.debug('%s -> compression %s', path, compress.name) + compressor = self.compression_decider1.decide(path) + self.file_compression_logger.debug('%s -> compression %s', path, compressor.name) with backup_io('open'): fh = Archive._open_rb(path) with os.fdopen(fh, 'rb') as fd: - self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd, fh)), compress=compress) + self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd, fh)), compressor=compressor) if not is_special_file: # we must not memorize special files, because the contents of e.g. a # block or char device will change without its mtime/size/inode changing. @@ -1652,20 +1652,20 @@ class ArchiveRecreater: self.cache.chunk_incref(chunk_id, target.stats) return item.chunks chunk_iterator = self.iter_chunks(archive, target, list(item.chunks)) - compress = self.compression_decider1.decide(item.path) - chunk_processor = partial(self.chunk_processor, target, compress) + compressor = self.compression_decider1.decide(item.path) + chunk_processor = partial(self.chunk_processor, target, compressor) target.chunk_file(item, self.cache, target.stats, chunk_iterator, chunk_processor) - def chunk_processor(self, target, compress, data): + def chunk_processor(self, target, compressor, data): chunk_id = self.key.id_hash(data) if chunk_id in self.seen_chunks: return self.cache.chunk_incref(chunk_id, target.stats) - chunk = Chunk(data, compress=compress) + chunk = Chunk(data, compressor=compressor) overwrite = self.recompress if self.recompress and not self.always_recompress and chunk_id in self.cache.chunks: # Check if this chunk is already compressed the way we want it old_chunk = self.key.decrypt(None, self.repository.get(chunk_id), decompress=False) - if Compressor.detect(old_chunk.data).name == compress.name: + if Compressor.detect(old_chunk.data).name == compressor.name: # Stored chunk has the same compression we wanted overwrite = False chunk_entry = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite, wait=False) diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index 8cd09450..07cfdd79 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -12,15 +12,19 @@ which sets the .compressor attribute on the Key. For chunks that emanate from files CompressionDecider1 may set a specific Compressor based on patterns (this is the --compression-from option). This is stored -as a Compressor instance in the "compress" key in the Chunk's meta dictionary. +as a Compressor instance in the "compressor" key in the Chunk's meta dictionary. -When compressing either the Compressor specified in the Chunk's meta dictionary -is used, or the default Compressor of the key. +When compressing (KeyBase.compress) either the Compressor specified in the Chunk's +meta dictionary is used, or the default Compressor of the key. The "auto" mode (e.g. --compression auto,lzma,4) is implemented as a meta Compressor, meaning that Auto acts like a Compressor, but defers actual work to others (namely LZ4 as a heuristic whether compression is worth it, and the specified Compressor for the actual compression). + +Decompression is normally handled through Compressor.decompress which will detect +which compressor has been used to compress the data and dispatch to the correct +decompressor. """ import zlib diff --git a/src/borg/key.py b/src/borg/key.py index b7d2cf60..018ef1ab 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -152,7 +152,7 @@ class KeyBase: def compress(self, chunk): meta, data = chunk - return meta.get('compress', self.compressor).compress(data) + return meta.get('compressor', self.compressor).compress(data) def encrypt(self, chunk): pass From d1826cca92e21a879c09e52a67774f80aa612ac7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 31 Mar 2017 13:46:28 +0200 Subject: [PATCH 0767/1387] Rename CompressionDecider1 -> CompressionDecider --- src/borg/archive.py | 14 +++++++------- src/borg/compress.pyx | 2 +- src/borg/helpers.py | 2 +- src/borg/testsuite/helpers.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index da383e50..57ca44ce 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -36,7 +36,7 @@ from .helpers import bin_to_hex from .helpers import safe_ns from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi from .helpers import PathPrefixPattern, FnmatchPattern -from .helpers import CompressionDecider1 +from .helpers import CompressionDecider from .item import Item, ArchiveItem from .key import key_factory from .platform import acl_get, acl_set, set_flags, get_flags, swidth @@ -310,8 +310,8 @@ class Archive: self.file_compression_logger = create_logger('borg.debug.file-compression') self.items_buffer = CacheChunkBuffer(self.cache, self.key, self.stats) self.chunker = Chunker(self.key.chunk_seed, *chunker_params) - self.compression_decider1 = CompressionDecider1(compression or CompressionSpec('none'), - compression_files or []) + self.compression_decider = CompressionDecider(compression or CompressionSpec('none'), + compression_files or []) if name in manifest.archives: raise self.AlreadyExists(name) self.last_checkpoint = time.monotonic() @@ -970,7 +970,7 @@ Utilization of max. archive size: {csize_max:.0%} if chunks is not None: item.chunks = chunks else: - compressor = self.compression_decider1.decide(path) + compressor = self.compression_decider.decide(path) self.file_compression_logger.debug('%s -> compression %s', path, compressor.name) with backup_io('open'): fh = Archive._open_rb(path) @@ -1582,8 +1582,8 @@ class ArchiveRecreater: self.always_recompress = always_recompress self.compression = compression or CompressionSpec('none') self.seen_chunks = set() - self.compression_decider1 = CompressionDecider1(compression or CompressionSpec('none'), - compression_files or []) + self.compression_decider = CompressionDecider(compression or CompressionSpec('none'), + compression_files or []) self.dry_run = dry_run self.stats = stats @@ -1652,7 +1652,7 @@ class ArchiveRecreater: self.cache.chunk_incref(chunk_id, target.stats) return item.chunks chunk_iterator = self.iter_chunks(archive, target, list(item.chunks)) - compressor = self.compression_decider1.decide(item.path) + compressor = self.compression_decider.decide(item.path) chunk_processor = partial(self.chunk_processor, target, compressor) target.chunk_file(item, self.cache, target.stats, chunk_iterator, chunk_processor) diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index 07cfdd79..4f80b83d 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -10,7 +10,7 @@ Borg has a flexible scheme for deciding which compression to use for chunks. First, there is a global default set by the --compression command line option, which sets the .compressor attribute on the Key. -For chunks that emanate from files CompressionDecider1 may set a specific +For chunks that emanate from files CompressionDecider may set a specific Compressor based on patterns (this is the --compression-from option). This is stored as a Compressor instance in the "compressor" key in the Chunk's meta dictionary. diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 685d9f43..c1306e01 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -2096,7 +2096,7 @@ def clean_lines(lines, lstrip=None, rstrip=None, remove_empty=True, remove_comme yield line -class CompressionDecider1: +class CompressionDecider: def __init__(self, compression, compression_files): """ Initialize a CompressionDecider instance (and read config files, if needed). diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index b905a18d..f99934a2 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -25,7 +25,7 @@ from ..helpers import StableDict, int_to_bigint, bigint_to_int, bin_to_hex from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams, Chunk from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless from ..helpers import load_exclude_file, load_pattern_file -from ..helpers import CompressionDecider1 +from ..helpers import CompressionDecider from ..helpers import parse_pattern, PatternMatcher from ..helpers import PathFullPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, RegexPattern from ..helpers import swidth_slice @@ -1202,7 +1202,7 @@ data2 assert list(clean_lines(conf, remove_comments=False)) == ['#comment', 'data1 #data1', 'data2', 'data3', ] -def test_compression_decider1(): +def test_compression_decider(): default = CompressionSpec('zlib') conf = """ # use super-fast lz4 compression on huge VM files in this path: @@ -1213,12 +1213,12 @@ none:*.jpeg none:*.zip """.splitlines() - cd = CompressionDecider1(default, []) # no conf, always use default + cd = CompressionDecider(default, []) # no conf, always use default assert cd.decide('/srv/vm_disks/linux').name == 'zlib' assert cd.decide('test.zip').name == 'zlib' assert cd.decide('test').name == 'zlib' - cd = CompressionDecider1(default, [conf, ]) + cd = CompressionDecider(default, [conf, ]) assert cd.decide('/srv/vm_disks/linux').name == 'lz4' assert cd.decide('test.zip').name == 'none' assert cd.decide('test').name == 'zlib' # no match in conf, use default From 0847c3f9a5acd4f75499ef567f4914167cdbe815 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 1 Apr 2017 00:12:16 +0200 Subject: [PATCH 0768/1387] Unify ComprSpec and CompressionSpec; don't instanciate Compressors right away --- src/borg/archive.py | 4 +- src/borg/archiver.py | 9 ++- src/borg/compress.pyx | 120 ++++++++++++++++++++++----------- src/borg/helpers.py | 2 +- src/borg/testsuite/compress.py | 2 +- 5 files changed, 92 insertions(+), 45 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 57ca44ce..75dbeef3 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1660,14 +1660,14 @@ class ArchiveRecreater: chunk_id = self.key.id_hash(data) if chunk_id in self.seen_chunks: return self.cache.chunk_incref(chunk_id, target.stats) - chunk = Chunk(data, compressor=compressor) overwrite = self.recompress if self.recompress and not self.always_recompress and chunk_id in self.cache.chunks: # Check if this chunk is already compressed the way we want it old_chunk = self.key.decrypt(None, self.repository.get(chunk_id), decompress=False) - if Compressor.detect(old_chunk.data).name == compressor.name: + if Compressor.detect(old_chunk.data).name == compressor.decide(data).name: # Stored chunk has the same compression we wanted overwrite = False + chunk = Chunk(data, compressor=compressor) chunk_entry = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite, wait=False) self.cache.repository.async_response(wait=False) self.seen_chunks.add(chunk_entry.id) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 43aa24c1..ef6c9cee 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -108,7 +108,14 @@ def with_repository(fake=False, invert_fake=False, create=False, lock=True, excl with repository: if manifest or cache: kwargs['manifest'], kwargs['key'] = Manifest.load(repository) - if args.__dict__.get('compression'): + # do_recreate uses args.compression is None as in band signalling for "don't recompress", + # note that it does not look at key.compressor. In this case the default compressor applies + # to new chunks. + # + # We can't use a check like `'compression' in args` (an argparse.Namespace speciality), + # since the compression attribute is set. So we need to see whether it's set to something + # true-ish, like a CompressionSpec instance. + if getattr(args, 'compression', False): kwargs['key'].compressor = args.compression.compressor if cache: with Cache(repository, kwargs['key'], kwargs['manifest'], diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index 4f80b83d..a029a373 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -28,17 +28,15 @@ decompressor. """ import zlib -from collections import namedtuple try: import lzma except ImportError: lzma = None -from .logger import create_logger from .helpers import Buffer, DecompressionError -API_VERSION = '1.1_02' +API_VERSION = '1.1_03' cdef extern from "lz4.h": int LZ4_compress_limitedOutput(const char* source, char* dest, int inputSize, int maxOutputSize) nogil @@ -66,11 +64,34 @@ cdef class CompressorBase: def __init__(self, **kwargs): pass + def decide(self, data): + """ + Return which compressor will perform the actual compression for *data*. + + This exists for a very specific case: If borg recreate is instructed to recompress + using Auto compression it needs to determine the _actual_ target compression of a chunk + in order to detect whether it should be recompressed. + + For all Compressors that are not Auto this always returns *self*. + """ + return self + def compress(self, data): + """ + Compress *data* (bytes) and return bytes result. Prepend the ID bytes of this compressor, + which is needed so that the correct decompressor can be used for decompression. + """ # add ID bytes return self.ID + data def decompress(self, data): + """ + Decompress *data* (bytes) and return bytes result. The leading Compressor ID + bytes need to be present. + + Only handles input generated by _this_ Compressor - for a general purpose + decompression method see *Compressor.decompress*. + """ # strip ID bytes return data[2:] @@ -222,22 +243,36 @@ class Auto(CompressorBase): ID = None name = 'auto' - logger = create_logger('borg.debug.file-compression') - def __init__(self, compressor): super().__init__() self.compressor = compressor self.lz4 = get_compressor('lz4') self.none = get_compressor('none') - def compress(self, data): + def _decide(self, data): + """ + Decides what to do with *data*. Returns (compressor, lz4_data). + + *lz4_data* is the LZ4 result if *compressor* is LZ4 as well, otherwise it is None. + """ lz4_data = self.lz4.compress(data) - if len(lz4_data) < 0.97 * len(data): - return self.compressor.compress(data) - elif len(lz4_data) < len(data): - return lz4_data + ratio = len(lz4_data) / len(data) + if ratio < 0.97: + return self.compressor, None + elif ratio < 1: + return self.lz4, lz4_data else: - return self.none.compress(data) + return self.none, None + + def decide(self, data): + return self._decide(data)[0] + + def compress(self, data): + compressor, lz4_data = self._decide(data) + if lz4_data is None: + return compressor.compress(data) + else: + return lz4_data def decompress(self, data): raise NotImplementedError @@ -288,35 +323,40 @@ class Compressor: raise ValueError('No decompressor for this data found: %r.', data[:2]) -ComprSpec = namedtuple('ComprSpec', ('name', 'spec', 'compressor')) - - -def CompressionSpec(s): - values = s.split(',') - count = len(values) - if count < 1: - raise ValueError - # --compression algo[,level] - name = values[0] - if name == 'none': - return ComprSpec(name=name, spec=None, compressor=CNONE()) - elif name == 'lz4': - return ComprSpec(name=name, spec=None, compressor=LZ4()) - if name in ('zlib', 'lzma', ): - if count < 2: - level = 6 # default compression level in py stdlib - elif count == 2: - level = int(values[1]) - if not 0 <= level <= 9: +class CompressionSpec: + def __init__(self, s): + values = s.split(',') + count = len(values) + if count < 1: + raise ValueError + # --compression algo[,level] + self.name = values[0] + if self.name in ('none', 'lz4', ): + return + elif self.name in ('zlib', 'lzma', ): + if count < 2: + level = 6 # default compression level in py stdlib + elif count == 2: + level = int(values[1]) + if not 0 <= level <= 9: + raise ValueError + else: raise ValueError + self.level = level + elif self.name == 'auto': + if 2 <= count <= 3: + compression = ','.join(values[1:]) + else: + raise ValueError + self.inner = CompressionSpec(compression) else: raise ValueError - return ComprSpec(name=name, spec=level, compressor=get_compressor(name, level=level)) - if name == 'auto': - if 2 <= count <= 3: - compression = ','.join(values[1:]) - else: - raise ValueError - inner = CompressionSpec(compression) - return ComprSpec(name=name, spec=inner, compressor=Auto(inner.compressor)) - raise ValueError + + @property + def compressor(self): + if self.name in ('none', 'lz4', ): + return get_compressor(self.name) + elif self.name in ('zlib', 'lzma', ): + return get_compressor(self.name, level=self.level) + elif self.name == 'auto': + return get_compressor(self.name, compressor=self.inner.compressor) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index c1306e01..c168bfd5 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -123,7 +123,7 @@ def check_extension_modules(): raise ExtensionModuleError if chunker.API_VERSION != '1.1_01': raise ExtensionModuleError - if compress.API_VERSION != '1.1_02': + if compress.API_VERSION != '1.1_03': raise ExtensionModuleError if crypto.API_VERSION != '1.1_01': raise ExtensionModuleError diff --git a/src/borg/testsuite/compress.py b/src/borg/testsuite/compress.py index 9bcf595c..ee6da55a 100644 --- a/src/borg/testsuite/compress.py +++ b/src/borg/testsuite/compress.py @@ -7,7 +7,7 @@ except ImportError: import pytest -from ..compress import get_compressor, Compressor, CompressionSpec, ComprSpec, CNONE, ZLIB, LZ4, LZMA, Auto +from ..compress import get_compressor, Compressor, CompressionSpec, CNONE, ZLIB, LZ4, LZMA, Auto buffer = bytes(2**16) From 929f2760dd7088d8fd466f7acd4e7df2bf933c42 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 3 Apr 2017 21:25:39 +0200 Subject: [PATCH 0769/1387] change global compression default to lz4 as well To be consistent with --compression defaults. --- src/borg/key.py | 4 +++- src/borg/testsuite/key.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/borg/key.py b/src/borg/key.py index 018ef1ab..05723ea3 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -142,7 +142,9 @@ class KeyBase: self.TYPE_STR = bytes([self.TYPE]) self.repository = repository self.target = None # key location file path / repo obj - self.compressor = Compressor('none') # for decompression + # Some commands write new chunks (e.g. rename) but don't take a --compression argument. This duplicates + # the default used by those commands who do take a --compression argument. + self.compressor = Compressor('lz4') self.decompress = self.compressor.decompress self.tam_required = True diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index dd3448c2..31e06f9d 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -246,8 +246,9 @@ class TestKey: key = AuthenticatedKey.create(self.MockRepository(), self.MockArgs()) plaintext = Chunk(b'123456789') authenticated = key.encrypt(plaintext) - # 0x06 is the key TYPE, 0x0000 identifies CNONE compression - assert authenticated == b'\x06\x00\x00' + plaintext.data + # 0x06 is the key TYPE, 0x0100 identifies LZ4 compression, 0x90 is part of LZ4 and means that an uncompressed + # block of length nine follows (the plaintext). + assert authenticated == b'\x06\x01\x00\x90' + plaintext.data class TestPassphrase: From 69fb9bd40371813d85cad112599eac70f8c881eb Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 3 Apr 2017 21:48:06 +0200 Subject: [PATCH 0770/1387] remove --compression-from --- src/borg/archive.py | 25 ++++++------------ src/borg/archiver.py | 48 +++-------------------------------- src/borg/compress.pyx | 12 --------- src/borg/helpers.py | 33 ------------------------ src/borg/key.py | 10 +++----- src/borg/testsuite/helpers.py | 24 ------------------ 6 files changed, 14 insertions(+), 138 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 75dbeef3..55afed3e 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -36,7 +36,6 @@ from .helpers import bin_to_hex from .helpers import safe_ns from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi from .helpers import PathPrefixPattern, FnmatchPattern -from .helpers import CompressionDecider from .item import Item, ArchiveItem from .key import key_factory from .platform import acl_get, acl_set, set_flags, get_flags, swidth @@ -278,7 +277,7 @@ class Archive: def __init__(self, repository, key, manifest, name, cache=None, create=False, checkpoint_interval=300, numeric_owner=False, noatime=False, noctime=False, progress=False, - chunker_params=CHUNKER_PARAMS, start=None, start_monotonic=None, end=None, compression=None, compression_files=None, + chunker_params=CHUNKER_PARAMS, start=None, start_monotonic=None, end=None, consider_part_files=False, log_json=False): self.cwd = os.getcwd() self.key = key @@ -307,11 +306,8 @@ class Archive: self.pipeline = DownloadPipeline(self.repository, self.key) self.create = create if self.create: - self.file_compression_logger = create_logger('borg.debug.file-compression') self.items_buffer = CacheChunkBuffer(self.cache, self.key, self.stats) self.chunker = Chunker(self.key.chunk_seed, *chunker_params) - self.compression_decider = CompressionDecider(compression or CompressionSpec('none'), - compression_files or []) if name in manifest.archives: raise self.AlreadyExists(name) self.last_checkpoint = time.monotonic() @@ -970,12 +966,10 @@ Utilization of max. archive size: {csize_max:.0%} if chunks is not None: item.chunks = chunks else: - compressor = self.compression_decider.decide(path) - self.file_compression_logger.debug('%s -> compression %s', path, compressor.name) with backup_io('open'): fh = Archive._open_rb(path) with os.fdopen(fh, 'rb') as fd: - self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd, fh)), compressor=compressor) + self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd, fh))) if not is_special_file: # we must not memorize special files, because the contents of e.g. a # block or char device will change without its mtime/size/inode changing. @@ -1561,7 +1555,7 @@ class ArchiveRecreater: def __init__(self, repository, manifest, key, cache, matcher, exclude_caches=False, exclude_if_present=None, keep_exclude_tags=False, - chunker_params=None, compression=None, compression_files=None, always_recompress=False, + chunker_params=None, compression=None, always_recompress=False, dry_run=False, stats=False, progress=False, file_status_printer=None, checkpoint_interval=1800): self.repository = repository @@ -1582,8 +1576,6 @@ class ArchiveRecreater: self.always_recompress = always_recompress self.compression = compression or CompressionSpec('none') self.seen_chunks = set() - self.compression_decider = CompressionDecider(compression or CompressionSpec('none'), - compression_files or []) self.dry_run = dry_run self.stats = stats @@ -1652,11 +1644,10 @@ class ArchiveRecreater: self.cache.chunk_incref(chunk_id, target.stats) return item.chunks chunk_iterator = self.iter_chunks(archive, target, list(item.chunks)) - compressor = self.compression_decider.decide(item.path) - chunk_processor = partial(self.chunk_processor, target, compressor) + chunk_processor = partial(self.chunk_processor, target) target.chunk_file(item, self.cache, target.stats, chunk_iterator, chunk_processor) - def chunk_processor(self, target, compressor, data): + def chunk_processor(self, target, data): chunk_id = self.key.id_hash(data) if chunk_id in self.seen_chunks: return self.cache.chunk_incref(chunk_id, target.stats) @@ -1664,10 +1655,10 @@ class ArchiveRecreater: if self.recompress and not self.always_recompress and chunk_id in self.cache.chunks: # Check if this chunk is already compressed the way we want it old_chunk = self.key.decrypt(None, self.repository.get(chunk_id), decompress=False) - if Compressor.detect(old_chunk.data).name == compressor.decide(data).name: + if Compressor.detect(old_chunk.data).name == self.key.compressor.decide(data).name: # Stored chunk has the same compression we wanted overwrite = False - chunk = Chunk(data, compressor=compressor) + chunk = Chunk(data) chunk_entry = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite, wait=False) self.cache.repository.async_response(wait=False) self.seen_chunks.add(chunk_entry.id) @@ -1753,7 +1744,7 @@ class ArchiveRecreater: def create_target_archive(self, name): target = Archive(self.repository, self.key, self.manifest, name, create=True, progress=self.progress, chunker_params=self.chunker_params, cache=self.cache, - checkpoint_interval=self.checkpoint_interval, compression=self.compression) + checkpoint_interval=self.checkpoint_interval) return target def open_archive(self, name, **kwargs): diff --git a/src/borg/archiver.py b/src/borg/archiver.py index ef6c9cee..5c26505c 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -481,7 +481,6 @@ class Archiver: numeric_owner=args.numeric_owner, noatime=args.noatime, noctime=args.noctime, progress=args.progress, chunker_params=args.chunker_params, start=t0, start_monotonic=t0_monotonic, - compression=args.compression, compression_files=args.compression_files, log_json=args.log_json) create_inner(archive, cache) else: @@ -1335,8 +1334,7 @@ class Archiver: recreater = ArchiveRecreater(repository, manifest, key, cache, matcher, exclude_caches=args.exclude_caches, exclude_if_present=args.exclude_if_present, keep_exclude_tags=args.keep_exclude_tags, chunker_params=args.chunker_params, - compression=args.compression, compression_files=args.compression_files, - always_recompress=args.always_recompress, + compression=args.compression, always_recompress=args.always_recompress, progress=args.progress, stats=args.stats, file_status_printer=self.print_file_status, checkpoint_interval=args.checkpoint_interval, @@ -1799,43 +1797,13 @@ class Archiver: For compressible data, it uses the given C[,L] compression - with C[,L] being any valid compression specifier. - The decision about which compression to use is done by borg like this: - - 1. find a compression specifier (per file): - match the path/filename against all patterns in all --compression-from - files (if any). If a pattern matches, use the compression spec given for - that pattern. If no pattern matches (and also if you do not give any - --compression-from option), default to the compression spec given by - --compression. See docs/misc/compression.conf for an example config. - - 2. if the found compression spec is not "auto", the decision is taken: - use the found compression spec. - - 3. if the found compression spec is "auto", test compressibility of each - chunk using lz4. - If it is compressible, use the C,[L] compression spec given within the - "auto" specifier. If it is not compressible, use no compression. - Examples:: borg create --compression lz4 REPO::ARCHIVE data borg create --compression zlib REPO::ARCHIVE data borg create --compression zlib,1 REPO::ARCHIVE data borg create --compression auto,lzma,6 REPO::ARCHIVE data - borg create --compression-from compression.conf --compression auto,lzma ... - - compression.conf has entries like:: - - # example config file for --compression-from option - # - # Format of non-comment / non-empty lines: - # : - # compression-spec is same format as for --compression option - # path/filename pattern is same format as for --exclude option - none:*.gz - none:*.zip - none:*.mp3 - none:*.ogg + borg create --compression auto,lzma ... General remarks: @@ -2424,11 +2392,6 @@ class Archiver: type=CompressionSpec, default=CompressionSpec('lz4'), metavar='COMPRESSION', help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') - archive_group.add_argument('--compression-from', dest='compression_files', - type=argparse.FileType('r'), action='append', - metavar='COMPRESSIONCONFIG', - help='read compression patterns from COMPRESSIONCONFIG, see the output of the ' - '"borg help compression" command for details.') subparser.add_argument('location', metavar='ARCHIVE', type=location_validator(archive=True), @@ -2964,7 +2927,7 @@ class Archiver: resulting archive will only contain files from these PATHs. Note that all paths in an archive are relative, therefore absolute patterns/paths - will *not* match (--exclude, --exclude-from, --compression-from, PATHs). + will *not* match (--exclude, --exclude-from, PATHs). --compression: all chunks seen will be stored using the given method. Due to how Borg stores compressed size information this might display @@ -3059,11 +3022,6 @@ class Archiver: archive_group.add_argument('--always-recompress', dest='always_recompress', action='store_true', help='always recompress chunks, don\'t skip chunks already compressed with the same ' 'algorithm.') - archive_group.add_argument('--compression-from', dest='compression_files', - type=argparse.FileType('r'), action='append', - metavar='COMPRESSIONCONFIG', - help='read compression patterns from COMPRESSIONCONFIG, see the output of the ' - '"borg help compression" command for details.') archive_group.add_argument('--chunker-params', dest='chunker_params', type=ChunkerParams, default=CHUNKER_PARAMS, metavar='PARAMS', diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index a029a373..c226d494 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -5,18 +5,6 @@ borg.compress Compression is applied to chunks after ID hashing (so the ID is a direct function of the plain chunk, compression is irrelevant to it), and of course before encryption. -Borg has a flexible scheme for deciding which compression to use for chunks. - -First, there is a global default set by the --compression command line option, -which sets the .compressor attribute on the Key. - -For chunks that emanate from files CompressionDecider may set a specific -Compressor based on patterns (this is the --compression-from option). This is stored -as a Compressor instance in the "compressor" key in the Chunk's meta dictionary. - -When compressing (KeyBase.compress) either the Compressor specified in the Chunk's -meta dictionary is used, or the default Compressor of the key. - The "auto" mode (e.g. --compression auto,lzma,4) is implemented as a meta Compressor, meaning that Auto acts like a Compressor, but defers actual work to others (namely LZ4 as a heuristic whether compression is worth it, and the specified Compressor diff --git a/src/borg/helpers.py b/src/borg/helpers.py index c168bfd5..451fa0af 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -2096,39 +2096,6 @@ def clean_lines(lines, lstrip=None, rstrip=None, remove_empty=True, remove_comme yield line -class CompressionDecider: - def __init__(self, compression, compression_files): - """ - Initialize a CompressionDecider instance (and read config files, if needed). - - :param compression: default CompressionSpec (e.g. from --compression option) - :param compression_files: list of compression config files (e.g. from --compression-from) or - a list of other line iterators - """ - from .compress import CompressionSpec - self.compressor = compression.compressor - if not compression_files: - self.matcher = None - else: - self.matcher = PatternMatcher(fallback=compression.compressor) - for file in compression_files: - try: - for line in clean_lines(file): - try: - compr_spec, fn_pattern = line.split(':', 1) - except: - continue - self.matcher.add([parse_pattern(fn_pattern)], CompressionSpec(compr_spec).compressor) - finally: - if hasattr(file, 'close'): - file.close() - - def decide(self, path): - if self.matcher is not None: - return self.matcher.match(path) - return self.compressor - - class ErrorIgnoringTextIOWrapper(io.TextIOWrapper): def read(self, n): if not self.closed: diff --git a/src/borg/key.py b/src/borg/key.py index 05723ea3..1bf60933 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -152,10 +152,6 @@ class KeyBase: """Return HMAC hash using the "id" HMAC key """ - def compress(self, chunk): - meta, data = chunk - return meta.get('compressor', self.compressor).compress(data) - def encrypt(self, chunk): pass @@ -256,7 +252,7 @@ class PlaintextKey(KeyBase): return sha256(data).digest() def encrypt(self, chunk): - data = self.compress(chunk) + data = self.compressor.compress(chunk.data) return b''.join([self.TYPE_STR, data]) def decrypt(self, id, data, decompress=True): @@ -334,7 +330,7 @@ class AESKeyBase(KeyBase): MAC = hmac_sha256 def encrypt(self, chunk): - data = self.compress(chunk) + data = self.compressor.compress(chunk.data) self.nonce_manager.ensure_reservation(num_aes_blocks(len(data))) self.enc_cipher.reset() data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(data))) @@ -746,7 +742,7 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): STORAGE = KeyBlobStorage.REPO def encrypt(self, chunk): - data = self.compress(chunk) + data = self.compressor.compress(chunk.data) return b''.join([self.TYPE_STR, data]) def decrypt(self, id, data, decompress=True): diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index f99934a2..1d0a0e9f 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -12,7 +12,6 @@ import msgpack import msgpack.fallback from .. import platform -from ..compress import CompressionSpec from ..helpers import Location from ..helpers import Buffer from ..helpers import partial_format, format_file_size, parse_file_size, format_timedelta, format_line, PlaceholderError, replace_placeholders @@ -25,7 +24,6 @@ from ..helpers import StableDict, int_to_bigint, bigint_to_int, bin_to_hex from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams, Chunk from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless from ..helpers import load_exclude_file, load_pattern_file -from ..helpers import CompressionDecider from ..helpers import parse_pattern, PatternMatcher from ..helpers import PathFullPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, RegexPattern from ..helpers import swidth_slice @@ -1202,28 +1200,6 @@ data2 assert list(clean_lines(conf, remove_comments=False)) == ['#comment', 'data1 #data1', 'data2', 'data3', ] -def test_compression_decider(): - default = CompressionSpec('zlib') - conf = """ -# use super-fast lz4 compression on huge VM files in this path: -lz4:/srv/vm_disks - -# jpeg or zip files do not compress: -none:*.jpeg -none:*.zip -""".splitlines() - - cd = CompressionDecider(default, []) # no conf, always use default - assert cd.decide('/srv/vm_disks/linux').name == 'zlib' - assert cd.decide('test.zip').name == 'zlib' - assert cd.decide('test').name == 'zlib' - - cd = CompressionDecider(default, [conf, ]) - assert cd.decide('/srv/vm_disks/linux').name == 'lz4' - assert cd.decide('test.zip').name == 'none' - assert cd.decide('test').name == 'zlib' # no match in conf, use default - - def test_format_line(): data = dict(foo='bar baz') assert format_line('', data) == '' From 2ff75d58f2ac63473efd8ac566c3ec24e275cc54 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 3 Apr 2017 22:05:53 +0200 Subject: [PATCH 0771/1387] remove Chunk() --- src/borg/archive.py | 55 +++++++++++++++++----------------- src/borg/archiver.py | 14 ++++----- src/borg/cache.py | 6 ++-- src/borg/fuse.py | 4 +-- src/borg/helpers.py | 15 +++------- src/borg/key.py | 20 ++++++------- src/borg/testsuite/archive.py | 4 +-- src/borg/testsuite/archiver.py | 20 ++++++------- src/borg/testsuite/helpers.py | 4 +-- src/borg/testsuite/key.py | 46 ++++++++++++++-------------- 10 files changed, 91 insertions(+), 97 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 55afed3e..995e0e4f 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -25,7 +25,7 @@ from .compress import Compressor, CompressionSpec from .constants import * # NOQA from .hashindex import ChunkIndex, ChunkIndexEntry from .helpers import Manifest -from .helpers import Chunk, ChunkIteratorFileWrapper, open_item +from .helpers import ChunkIteratorFileWrapper, open_item from .helpers import Error, IntegrityError, set_ec from .helpers import uid2user, user2uid, gid2group, group2gid from .helpers import parse_timestamp, to_localtime @@ -195,7 +195,7 @@ class DownloadPipeline: otherwise preloaded chunks will accumulate in RemoteRepository and create a memory leak. """ unpacker = msgpack.Unpacker(use_list=False) - for _, data in self.fetch_many(ids): + for data in self.fetch_many(ids): unpacker.feed(data) items = [Item(internal_dict=item) for item in unpacker] for item in items: @@ -237,7 +237,9 @@ class ChunkBuffer: if self.buffer.tell() == 0: return self.buffer.seek(0) - chunks = list(Chunk(bytes(s)) for s in self.chunker.chunkify(self.buffer)) + # The chunker returns a memoryview to its internal buffer, + # thus a copy is needed before resuming the chunker iterator. + chunks = list(bytes(s) for s in self.chunker.chunkify(self.buffer)) self.buffer.seek(0) self.buffer.truncate(0) # Leave the last partial chunk in the buffer unless flush is True @@ -245,7 +247,7 @@ class ChunkBuffer: for chunk in chunks[:end]: self.chunks.append(self.write_chunk(chunk)) if end == -1: - self.buffer.write(chunks[-1].data) + self.buffer.write(chunks[-1]) def is_full(self): return self.buffer.tell() > self.BUFFER_SIZE @@ -259,7 +261,7 @@ class CacheChunkBuffer(ChunkBuffer): self.stats = stats def write_chunk(self, chunk): - id_, _, _ = self.cache.add_chunk(self.key.id_hash(chunk.data), chunk, self.stats, wait=False) + id_, _, _ = self.cache.add_chunk(self.key.id_hash(chunk), chunk, self.stats, wait=False) self.cache.repository.async_response(wait=False) return id_ @@ -325,7 +327,7 @@ class Archive: self.zeros = None def _load_meta(self, id): - _, data = self.key.decrypt(id, self.repository.get(id)) + data = self.key.decrypt(id, self.repository.get(id)) metadata = ArchiveItem(internal_dict=msgpack.unpackb(data, unicode_errors='surrogateescape')) if metadata.version != 1: raise Exception('Unknown archive metadata version') @@ -464,7 +466,7 @@ Utilization of max. archive size: {csize_max:.0%} metadata = ArchiveItem(metadata) data = self.key.pack_and_authenticate_metadata(metadata.as_dict(), context=b'archive') self.id = self.key.id_hash(data) - self.cache.add_chunk(self.id, Chunk(data), self.stats) + self.cache.add_chunk(self.id, data, self.stats) while self.repository.async_response(wait=True) is not None: pass self.manifest.archives[name] = (self.id, metadata.time) @@ -490,7 +492,7 @@ Utilization of max. archive size: {csize_max:.0%} add(self.id) for id, chunk in zip(self.metadata.items, self.repository.get_many(self.metadata.items)): add(id) - _, data = self.key.decrypt(id, chunk) + data = self.key.decrypt(id, chunk) unpacker.feed(data) for item in unpacker: chunks = item.get(b'chunks') @@ -520,7 +522,7 @@ Utilization of max. archive size: {csize_max:.0%} if dry_run or stdout: if 'chunks' in item: item_chunks_size = 0 - for _, data in self.pipeline.fetch_many([c.id for c in item.chunks], is_preloaded=True): + for data in self.pipeline.fetch_many([c.id for c in item.chunks], is_preloaded=True): if pi: pi.show(increase=len(data), info=[remove_surrogates(item.path)]) if stdout: @@ -584,7 +586,7 @@ Utilization of max. archive size: {csize_max:.0%} self.zeros = b'\0' * (1 << self.chunker_params[1]) with fd: ids = [c.id for c in item.chunks] - for _, data in self.pipeline.fetch_many(ids, is_preloaded=True): + for data in self.pipeline.fetch_many(ids, is_preloaded=True): if pi: pi.show(increase=len(data), info=[remove_surrogates(item.path)]) with backup_io('write'): @@ -712,7 +714,7 @@ Utilization of max. archive size: {csize_max:.0%} setattr(metadata, key, value) data = msgpack.packb(metadata.as_dict(), unicode_errors='surrogateescape') new_id = self.key.id_hash(data) - self.cache.add_chunk(new_id, Chunk(data), self.stats) + self.cache.add_chunk(new_id, data, self.stats) self.manifest.archives[self.name] = (new_id, metadata.time) self.cache.chunk_decref(self.id, self.stats) self.id = new_id @@ -759,7 +761,7 @@ Utilization of max. archive size: {csize_max:.0%} for (i, (items_id, data)) in enumerate(zip(items_ids, self.repository.get_many(items_ids))): if progress: pi.show(i) - _, data = self.key.decrypt(items_id, data) + data = self.key.decrypt(items_id, data) unpacker.feed(data) chunk_decref(items_id, stats) try: @@ -874,10 +876,10 @@ Utilization of max. archive size: {csize_max:.0%} self.write_checkpoint() return length, number - def chunk_file(self, item, cache, stats, chunk_iter, chunk_processor=None, **chunk_kw): + def chunk_file(self, item, cache, stats, chunk_iter, chunk_processor=None): if not chunk_processor: def chunk_processor(data): - chunk_entry = cache.add_chunk(self.key.id_hash(data), Chunk(data, **chunk_kw), stats, wait=False) + chunk_entry = cache.add_chunk(self.key.id_hash(data), data, stats, wait=False) self.cache.repository.async_response(wait=False) return chunk_entry @@ -1205,9 +1207,9 @@ class ArchiveChecker: chunk_ids = list(reversed(chunk_ids_revd)) chunk_data_iter = self.repository.get_many(chunk_ids) else: + _chunk_id = None if chunk_id == Manifest.MANIFEST_ID else chunk_id try: - _chunk_id = None if chunk_id == Manifest.MANIFEST_ID else chunk_id - _, data = self.key.decrypt(_chunk_id, encrypted_data) + self.key.decrypt(_chunk_id, encrypted_data) except IntegrityError as integrity_error: self.error_found = True errors += 1 @@ -1277,7 +1279,7 @@ class ArchiveChecker: for chunk_id, _ in self.chunks.iteritems(): cdata = self.repository.get(chunk_id) try: - _, data = self.key.decrypt(chunk_id, cdata) + data = self.key.decrypt(chunk_id, cdata) except IntegrityError as exc: logger.error('Skipping corrupted chunk: %s', exc) self.error_found = True @@ -1322,9 +1324,9 @@ class ArchiveChecker: self.possibly_superseded.add(id_) def add_callback(chunk): - id_ = self.key.id_hash(chunk.data) + id_ = self.key.id_hash(chunk) cdata = self.key.encrypt(chunk) - add_reference(id_, len(chunk.data), len(cdata), cdata) + add_reference(id_, len(chunk), len(cdata), cdata) return id_ def add_reference(id_, size, csize, cdata=None): @@ -1345,7 +1347,7 @@ class ArchiveChecker: def replacement_chunk(size): data = bytes(size) chunk_id = self.key.id_hash(data) - cdata = self.key.encrypt(Chunk(data)) + cdata = self.key.encrypt(data) csize = len(cdata) return chunk_id, size, csize, cdata @@ -1454,7 +1456,7 @@ class ArchiveChecker: if state > 0: unpacker.resync() for chunk_id, cdata in zip(items, repository.get_many(items)): - _, data = self.key.decrypt(chunk_id, cdata) + data = self.key.decrypt(chunk_id, cdata) unpacker.feed(data) try: for item in unpacker: @@ -1504,7 +1506,7 @@ class ArchiveChecker: continue mark_as_possibly_superseded(archive_id) cdata = self.repository.get(archive_id) - _, data = self.key.decrypt(archive_id, cdata) + data = self.key.decrypt(archive_id, cdata) archive = ArchiveItem(internal_dict=msgpack.unpackb(data)) if archive.version != 1: raise Exception('Unknown archive metadata version') @@ -1521,7 +1523,7 @@ class ArchiveChecker: archive.items = items_buffer.chunks data = msgpack.packb(archive.as_dict(), unicode_errors='surrogateescape') new_archive_id = self.key.id_hash(data) - cdata = self.key.encrypt(Chunk(data)) + cdata = self.key.encrypt(data) add_reference(new_archive_id, len(data), len(cdata), cdata) self.manifest.archives[info.name] = (new_archive_id, info.ts) @@ -1655,11 +1657,10 @@ class ArchiveRecreater: if self.recompress and not self.always_recompress and chunk_id in self.cache.chunks: # Check if this chunk is already compressed the way we want it old_chunk = self.key.decrypt(None, self.repository.get(chunk_id), decompress=False) - if Compressor.detect(old_chunk.data).name == self.key.compressor.decide(data).name: + if Compressor.detect(old_chunk).name == self.key.compressor.decide(data).name: # Stored chunk has the same compression we wanted overwrite = False - chunk = Chunk(data) - chunk_entry = self.cache.add_chunk(chunk_id, chunk, target.stats, overwrite=overwrite, wait=False) + chunk_entry = self.cache.add_chunk(chunk_id, data, target.stats, overwrite=overwrite, wait=False) self.cache.repository.async_response(wait=False) self.seen_chunks.add(chunk_entry.id) return chunk_entry @@ -1673,7 +1674,7 @@ class ArchiveRecreater: yield from target.chunker.chunkify(file) else: for chunk in chunk_iterator: - yield chunk.data + yield chunk def save(self, archive, target, comment=None, replace_original=True): if self.dry_run: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 5c26505c..7dccd55a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -177,14 +177,14 @@ class Archiver: a = next(chunks1, end) if a is end: return not blen - bi and next(chunks2, end) is end - a = memoryview(a.data) + a = memoryview(a) alen = len(a) ai = 0 if not blen - bi: b = next(chunks2, end) if b is end: return not alen - ai and next(chunks1, end) is end - b = memoryview(b.data) + b = memoryview(b) blen = len(b) bi = 0 slicelen = min(alen - ai, blen - bi) @@ -1395,7 +1395,7 @@ class Archiver: archive = Archive(repository, key, manifest, args.location.archive, consider_part_files=args.consider_part_files) for i, item_id in enumerate(archive.metadata.items): - _, data = key.decrypt(item_id, repository.get(item_id)) + data = key.decrypt(item_id, repository.get(item_id)) filename = '%06d_%s.items' % (i, bin_to_hex(item_id)) print('Dumping', filename) with open(filename, 'wb') as fd: @@ -1425,7 +1425,7 @@ class Archiver: fd.write(do_indent(prepare_dump_dict(archive_meta_orig))) fd.write(',\n') - _, data = key.decrypt(archive_meta_orig[b'id'], repository.get(archive_meta_orig[b'id'])) + data = key.decrypt(archive_meta_orig[b'id'], repository.get(archive_meta_orig[b'id'])) archive_org_dict = msgpack.unpackb(data, object_hook=StableDict, unicode_errors='surrogateescape') fd.write(' "_meta":\n') @@ -1436,7 +1436,7 @@ class Archiver: unpacker = msgpack.Unpacker(use_list=False, object_hook=StableDict) first = True for item_id in archive_org_dict[b'items']: - _, data = key.decrypt(item_id, repository.get(item_id)) + data = key.decrypt(item_id, repository.get(item_id)) unpacker.feed(data) for item in unpacker: item = prepare_dump_dict(item) @@ -1460,7 +1460,7 @@ class Archiver: def do_debug_dump_manifest(self, args, repository, manifest, key): """dump decoded repository manifest""" - _, data = key.decrypt(None, repository.get(manifest.MANIFEST_ID)) + data = key.decrypt(None, repository.get(manifest.MANIFEST_ID)) meta = prepare_dump_dict(msgpack.fallback.unpackb(data, object_hook=StableDict, unicode_errors='surrogateescape')) @@ -1484,7 +1484,7 @@ class Archiver: for id in result: cdata = repository.get(id) give_id = id if id != Manifest.MANIFEST_ID else None - _, data = key.decrypt(give_id, cdata) + data = key.decrypt(give_id, cdata) filename = '%06d_%s.obj' % (i, bin_to_hex(id)) print('Dumping', filename) with open(filename, 'wb') as fd: diff --git a/src/borg/cache.py b/src/borg/cache.py index b3d7e12f..e1138e7a 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -424,14 +424,14 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def fetch_and_build_idx(archive_id, repository, key, chunk_idx): cdata = repository.get(archive_id) - _, data = key.decrypt(archive_id, cdata) + data = key.decrypt(archive_id, cdata) chunk_idx.add(archive_id, 1, len(data), len(cdata)) archive = ArchiveItem(internal_dict=msgpack.unpackb(data)) if archive.version != 1: raise Exception('Unknown archive metadata version') unpacker = msgpack.Unpacker() for item_id, chunk in zip(archive.items, repository.get_many(archive.items)): - _, data = key.decrypt(item_id, chunk) + data = key.decrypt(item_id, chunk) chunk_idx.add(item_id, 1, len(data), len(chunk)) unpacker.feed(data) for item in unpacker: @@ -527,7 +527,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def add_chunk(self, id, chunk, stats, overwrite=False, wait=True): if not self.txn_active: self.begin_txn() - size = len(chunk.data) + size = len(chunk) refcount = self.seen_chunk(id, size) if refcount and not overwrite: return self.chunk_incref(id, stats) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 33c6b389..fc19e6e0 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -144,7 +144,7 @@ class FuseOperations(llfuse.Operations): self.file_versions = {} # for versions mode: original path -> version unpacker = msgpack.Unpacker() for key, chunk in zip(archive.metadata.items, self.repository.get_many(archive.metadata.items)): - _, data = self.key.decrypt(key, chunk) + data = self.key.decrypt(key, chunk) unpacker.feed(data) for item in unpacker: item = Item(internal_dict=item) @@ -340,7 +340,7 @@ class FuseOperations(llfuse.Operations): # evict fully read chunk from cache del self.data_cache[id] else: - _, data = self.key.decrypt(id, self.repository.get(id)) + data = self.key.decrypt(id, self.repository.get(id)) if offset + n < len(data): # chunk was only partially read, cache it self.data_cache[id] = data diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 451fa0af..90213243 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -44,13 +44,6 @@ from . import hashindex from . import shellpattern from .constants import * # NOQA -# meta dict, data bytes -_Chunk = namedtuple('_Chunk', 'meta data') - - -def Chunk(data, **meta): - return _Chunk(meta, data) - ''' The global exit_code variable is used so that modules other than archiver can increase the program exit code if a @@ -247,7 +240,7 @@ class Manifest: if not key: key = key_factory(repository, cdata) manifest = cls(key, repository) - data = key.decrypt(None, cdata).data + data = key.decrypt(None, cdata) manifest_dict, manifest.tam_verified = key.unpack_and_verify_manifest(data, force_tam_not_required=force_tam_not_required) m = ManifestItem(internal_dict=manifest_dict) manifest.id = key.id_hash(data) @@ -292,7 +285,7 @@ class Manifest: self.tam_verified = True data = self.key.pack_and_authenticate_metadata(manifest.as_dict()) self.id = self.key.id_hash(data) - self.repository.put(self.MANIFEST_ID, self.key.encrypt(Chunk(data, compression={'name': 'none'}))) + self.repository.put(self.MANIFEST_ID, self.key.encrypt(data)) def prune_within(archives, within): @@ -1909,7 +1902,7 @@ class ItemFormatter(BaseFormatter): if 'chunks' not in item: return "" hash = hashlib.new(hash_function) - for _, data in self.archive.pipeline.fetch_many([c.id for c in item.chunks]): + for data in self.archive.pipeline.fetch_many([c.id for c in item.chunks]): hash.update(data) return hash.hexdigest() @@ -1934,7 +1927,7 @@ class ChunkIteratorFileWrapper: if not remaining: try: chunk = next(self.chunk_iterator) - self.chunk = memoryview(chunk.data) + self.chunk = memoryview(chunk) except StopIteration: self.exhausted = True return 0 # EOF diff --git a/src/borg/key.py b/src/borg/key.py index 1bf60933..6cefba24 100644 --- a/src/borg/key.py +++ b/src/borg/key.py @@ -15,7 +15,7 @@ logger = create_logger() from .constants import * # NOQA from .compress import Compressor from .crypto import AES, bytes_to_long, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512 -from .helpers import Chunk, StableDict +from .helpers import StableDict from .helpers import Error, IntegrityError from .helpers import yes from .helpers import get_keys_dir, get_security_dir @@ -252,7 +252,7 @@ class PlaintextKey(KeyBase): return sha256(data).digest() def encrypt(self, chunk): - data = self.compressor.compress(chunk.data) + data = self.compressor.compress(chunk) return b''.join([self.TYPE_STR, data]) def decrypt(self, id, data, decompress=True): @@ -261,10 +261,10 @@ class PlaintextKey(KeyBase): raise IntegrityError('Chunk %s: Invalid encryption envelope' % id_str) payload = memoryview(data)[1:] if not decompress: - return Chunk(payload) + return payload data = self.decompress(payload) self.assert_id(id, data) - return Chunk(data) + return data def _tam_key(self, salt, context): return salt + context @@ -330,7 +330,7 @@ class AESKeyBase(KeyBase): MAC = hmac_sha256 def encrypt(self, chunk): - data = self.compressor.compress(chunk.data) + data = self.compressor.compress(chunk) self.nonce_manager.ensure_reservation(num_aes_blocks(len(data))) self.enc_cipher.reset() data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(data))) @@ -355,10 +355,10 @@ class AESKeyBase(KeyBase): self.dec_cipher.reset(iv=PREFIX + data[33:41]) payload = self.dec_cipher.decrypt(data_view[41:]) if not decompress: - return Chunk(payload) + return payload data = self.decompress(payload) self.assert_id(id, data) - return Chunk(data) + return data def extract_nonce(self, payload): if not (payload[0] == self.TYPE or @@ -742,7 +742,7 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): STORAGE = KeyBlobStorage.REPO def encrypt(self, chunk): - data = self.compressor.compress(chunk.data) + data = self.compressor.compress(chunk) return b''.join([self.TYPE_STR, data]) def decrypt(self, id, data, decompress=True): @@ -750,10 +750,10 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): raise IntegrityError('Chunk %s: Invalid envelope' % bin_to_hex(id)) payload = memoryview(data)[1:] if not decompress: - return Chunk(payload) + return payload data = self.decompress(payload) self.assert_id(id, data) - return Chunk(data) + return data AVAILABLE_KEY_TYPES = ( diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index dc172d43..3d82a4ce 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -72,8 +72,8 @@ class MockCache: self.repository = self.MockRepo() def add_chunk(self, id, chunk, stats=None, wait=True): - self.objects[id] = chunk.data - return id, len(chunk.data), len(chunk.data) + self.objects[id] = chunk + return id, len(chunk), len(chunk) class ArchiveTimestampTestCase(BaseTestCase): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 87498726..287dfe2c 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -34,7 +34,7 @@ from ..cache import Cache from ..constants import * # NOQA from ..crypto import bytes_to_long, num_aes_blocks from ..helpers import PatternMatcher, parse_pattern, Location, get_security_dir -from ..helpers import Chunk, Manifest +from ..helpers import Manifest from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from ..helpers import bin_to_hex from ..item import Item @@ -2449,7 +2449,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): 'version': 1, }) archive_id = key.id_hash(archive) - repository.put(archive_id, key.encrypt(Chunk(archive))) + repository.put(archive_id, key.encrypt(archive)) repository.commit() self.cmd('check', self.repository_location, exit_code=1) self.cmd('check', '--repair', self.repository_location, exit_code=0) @@ -2537,12 +2537,12 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): def spoof_manifest(self, repository): with repository: _, key = Manifest.load(repository) - repository.put(Manifest.MANIFEST_ID, key.encrypt(Chunk(msgpack.packb({ + repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({ 'version': 1, 'archives': {}, 'config': {}, 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), - })))) + }))) repository.commit() def test_fresh_init_tam_required(self): @@ -2550,11 +2550,11 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): repository = Repository(self.repository_path, exclusive=True) with repository: manifest, key = Manifest.load(repository) - repository.put(Manifest.MANIFEST_ID, key.encrypt(Chunk(msgpack.packb({ + repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({ 'version': 1, 'archives': {}, 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), - })))) + }))) repository.commit() with pytest.raises(TAMRequiredError): @@ -2570,9 +2570,9 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): key.tam_required = False key.change_passphrase(key._passphrase) - manifest = msgpack.unpackb(key.decrypt(None, repository.get(Manifest.MANIFEST_ID)).data) + manifest = msgpack.unpackb(key.decrypt(None, repository.get(Manifest.MANIFEST_ID))) del manifest[b'tam'] - repository.put(Manifest.MANIFEST_ID, key.encrypt(Chunk(msgpack.packb(manifest)))) + repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb(manifest))) repository.commit() output = self.cmd('list', '--debug', self.repository_location) assert 'archive1234' in output @@ -2844,8 +2844,8 @@ def test_get_args(): def test_compare_chunk_contents(): def ccc(a, b): - chunks_a = [Chunk(data) for data in a] - chunks_b = [Chunk(data) for data in b] + chunks_a = [data for data in a] + chunks_b = [data for data in b] compare1 = Archiver.compare_chunk_contents(iter(chunks_a), iter(chunks_b)) compare2 = Archiver.compare_chunk_contents(iter(chunks_b), iter(chunks_a)) assert compare1 == compare2 diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 1d0a0e9f..075967c4 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -21,7 +21,7 @@ from ..helpers import get_cache_dir, get_keys_dir, get_security_dir from ..helpers import is_slow_msgpack from ..helpers import yes, TRUISH, FALSISH, DEFAULTISH from ..helpers import StableDict, int_to_bigint, bigint_to_int, bin_to_hex -from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams, Chunk +from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless from ..helpers import load_exclude_file, load_pattern_file from ..helpers import parse_pattern, PatternMatcher @@ -1158,7 +1158,7 @@ def test_partial_format(): def test_chunk_file_wrapper(): - cfw = ChunkIteratorFileWrapper(iter([Chunk(b'abc'), Chunk(b'def')])) + cfw = ChunkIteratorFileWrapper(iter([b'abc', b'def'])) assert cfw.read(2) == b'ab' assert cfw.read(50) == b'cdef' assert cfw.exhausted diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index 31e06f9d..e92f9f0c 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -9,7 +9,7 @@ import msgpack from ..crypto import bytes_to_long, num_aes_blocks from ..helpers import Location -from ..helpers import Chunk, StableDict +from ..helpers import StableDict from ..helpers import IntegrityError from ..helpers import get_security_dir from ..key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, AuthenticatedKey @@ -104,17 +104,17 @@ class TestKey: def test_plaintext(self): key = PlaintextKey.create(None, None) - chunk = Chunk(b'foo') - assert hexlify(key.id_hash(chunk.data)) == b'2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae' - assert chunk == key.decrypt(key.id_hash(chunk.data), key.encrypt(chunk)) + chunk = b'foo' + assert hexlify(key.id_hash(chunk)) == b'2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae' + assert chunk == key.decrypt(key.id_hash(chunk), key.encrypt(chunk)) def test_keyfile(self, monkeypatch, keys_dir): monkeypatch.setenv('BORG_PASSPHRASE', 'test') key = KeyfileKey.create(self.MockRepository(), self.MockArgs()) assert bytes_to_long(key.enc_cipher.iv, 8) == 0 - manifest = key.encrypt(Chunk(b'ABC')) + manifest = key.encrypt(b'ABC') assert key.extract_nonce(manifest) == 0 - manifest2 = key.encrypt(Chunk(b'ABC')) + manifest2 = key.encrypt(b'ABC') assert manifest != manifest2 assert key.decrypt(None, manifest) == key.decrypt(None, manifest2) assert key.extract_nonce(manifest2) == 1 @@ -124,8 +124,8 @@ class TestKey: # Key data sanity check assert len({key2.id_key, key2.enc_key, key2.enc_hmac_key}) == 3 assert key2.chunk_seed != 0 - chunk = Chunk(b'foo') - assert chunk == key2.decrypt(key.id_hash(chunk.data), key.encrypt(chunk)) + chunk = b'foo' + assert chunk == key2.decrypt(key.id_hash(chunk), key.encrypt(chunk)) def test_keyfile_nonce_rollback_protection(self, monkeypatch, keys_dir): monkeypatch.setenv('BORG_PASSPHRASE', 'test') @@ -133,9 +133,9 @@ class TestKey: with open(os.path.join(get_security_dir(repository.id_str), 'nonce'), "w") as fd: fd.write("0000000000002000") key = KeyfileKey.create(repository, self.MockArgs()) - data = key.encrypt(Chunk(b'ABC')) + data = key.encrypt(b'ABC') assert key.extract_nonce(data) == 0x2000 - assert key.decrypt(None, data).data == b'ABC' + assert key.decrypt(None, data) == b'ABC' def test_keyfile_kfenv(self, tmpdir, monkeypatch): keyfile = tmpdir.join('keyfile') @@ -144,8 +144,8 @@ class TestKey: assert not keyfile.exists() key = KeyfileKey.create(self.MockRepository(), self.MockArgs()) assert keyfile.exists() - chunk = Chunk(b'ABC') - chunk_id = key.id_hash(chunk.data) + chunk = b'ABC' + chunk_id = key.id_hash(chunk) chunk_cdata = key.encrypt(chunk) key = KeyfileKey.detect(self.MockRepository(), chunk_cdata) assert chunk == key.decrypt(chunk_id, chunk_cdata) @@ -158,7 +158,7 @@ class TestKey: fd.write(self.keyfile2_key_file) monkeypatch.setenv('BORG_PASSPHRASE', 'passphrase') key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata) - assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata).data == b'payload' + assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata) == b'payload' def test_keyfile2_kfenv(self, tmpdir, monkeypatch): keyfile = tmpdir.join('keyfile') @@ -167,14 +167,14 @@ class TestKey: monkeypatch.setenv('BORG_KEY_FILE', str(keyfile)) monkeypatch.setenv('BORG_PASSPHRASE', 'passphrase') key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata) - assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata).data == b'payload' + assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata) == b'payload' def test_keyfile_blake2(self, monkeypatch, keys_dir): with keys_dir.join('keyfile').open('w') as fd: fd.write(self.keyfile_blake2_key_file) monkeypatch.setenv('BORG_PASSPHRASE', 'passphrase') key = Blake2KeyfileKey.detect(self.MockRepository(), self.keyfile_blake2_cdata) - assert key.decrypt(self.keyfile_blake2_id, self.keyfile_blake2_cdata).data == b'payload' + assert key.decrypt(self.keyfile_blake2_id, self.keyfile_blake2_cdata) == b'payload' def test_passphrase(self, keys_dir, monkeypatch): monkeypatch.setenv('BORG_PASSPHRASE', 'test') @@ -184,9 +184,9 @@ class TestKey: assert hexlify(key.enc_hmac_key) == b'b885a05d329a086627412a6142aaeb9f6c54ab7950f996dd65587251f6bc0901' assert hexlify(key.enc_key) == b'2ff3654c6daf7381dbbe718d2b20b4f1ea1e34caa6cc65f6bb3ac376b93fed2a' assert key.chunk_seed == -775740477 - manifest = key.encrypt(Chunk(b'ABC')) + manifest = key.encrypt(b'ABC') assert key.extract_nonce(manifest) == 0 - manifest2 = key.encrypt(Chunk(b'ABC')) + manifest2 = key.encrypt(b'ABC') assert manifest != manifest2 assert key.decrypt(None, manifest) == key.decrypt(None, manifest2) assert key.extract_nonce(manifest2) == 1 @@ -197,9 +197,9 @@ class TestKey: assert key.enc_hmac_key == key2.enc_hmac_key assert key.enc_key == key2.enc_key assert key.chunk_seed == key2.chunk_seed - chunk = Chunk(b'foo') - assert hexlify(key.id_hash(chunk.data)) == b'818217cf07d37efad3860766dcdf1d21e401650fed2d76ed1d797d3aae925990' - assert chunk == key2.decrypt(key2.id_hash(chunk.data), key.encrypt(chunk)) + chunk = b'foo' + assert hexlify(key.id_hash(chunk)) == b'818217cf07d37efad3860766dcdf1d21e401650fed2d76ed1d797d3aae925990' + assert chunk == key2.decrypt(key2.id_hash(chunk), key.encrypt(chunk)) def _corrupt_byte(self, key, data, offset): data = bytearray(data) @@ -224,7 +224,7 @@ class TestKey: key.decrypt(id, data) def test_decrypt_decompress(self, key): - plaintext = Chunk(b'123456789') + plaintext = b'123456789' encrypted = key.encrypt(plaintext) assert key.decrypt(None, encrypted, decompress=False) != plaintext assert key.decrypt(None, encrypted) == plaintext @@ -244,11 +244,11 @@ class TestKey: def test_authenticated_encrypt(self, monkeypatch): monkeypatch.setenv('BORG_PASSPHRASE', 'test') key = AuthenticatedKey.create(self.MockRepository(), self.MockArgs()) - plaintext = Chunk(b'123456789') + plaintext = b'123456789' authenticated = key.encrypt(plaintext) # 0x06 is the key TYPE, 0x0100 identifies LZ4 compression, 0x90 is part of LZ4 and means that an uncompressed # block of length nine follows (the plaintext). - assert authenticated == b'\x06\x01\x00\x90' + plaintext.data + assert authenticated == b'\x06\x01\x00\x90' + plaintext class TestPassphrase: From b2953357ed6120c803f80207d210a7acdb46fbb3 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 4 Apr 2017 15:11:15 +0200 Subject: [PATCH 0772/1387] recreate: add --recompress flag, avoid weirdo use of args.compression --- docs/usage.rst | 2 +- src/borg/archive.py | 4 ++-- src/borg/archiver.py | 18 +++++++----------- src/borg/testsuite/archiver.py | 2 +- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 0844159c..eb0fe6f5 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -463,7 +463,7 @@ Examples $ borg create /mnt/backup::archive /some/files --compression lz4 # Then compress it - this might take longer, but the backup has already completed, so no inconsistencies # from a long-running backup job. - $ borg recreate /mnt/backup::archive --compression zlib,9 + $ borg recreate /mnt/backup::archive --recompress --compression zlib,9 # Remove unwanted files from all archives in a repository $ borg recreate /mnt/backup -e /home/icke/Pictures/drunk_photos diff --git a/src/borg/archive.py b/src/borg/archive.py index 995e0e4f..6bfe1d49 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1557,7 +1557,7 @@ class ArchiveRecreater: def __init__(self, repository, manifest, key, cache, matcher, exclude_caches=False, exclude_if_present=None, keep_exclude_tags=False, - chunker_params=None, compression=None, always_recompress=False, + chunker_params=None, compression=None, recompress=False, always_recompress=False, dry_run=False, stats=False, progress=False, file_status_printer=None, checkpoint_interval=1800): self.repository = repository @@ -1574,7 +1574,7 @@ class ArchiveRecreater: if self.rechunkify: logger.debug('Rechunking archives to %s', chunker_params) self.chunker_params = chunker_params or CHUNKER_PARAMS - self.recompress = bool(compression) + self.recompress = recompress self.always_recompress = always_recompress self.compression = compression or CompressionSpec('none') self.seen_chunks = set() diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 7dccd55a..802a972d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -108,14 +108,7 @@ def with_repository(fake=False, invert_fake=False, create=False, lock=True, excl with repository: if manifest or cache: kwargs['manifest'], kwargs['key'] = Manifest.load(repository) - # do_recreate uses args.compression is None as in band signalling for "don't recompress", - # note that it does not look at key.compressor. In this case the default compressor applies - # to new chunks. - # - # We can't use a check like `'compression' in args` (an argparse.Namespace speciality), - # since the compression attribute is set. So we need to see whether it's set to something - # true-ish, like a CompressionSpec instance. - if getattr(args, 'compression', False): + if 'compression' in args: kwargs['key'].compressor = args.compression.compressor if cache: with Cache(repository, kwargs['key'], kwargs['manifest'], @@ -1334,7 +1327,7 @@ class Archiver: recreater = ArchiveRecreater(repository, manifest, key, cache, matcher, exclude_caches=args.exclude_caches, exclude_if_present=args.exclude_if_present, keep_exclude_tags=args.keep_exclude_tags, chunker_params=args.chunker_params, - compression=args.compression, always_recompress=args.always_recompress, + compression=args.compression, recompress=args.recompress, always_recompress=args.always_recompress, progress=args.progress, stats=args.stats, file_status_printer=self.print_file_status, checkpoint_interval=args.checkpoint_interval, @@ -2929,7 +2922,8 @@ class Archiver: Note that all paths in an archive are relative, therefore absolute patterns/paths will *not* match (--exclude, --exclude-from, PATHs). - --compression: all chunks seen will be stored using the given method. + --recompress: all chunks seen will be recompressed using the --compression + specified. Due to how Borg stores compressed size information this might display incorrect information for archives that were not recreated at the same time. There is no risk of data loss by this. @@ -3016,9 +3010,11 @@ class Archiver: help='manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). ' 'alternatively, give a reference file/directory.') archive_group.add_argument('-C', '--compression', dest='compression', - type=CompressionSpec, default=None, metavar='COMPRESSION', + type=CompressionSpec, default=CompressionSpec('lz4'), metavar='COMPRESSION', help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') + archive_group.add_argument('--recompress', dest='recompress', action='store_true', + help='recompress chunks according to --compression.') archive_group.add_argument('--always-recompress', dest='always_recompress', action='store_true', help='always recompress chunks, don\'t skip chunks already compressed with the same ' 'algorithm.') diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 287dfe2c..9ec08102 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2049,7 +2049,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): '--format', '{size} {csize} {sha256}') size, csize, sha256_before = file_list.split(' ') assert int(csize) >= int(size) # >= due to metadata overhead - self.cmd('recreate', self.repository_location, '-C', 'lz4') + self.cmd('recreate', self.repository_location, '-C', 'lz4', '--recompress') self.check_cache() file_list = self.cmd('list', self.repository_location + '::test', 'input/compressible', '--format', '{size} {csize} {sha256}') From 88dfb3e9c53bb335e339307232849e9ad0426bba Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 1 Apr 2017 21:10:31 +0200 Subject: [PATCH 0773/1387] serve: fix forced command lines containing BORG_ env vars --- src/borg/archiver.py | 4 ++++ src/borg/testsuite/archiver.py | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 7dccd55a..e73aaf2d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -4,6 +4,7 @@ import faulthandler import functools import hashlib import inspect +import itertools import json import logging import os @@ -3306,6 +3307,9 @@ class Archiver: if cmd is not None and result.func == self.do_serve: forced_result = result argv = shlex.split(cmd) + # Drop environment variables (do *not* interpret them) before trying to parse + # the borg command line. + argv = list(itertools.dropwhile(lambda arg: '=' in arg, argv)) result = self.parse_args(argv[1:]) if result.func != forced_result.func: # someone is trying to execute a different borg subcommand, don't do that! diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 287dfe2c..f2608f13 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2841,6 +2841,13 @@ def test_get_args(): 'borg init --encryption=repokey /') assert args.func == archiver.do_serve + # Check that environment variables in the forced command don't cause issues. If the command + # were not forced, environment variables would be interpreted by the shell, but this does not + # happen for forced commands - we get the verbatim command line and need to deal with env vars. + args = archiver.get_args(['borg', 'serve', ], + 'BORG_HOSTNAME_IS_UNIQUE=yes borg serve --info') + assert args.func == archiver.do_serve + def test_compare_chunk_contents(): def ccc(a, b): From dcfbd39125455eff5ae13b5105866abd9e41128d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 4 Apr 2017 18:34:37 +0200 Subject: [PATCH 0774/1387] recreate: unify --always-recompress and --recompress --- src/borg/archiver.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 802a972d..48775c86 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1323,11 +1323,13 @@ class Archiver: matcher, include_patterns = self.build_matcher(args.patterns, args.paths) self.output_list = args.output_list self.output_filter = args.output_filter + recompress = args.recompress != 'never' + always_recompress = args.recompress == 'always' recreater = ArchiveRecreater(repository, manifest, key, cache, matcher, exclude_caches=args.exclude_caches, exclude_if_present=args.exclude_if_present, keep_exclude_tags=args.keep_exclude_tags, chunker_params=args.chunker_params, - compression=args.compression, recompress=args.recompress, always_recompress=args.always_recompress, + compression=args.compression, recompress=recompress, always_recompress=always_recompress, progress=args.progress, stats=args.stats, file_status_printer=self.print_file_status, checkpoint_interval=args.checkpoint_interval, @@ -2922,8 +2924,7 @@ class Archiver: Note that all paths in an archive are relative, therefore absolute patterns/paths will *not* match (--exclude, --exclude-from, PATHs). - --recompress: all chunks seen will be recompressed using the --compression - specified. + --recompress allows to change the compression of existing data in archives. Due to how Borg stores compressed size information this might display incorrect information for archives that were not recreated at the same time. There is no risk of data loss by this. @@ -3013,11 +3014,12 @@ class Archiver: type=CompressionSpec, default=CompressionSpec('lz4'), metavar='COMPRESSION', help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') - archive_group.add_argument('--recompress', dest='recompress', action='store_true', - help='recompress chunks according to --compression.') - archive_group.add_argument('--always-recompress', dest='always_recompress', action='store_true', - help='always recompress chunks, don\'t skip chunks already compressed with the same ' - 'algorithm.') + archive_group.add_argument('--recompress', dest='recompress', nargs='?', default='never', const='if-different', + choices=('never', 'if-different', 'always'), + help='recompress data chunks according to --compression if "if-different". ' + 'When "always", chunks that are already compressed that way are not skipped, ' + 'but compressed again. Only the algorithm is considered for "if-different", ' + 'not the compression level (if any).') archive_group.add_argument('--chunker-params', dest='chunker_params', type=ChunkerParams, default=CHUNKER_PARAMS, metavar='PARAMS', From 1924e33ef58a716393712f3e29069a2b4eca7c44 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 4 Apr 2017 23:59:58 +0200 Subject: [PATCH 0775/1387] format_line: deny access to internal objects --- src/borg/helpers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 90213243..313829c5 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -110,6 +110,10 @@ class PlaceholderError(Error): """Formatting Error: "{}".format({}): {}({})""" +class InvalidPlaceholder(PlaceholderError): + """Invalid placeholder "{}" in string: {}""" + + def check_extension_modules(): from . import platform, compress, item if hashindex.API_VERSION != '1.1_01': @@ -780,6 +784,10 @@ class DatetimeWrapper: def format_line(format, data): + keys = [f[1] for f in Formatter().parse(format)] + for key in keys: + if '.' in key or '__' in key: + raise InvalidPlaceholder(key, format) try: return format.format(**data) except Exception as e: From e2e172c74f4c699116402c8b53acc3891bd363a2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 5 Apr 2017 00:00:18 +0200 Subject: [PATCH 0776/1387] format_line: clearer error message for unrecognized placeholder --- src/borg/helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 313829c5..8e45f36b 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -790,6 +790,8 @@ def format_line(format, data): raise InvalidPlaceholder(key, format) try: return format.format(**data) + except KeyError as ke: + raise InvalidPlaceholder(ke.args[0], format) except Exception as e: raise PlaceholderError(format, data, e.__class__.__name__, str(e)) From 1bd381a13a14c60dcc36eefd8a2acd60bca52673 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 5 Apr 2017 00:05:46 +0200 Subject: [PATCH 0777/1387] format_line: deny conversions (!r, !s, !a) --- src/borg/helpers.py | 7 ++++--- src/borg/testsuite/helpers.py | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 8e45f36b..2d265fc2 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -784,9 +784,10 @@ class DatetimeWrapper: def format_line(format, data): - keys = [f[1] for f in Formatter().parse(format)] - for key in keys: - if '.' in key or '__' in key: + for _, key, _, conversion in Formatter().parse(format): + if not key: + continue + if '.' in key or '__' in key or conversion: raise InvalidPlaceholder(key, format) try: return format.format(**data) diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 075967c4..3938b722 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -1213,6 +1213,10 @@ def test_format_line_erroneous(): assert format_line('{invalid}', data) with pytest.raises(PlaceholderError): assert format_line('{}', data) + with pytest.raises(PlaceholderError): + assert format_line('{now!r}', data) + with pytest.raises(PlaceholderError): + assert format_line('{now.__class__.__module__.__builtins__}', data) def test_replace_placeholders(): From 707316b0ea10e122799d51fff2fd0a47691ca8c8 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 5 Apr 2017 00:11:46 +0200 Subject: [PATCH 0778/1387] placeholders: document escaping --- src/borg/archiver.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e73aaf2d..7363c39c 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1755,6 +1755,10 @@ class Archiver: The version of borg, only major, minor and patch version, e.g.: 1.0.8 + If literal curly braces need to be used, double them for escaping:: + + borg create /path/to/repo::{{literal_text}} + Examples:: borg create /path/to/repo::{hostname}-{user}-{utcnow} ... From 66f4cd1a29868f96881bd1b08d77e954b7ba2c58 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Mar 2017 06:01:13 +0200 Subject: [PATCH 0779/1387] minor refactor for regular file hardlink processing --- src/borg/archive.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 995e0e4f..996031b0 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -927,15 +927,19 @@ Utilization of max. archive size: {csize_max:.0%} def process_file(self, path, st, cache, ignore_inode=False): status = None safe_path = make_path_safe(path) - # Is it a hard link? - if st.st_nlink > 1: + item = Item(path=safe_path) + hardlink_master = False + hardlinked = st.st_nlink > 1 + if hardlinked: source = self.hard_links.get((st.st_ino, st.st_dev)) if source is not None: - item = Item(path=safe_path, source=source) + item.source = source item.update(self.stat_attrs(st, path)) self.add_item(item) status = 'h' # regular file, hardlink (to already seen inodes) return status + else: + hardlink_master = True is_special_file = is_special(st.st_mode) if not is_special_file: path_hash = self.key.id_hash(safe_encode(os.path.join(self.cwd, path))) @@ -959,10 +963,7 @@ Utilization of max. archive size: {csize_max:.0%} status = 'U' # regular file, unchanged else: status = 'A' # regular file, added - item = Item( - path=safe_path, - hardlink_master=st.st_nlink > 1, # item is a hard link and has the chunks - ) + item.hardlink_master = hardlinked item.update(self.stat_simple_attrs(st)) # Only chunkify the file if needed if chunks is not None: @@ -985,7 +986,7 @@ Utilization of max. archive size: {csize_max:.0%} item.mode = stat.S_IFREG | stat.S_IMODE(item.mode) self.stats.nfiles += 1 self.add_item(item) - if st.st_nlink > 1 and source is None: + if hardlinked and hardlink_master: # Add the hard link reference *after* the file has been added to the archive. self.hard_links[st.st_ino, st.st_dev] = safe_path return status From a206a85890bf8b0af414926257087b29ec3869bb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Mar 2017 06:07:02 +0200 Subject: [PATCH 0780/1387] indent block, no semantics change --- src/borg/archive.py | 73 +++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 996031b0..c5124b6d 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -941,43 +941,44 @@ Utilization of max. archive size: {csize_max:.0%} else: hardlink_master = True is_special_file = is_special(st.st_mode) - if not is_special_file: - path_hash = self.key.id_hash(safe_encode(os.path.join(self.cwd, path))) - ids = cache.file_known_and_unchanged(path_hash, st, ignore_inode) - else: - # in --read-special mode, we may be called for special files. - # there should be no information in the cache about special files processed in - # read-special mode, but we better play safe as this was wrong in the past: - path_hash = ids = None - first_run = not cache.files and cache.do_files - if first_run: - logger.debug('Processing files ...') - chunks = None - if ids is not None: - # Make sure all ids are available - for id_ in ids: - if not cache.seen_chunk(id_): - break - else: - chunks = [cache.chunk_incref(id_, self.stats) for id_ in ids] - status = 'U' # regular file, unchanged - else: - status = 'A' # regular file, added - item.hardlink_master = hardlinked - item.update(self.stat_simple_attrs(st)) - # Only chunkify the file if needed - if chunks is not None: - item.chunks = chunks - else: - with backup_io('open'): - fh = Archive._open_rb(path) - with os.fdopen(fh, 'rb') as fd: - self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd, fh))) + if True: if not is_special_file: - # we must not memorize special files, because the contents of e.g. a - # block or char device will change without its mtime/size/inode changing. - cache.memorize_file(path_hash, st, [c.id for c in item.chunks]) - status = status or 'M' # regular file, modified (if not 'A' already) + path_hash = self.key.id_hash(safe_encode(os.path.join(self.cwd, path))) + ids = cache.file_known_and_unchanged(path_hash, st, ignore_inode) + else: + # in --read-special mode, we may be called for special files. + # there should be no information in the cache about special files processed in + # read-special mode, but we better play safe as this was wrong in the past: + path_hash = ids = None + first_run = not cache.files and cache.do_files + if first_run: + logger.debug('Processing files ...') + chunks = None + if ids is not None: + # Make sure all ids are available + for id_ in ids: + if not cache.seen_chunk(id_): + break + else: + chunks = [cache.chunk_incref(id_, self.stats) for id_ in ids] + status = 'U' # regular file, unchanged + else: + status = 'A' # regular file, added + item.hardlink_master = hardlinked + item.update(self.stat_simple_attrs(st)) + # Only chunkify the file if needed + if chunks is not None: + item.chunks = chunks + else: + with backup_io('open'): + fh = Archive._open_rb(path) + with os.fdopen(fh, 'rb') as fd: + self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd, fh))) + if not is_special_file: + # we must not memorize special files, because the contents of e.g. a + # block or char device will change without its mtime/size/inode changing. + cache.memorize_file(path_hash, st, [c.id for c in item.chunks]) + status = status or 'M' # regular file, modified (if not 'A' already) item.update(self.stat_attrs(st, path)) item.get_size(memorize=True) if is_special_file: From e5d094d0ceafcbc5ff781a2285c842c10453c48b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Mar 2017 06:15:36 +0200 Subject: [PATCH 0781/1387] use same finalizing code for hardlink masters and slaves hardlink slaves get a precomputed size attribute now. --- src/borg/archive.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index c5124b6d..e3804974 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -934,14 +934,11 @@ Utilization of max. archive size: {csize_max:.0%} source = self.hard_links.get((st.st_ino, st.st_dev)) if source is not None: item.source = source - item.update(self.stat_attrs(st, path)) - self.add_item(item) status = 'h' # regular file, hardlink (to already seen inodes) - return status else: hardlink_master = True is_special_file = is_special(st.st_mode) - if True: + if not hardlinked or hardlink_master: if not is_special_file: path_hash = self.key.id_hash(safe_encode(os.path.join(self.cwd, path))) ids = cache.file_known_and_unchanged(path_hash, st, ignore_inode) @@ -979,15 +976,15 @@ Utilization of max. archive size: {csize_max:.0%} # block or char device will change without its mtime/size/inode changing. cache.memorize_file(path_hash, st, [c.id for c in item.chunks]) status = status or 'M' # regular file, modified (if not 'A' already) + self.stats.nfiles += 1 item.update(self.stat_attrs(st, path)) item.get_size(memorize=True) if is_special_file: # we processed a special file like a regular file. reflect that in mode, # so it can be extracted / accessed in FUSE mount like a regular file: item.mode = stat.S_IFREG | stat.S_IMODE(item.mode) - self.stats.nfiles += 1 self.add_item(item) - if hardlinked and hardlink_master: + if hardlink_master: # Add the hard link reference *after* the file has been added to the archive. self.hard_links[st.st_ino, st.st_dev] = safe_path return status From 9478e8abd092a3871861992de6639aa77656e7e7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Mar 2017 13:51:04 +0200 Subject: [PATCH 0782/1387] support hardlinks via create_helper context manager also: reduce code duplication --- src/borg/archive.py | 171 +++++++++++++++++++++++--------------------- 1 file changed, 88 insertions(+), 83 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index e3804974..4e331a77 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -834,34 +834,54 @@ Utilization of max. archive size: {csize_max:.0%} attrs.update(self.stat_ext_attrs(st, path)) return attrs - def process_dir(self, path, st): - item = Item(path=make_path_safe(path)) - item.update(self.stat_attrs(st, path)) + @contextmanager + def create_helper(self, path, st, status=None): + safe_path = make_path_safe(path) + item = Item(path=safe_path) + hardlink_master = False + hardlinked = st.st_nlink > 1 + if hardlinked: + source = self.hard_links.get((st.st_ino, st.st_dev)) + if source is not None: + item.source = source + status = 'h' # hardlink (to already seen inodes) + else: + hardlink_master = True + yield item, status, hardlinked, hardlink_master + # if we get here, "with"-block worked ok without error/exception, the item was processed ok... self.add_item(item) - return 'd' # directory + # ... and added to the archive, so we can remember it to refer to it later in the archive: + if hardlink_master: + self.hard_links[(st.st_ino, st.st_dev)] = safe_path + + def process_dir(self, path, st): + with self.create_helper(path, st, 'd') as (item, status, hardlinked, hardlink_master): # directory + item.update(self.stat_attrs(st, path)) + return status def process_fifo(self, path, st): - item = Item(path=make_path_safe(path)) - item.update(self.stat_attrs(st, path)) - self.add_item(item) - return 'f' # fifo + with self.create_helper(path, st, 'f') as (item, status, hardlinked, hardlink_master): # fifo + item.update(self.stat_attrs(st, path)) + return status def process_dev(self, path, st): - item = Item(path=make_path_safe(path), rdev=st.st_rdev) - item.update(self.stat_attrs(st, path)) - self.add_item(item) - if stat.S_ISCHR(st.st_mode): - return 'c' # char device - elif stat.S_ISBLK(st.st_mode): - return 'b' # block device + with self.create_helper(path, st, None) as (item, status, hardlinked, hardlink_master): # no status yet + item.rdev = st.st_rdev + item.update(self.stat_attrs(st, path)) + if stat.S_ISCHR(st.st_mode): + return 'c' # char device + elif stat.S_ISBLK(st.st_mode): + return 'b' # block device def process_symlink(self, path, st): - with backup_io('readlink'): - source = os.readlink(path) - item = Item(path=make_path_safe(path), source=source) - item.update(self.stat_attrs(st, path)) - self.add_item(item) - return 's' # symlink + with self.create_helper(path, st, 's') as (item, status, hardlinked, hardlink_master): # symlink + with backup_io('readlink'): + source = os.readlink(path) + item.source = source # XXX this overwrites hardlink slave's usage of item.source + if hardlinked: + logger.warning('hardlinked symlinks will be extracted as non-hardlinked symlinks!') + item.update(self.stat_attrs(st, path)) + return status def write_part_file(self, item, from_chunk, number): item = Item(internal_dict=item.as_dict()) @@ -925,69 +945,54 @@ Utilization of max. archive size: {csize_max:.0%} return 'i' # stdin def process_file(self, path, st, cache, ignore_inode=False): - status = None - safe_path = make_path_safe(path) - item = Item(path=safe_path) - hardlink_master = False - hardlinked = st.st_nlink > 1 - if hardlinked: - source = self.hard_links.get((st.st_ino, st.st_dev)) - if source is not None: - item.source = source - status = 'h' # regular file, hardlink (to already seen inodes) - else: - hardlink_master = True - is_special_file = is_special(st.st_mode) - if not hardlinked or hardlink_master: - if not is_special_file: - path_hash = self.key.id_hash(safe_encode(os.path.join(self.cwd, path))) - ids = cache.file_known_and_unchanged(path_hash, st, ignore_inode) - else: - # in --read-special mode, we may be called for special files. - # there should be no information in the cache about special files processed in - # read-special mode, but we better play safe as this was wrong in the past: - path_hash = ids = None - first_run = not cache.files and cache.do_files - if first_run: - logger.debug('Processing files ...') - chunks = None - if ids is not None: - # Make sure all ids are available - for id_ in ids: - if not cache.seen_chunk(id_): - break - else: - chunks = [cache.chunk_incref(id_, self.stats) for id_ in ids] - status = 'U' # regular file, unchanged - else: - status = 'A' # regular file, added - item.hardlink_master = hardlinked - item.update(self.stat_simple_attrs(st)) - # Only chunkify the file if needed - if chunks is not None: - item.chunks = chunks - else: - with backup_io('open'): - fh = Archive._open_rb(path) - with os.fdopen(fh, 'rb') as fd: - self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd, fh))) + with self.create_helper(path, st, None) as (item, status, hardlinked, hardlink_master): # no status yet + is_special_file = is_special(st.st_mode) + if not hardlinked or hardlink_master: if not is_special_file: - # we must not memorize special files, because the contents of e.g. a - # block or char device will change without its mtime/size/inode changing. - cache.memorize_file(path_hash, st, [c.id for c in item.chunks]) - status = status or 'M' # regular file, modified (if not 'A' already) - self.stats.nfiles += 1 - item.update(self.stat_attrs(st, path)) - item.get_size(memorize=True) - if is_special_file: - # we processed a special file like a regular file. reflect that in mode, - # so it can be extracted / accessed in FUSE mount like a regular file: - item.mode = stat.S_IFREG | stat.S_IMODE(item.mode) - self.add_item(item) - if hardlink_master: - # Add the hard link reference *after* the file has been added to the archive. - self.hard_links[st.st_ino, st.st_dev] = safe_path - return status + path_hash = self.key.id_hash(safe_encode(os.path.join(self.cwd, path))) + ids = cache.file_known_and_unchanged(path_hash, st, ignore_inode) + else: + # in --read-special mode, we may be called for special files. + # there should be no information in the cache about special files processed in + # read-special mode, but we better play safe as this was wrong in the past: + path_hash = ids = None + first_run = not cache.files and cache.do_files + if first_run: + logger.debug('Processing files ...') + chunks = None + if ids is not None: + # Make sure all ids are available + for id_ in ids: + if not cache.seen_chunk(id_): + break + else: + chunks = [cache.chunk_incref(id_, self.stats) for id_ in ids] + status = 'U' # regular file, unchanged + else: + status = 'A' # regular file, added + item.hardlink_master = hardlinked + item.update(self.stat_simple_attrs(st)) + # Only chunkify the file if needed + if chunks is not None: + item.chunks = chunks + else: + with backup_io('open'): + fh = Archive._open_rb(path) + with os.fdopen(fh, 'rb') as fd: + self.chunk_file(item, cache, self.stats, backup_io_iter(self.chunker.chunkify(fd, fh))) + if not is_special_file: + # we must not memorize special files, because the contents of e.g. a + # block or char device will change without its mtime/size/inode changing. + cache.memorize_file(path_hash, st, [c.id for c in item.chunks]) + status = status or 'M' # regular file, modified (if not 'A' already) + self.stats.nfiles += 1 + item.update(self.stat_attrs(st, path)) + item.get_size(memorize=True) + if is_special_file: + # we processed a special file like a regular file. reflect that in mode, + # so it can be extracted / accessed in FUSE mount like a regular file: + item.mode = stat.S_IFREG | stat.S_IMODE(item.mode) + return status @staticmethod def list_archives(repository, key, manifest, cache=None): From 1f6dc55eab6f2859746c9fd2fcffe3d09c517d40 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Mar 2017 14:03:39 +0200 Subject: [PATCH 0783/1387] simplify char/block device file dispatching --- src/borg/archive.py | 9 +++------ src/borg/archiver.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 4e331a77..d602faf4 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -864,14 +864,11 @@ Utilization of max. archive size: {csize_max:.0%} item.update(self.stat_attrs(st, path)) return status - def process_dev(self, path, st): - with self.create_helper(path, st, None) as (item, status, hardlinked, hardlink_master): # no status yet + def process_dev(self, path, st, dev_type): + with self.create_helper(path, st, dev_type) as (item, status, hardlinked, hardlink_master): # char/block device item.rdev = st.st_rdev item.update(self.stat_attrs(st, path)) - if stat.S_ISCHR(st.st_mode): - return 'c' # char device - elif stat.S_ISBLK(st.st_mode): - return 'b' # block device + return status def process_symlink(self, path, st): with self.create_helper(path, st, 's') as (item, status, hardlinked, hardlink_master): # symlink diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e73aaf2d..26cacba3 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -562,10 +562,16 @@ class Archiver: status = archive.process_fifo(path, st) else: status = archive.process_file(path, st, cache) - elif stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode): + elif stat.S_ISCHR(st.st_mode): if not dry_run: if not read_special: - status = archive.process_dev(path, st) + status = archive.process_dev(path, st, 'c') + else: + status = archive.process_file(path, st, cache) + elif stat.S_ISBLK(st.st_mode): + if not dry_run: + if not read_special: + status = archive.process_dev(path, st, 'b') else: status = archive.process_file(path, st, cache) elif stat.S_ISSOCK(st.st_mode): From 23cc6796177568f1ba2b3d56d823e4b0d7ed8043 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 Mar 2017 14:25:33 +0200 Subject: [PATCH 0784/1387] no hardlinking for directories and symlinks - nlink > 1 for dirs does not mean hardlinking (at least not everywhere, wondering how apple does it) - we can not archive hardlinked symlinks due to item.source dual-use, see issue #2343. likely nobody uses this anyway. --- src/borg/archive.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index d602faf4..8d1c8b95 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -835,11 +835,11 @@ Utilization of max. archive size: {csize_max:.0%} return attrs @contextmanager - def create_helper(self, path, st, status=None): + def create_helper(self, path, st, status=None, hardlinkable=True): safe_path = make_path_safe(path) item = Item(path=safe_path) hardlink_master = False - hardlinked = st.st_nlink > 1 + hardlinked = hardlinkable and st.st_nlink > 1 if hardlinked: source = self.hard_links.get((st.st_ino, st.st_dev)) if source is not None: @@ -855,7 +855,7 @@ Utilization of max. archive size: {csize_max:.0%} self.hard_links[(st.st_ino, st.st_dev)] = safe_path def process_dir(self, path, st): - with self.create_helper(path, st, 'd') as (item, status, hardlinked, hardlink_master): # directory + with self.create_helper(path, st, 'd', hardlinkable=False) as (item, status, hardlinked, hardlink_master): item.update(self.stat_attrs(st, path)) return status @@ -871,12 +871,14 @@ Utilization of max. archive size: {csize_max:.0%} return status def process_symlink(self, path, st): - with self.create_helper(path, st, 's') as (item, status, hardlinked, hardlink_master): # symlink + # note: using hardlinkable=False because we can not support hardlinked symlinks, + # due to the dual-use of item.source, see issue #2343: + with self.create_helper(path, st, 's', hardlinkable=False) as (item, status, hardlinked, hardlink_master): with backup_io('readlink'): source = os.readlink(path) - item.source = source # XXX this overwrites hardlink slave's usage of item.source - if hardlinked: - logger.warning('hardlinked symlinks will be extracted as non-hardlinked symlinks!') + item.source = source + if st.st_nlink > 1: + logger.warning('hardlinked symlinks will be archived as non-hardlinked symlinks!') item.update(self.stat_attrs(st, path)) return status From 32c6e3ad95b7607774591a79c5e0f9fe5b38ad85 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 28 Mar 2017 20:49:38 +0200 Subject: [PATCH 0785/1387] docs: tell what kind of hardlinks we support --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 1d2c316a..26a200af 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -124,7 +124,6 @@ Features & platforms Besides regular file and directory structures, |project_name| can preserve - * Hardlinks (considering all files in the same archive) * Symlinks (stored as symlink, the symlink is not followed) * Special files: @@ -132,6 +131,7 @@ Besides regular file and directory structures, |project_name| can preserve * FIFOs ("named pipes") * Special file *contents* can be backed up in ``--read-special`` mode. By default the metadata to create them with mknod(2), mkfifo(2) etc. is stored. + * Hardlinked regular files, devices, FIFOs (considering all items in the same archive) * Timestamps in nanosecond precision: mtime, atime, ctime * Permissions: From 3cc1cdd2eddd6e78750bc8df8e98f6eff1fe4f7c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 2 Apr 2017 01:19:46 +0200 Subject: [PATCH 0786/1387] extract: refactor hardlinks related code prepare for a extract_helper context manager (some changes may seem superfluous, but see the following changesets) --- src/borg/archive.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 8d1c8b95..9b3a7b3f 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -566,24 +566,25 @@ Utilization of max. archive size: {csize_max:.0%} if stat.S_ISREG(mode): with backup_io('makedirs'): make_parent(path) + hardlink_set = False # Hard link? if 'source' in item: source = os.path.join(dest, *item.source.split(os.sep)[stripped_components:]) - with backup_io('link'): - if item.source not in hardlink_masters: - os.link(source, path) - return - item.chunks, link_target = hardlink_masters[item.source] + chunks, link_target = hardlink_masters.get(item.source, (None, source)) if link_target: # Hard link was extracted previously, just link - with backup_io: + with backup_io('link'): os.link(link_target, path) - return - # Extract chunks, since the item which had the chunks was not extracted - with backup_io('open'): - fd = open(path, 'wb') + hardlink_set = True + elif chunks is not None: + # assign chunks to this item, since the item which had the chunks was not extracted + item.chunks = chunks + if hardlink_set: + return if sparse and self.zeros is None: self.zeros = b'\0' * (1 << self.chunker_params[1]) + with backup_io('open'): + fd = open(path, 'wb') with fd: ids = [c.id for c in item.chunks] for data in self.pipeline.fetch_many(ids, is_preloaded=True): @@ -595,7 +596,7 @@ Utilization of max. archive size: {csize_max:.0%} fd.seek(len(data), 1) else: fd.write(data) - with backup_io('truncate'): + with backup_io('truncate_and_attrs'): pos = item_chunks_size = fd.tell() fd.truncate(pos) fd.flush() @@ -608,7 +609,7 @@ Utilization of max. archive size: {csize_max:.0%} if has_damaged_chunks: logger.warning('File %s has damaged (all-zero) chunks. Try running borg check --repair.' % remove_surrogates(item.path)) - if hardlink_masters: + if not hardlink_set and hardlink_masters: # 2nd term, is it correct/needed? # Update master entry with extracted file path, so that following hardlinks don't extract twice. hardlink_masters[item.get('source') or original_path] = (None, path) return From 49f6128d1c2ae5d988e8fe3d0264a4e481c304b7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 4 Apr 2017 16:29:32 +0200 Subject: [PATCH 0787/1387] docs: serve: env vars in original commands are ignored --- docs/usage.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index 0844159c..ce7d0e34 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -403,11 +403,17 @@ Examples borg serve has special support for ssh forced commands (see ``authorized_keys`` example below): it will detect that you use such a forced command and extract the value of the ``--restrict-to-path`` option(s). + It will then parse the original command that came from the client, makes sure that it is also ``borg serve`` and enforce path restriction(s) as given by the forced command. That way, other options given by the client (like ``--info`` or ``--umask``) are preserved (and are not fixed by the forced command). +Environment variables (such as BORG_HOSTNAME_IS_UNIQUE) contained in the original +command sent by the client are *not* interpreted, but ignored. If BORG_XXX environment +variables should be set on the ``borg serve`` side, then these must be set in system-specific +locations like ``/etc/environment`` or in the forced command itself (example below). + :: # Allow an SSH keypair to only run borg, and only have access to /path/to/repo. @@ -416,6 +422,9 @@ forced command. That way, other options given by the client (like ``--info`` or $ cat ~/.ssh/authorized_keys command="borg serve --restrict-to-path /path/to/repo",no-pty,no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-user-rc ssh-rsa AAAAB3[...] + # Set a BORG_XXX environment variable on the "borg serve" side + $ cat ~/.ssh/authorized_keys + command="export BORG_XXX=value; borg serve [...]",restrict ssh-rsa [...] .. include:: usage/upgrade.rst.inc From cda74650385ed33af63090392febf23189cfa2e8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 2 Apr 2017 01:22:25 +0200 Subject: [PATCH 0788/1387] extract: indent code, no semantics change prepare for a extract_helper context manager (some changes may seem superfluous, but see the following changesets) --- src/borg/archive.py | 61 +++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 9b3a7b3f..7db9826e 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -579,36 +579,37 @@ Utilization of max. archive size: {csize_max:.0%} elif chunks is not None: # assign chunks to this item, since the item which had the chunks was not extracted item.chunks = chunks - if hardlink_set: - return - if sparse and self.zeros is None: - self.zeros = b'\0' * (1 << self.chunker_params[1]) - with backup_io('open'): - fd = open(path, 'wb') - with fd: - ids = [c.id for c in item.chunks] - for data in self.pipeline.fetch_many(ids, is_preloaded=True): - if pi: - pi.show(increase=len(data), info=[remove_surrogates(item.path)]) - with backup_io('write'): - if sparse and self.zeros.startswith(data): - # all-zero chunk: create a hole in a sparse file - fd.seek(len(data), 1) - else: - fd.write(data) - with backup_io('truncate_and_attrs'): - pos = item_chunks_size = fd.tell() - fd.truncate(pos) - fd.flush() - self.restore_attrs(path, item, fd=fd.fileno()) - if 'size' in item: - item_size = item.size - if item_size != item_chunks_size: - logger.warning('{}: size inconsistency detected: size {}, chunks size {}'.format( - item.path, item_size, item_chunks_size)) - if has_damaged_chunks: - logger.warning('File %s has damaged (all-zero) chunks. Try running borg check --repair.' % - remove_surrogates(item.path)) + if True: + if hardlink_set: + return + if sparse and self.zeros is None: + self.zeros = b'\0' * (1 << self.chunker_params[1]) + with backup_io('open'): + fd = open(path, 'wb') + with fd: + ids = [c.id for c in item.chunks] + for data in self.pipeline.fetch_many(ids, is_preloaded=True): + if pi: + pi.show(increase=len(data), info=[remove_surrogates(item.path)]) + with backup_io('write'): + if sparse and self.zeros.startswith(data): + # all-zero chunk: create a hole in a sparse file + fd.seek(len(data), 1) + else: + fd.write(data) + with backup_io('truncate_and_attrs'): + pos = item_chunks_size = fd.tell() + fd.truncate(pos) + fd.flush() + self.restore_attrs(path, item, fd=fd.fileno()) + if 'size' in item: + item_size = item.size + if item_size != item_chunks_size: + logger.warning('{}: size inconsistency detected: size {}, chunks size {}'.format( + item.path, item_size, item_chunks_size)) + if has_damaged_chunks: + logger.warning('File %s has damaged (all-zero) chunks. Try running borg check --repair.' % + remove_surrogates(item.path)) if not hardlink_set and hardlink_masters: # 2nd term, is it correct/needed? # Update master entry with extracted file path, so that following hardlinks don't extract twice. hardlink_masters[item.get('source') or original_path] = (None, path) From cb86bda4131e8fc0afb58378ac61b3705e8cfa25 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 2 Apr 2017 01:38:58 +0200 Subject: [PATCH 0789/1387] extract: implement extract_helper context manager Most code of the CM is just moved 1:1 from the regular file block. Use the CM for regular files, FIFOs and devices, but not for: - directories (can not have hardlinks) - symlinks (we can not support hardlinked symlinks) --- src/borg/archive.py | 55 ++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 7db9826e..417bf0d6 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -502,6 +502,26 @@ Utilization of max. archive size: {csize_max:.0%} cache.rollback() return stats + @contextmanager + def extract_helper(self, dest, item, path, stripped_components, original_path, hardlink_masters): + hardlink_set = False + # Hard link? + if 'source' in item: + source = os.path.join(dest, *item.source.split(os.sep)[stripped_components:]) + chunks, link_target = hardlink_masters.get(item.source, (None, source)) + if link_target: + # Hard link was extracted previously, just link + with backup_io('link'): + os.link(link_target, path) + hardlink_set = True + elif chunks is not None: + # assign chunks to this item, since the item which had the chunks was not extracted + item.chunks = chunks + yield hardlink_set + if not hardlink_set and hardlink_masters: # 2nd term, is it correct/needed? + # Update master entry with extracted item path, so that following hardlinks don't extract twice. + hardlink_masters[item.get('source') or original_path] = (None, path) + def extract_item(self, item, restore_attrs=True, dry_run=False, stdout=False, sparse=False, hardlink_masters=None, stripped_components=0, original_path=None, pi=None): """ @@ -566,20 +586,8 @@ Utilization of max. archive size: {csize_max:.0%} if stat.S_ISREG(mode): with backup_io('makedirs'): make_parent(path) - hardlink_set = False - # Hard link? - if 'source' in item: - source = os.path.join(dest, *item.source.split(os.sep)[stripped_components:]) - chunks, link_target = hardlink_masters.get(item.source, (None, source)) - if link_target: - # Hard link was extracted previously, just link - with backup_io('link'): - os.link(link_target, path) - hardlink_set = True - elif chunks is not None: - # assign chunks to this item, since the item which had the chunks was not extracted - item.chunks = chunks - if True: + with self.extract_helper(dest, item, path, stripped_components, original_path, + hardlink_masters) as hardlink_set: if hardlink_set: return if sparse and self.zeros is None: @@ -610,9 +618,6 @@ Utilization of max. archive size: {csize_max:.0%} if has_damaged_chunks: logger.warning('File %s has damaged (all-zero) chunks. Try running borg check --repair.' % remove_surrogates(item.path)) - if not hardlink_set and hardlink_masters: # 2nd term, is it correct/needed? - # Update master entry with extracted file path, so that following hardlinks don't extract twice. - hardlink_masters[item.get('source') or original_path] = (None, path) return with backup_io: # No repository access beyond this point. @@ -632,12 +637,20 @@ Utilization of max. archive size: {csize_max:.0%} self.restore_attrs(path, item, symlink=True) elif stat.S_ISFIFO(mode): make_parent(path) - os.mkfifo(path) - self.restore_attrs(path, item) + with self.extract_helper(dest, item, path, stripped_components, original_path, + hardlink_masters) as hardlink_set: + if hardlink_set: + return + os.mkfifo(path) + self.restore_attrs(path, item) elif stat.S_ISCHR(mode) or stat.S_ISBLK(mode): make_parent(path) - os.mknod(path, item.mode, item.rdev) - self.restore_attrs(path, item) + with self.extract_helper(dest, item, path, stripped_components, original_path, + hardlink_masters) as hardlink_set: + if hardlink_set: + return + os.mknod(path, item.mode, item.rdev) + self.restore_attrs(path, item) else: raise Exception('Unknown archive item type %r' % item.mode) From 8f769a9b24ed55367d44ce362084e81e2539576c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 2 Apr 2017 02:46:44 +0200 Subject: [PATCH 0790/1387] implement and use hardlinkable() helper --- src/borg/archive.py | 5 +++-- src/borg/archiver.py | 7 ++++--- src/borg/fuse.py | 4 ++-- src/borg/helpers.py | 5 +++++ 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 417bf0d6..27917ed9 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -25,6 +25,7 @@ from .compress import Compressor, CompressionSpec from .constants import * # NOQA from .hashindex import ChunkIndex, ChunkIndexEntry from .helpers import Manifest +from .helpers import hardlinkable from .helpers import ChunkIteratorFileWrapper, open_item from .helpers import Error, IntegrityError, set_ec from .helpers import uid2user, user2uid, gid2group, group2gid @@ -1623,7 +1624,7 @@ class ArchiveRecreater: def item_is_hardlink_master(item): return (target_is_subset and - stat.S_ISREG(item.mode) and + hardlinkable(item.mode) and item.get('hardlink_master', True) and 'source' not in item) @@ -1633,7 +1634,7 @@ class ArchiveRecreater: if item_is_hardlink_master(item): hardlink_masters[item.path] = (item.get('chunks'), None) continue - if target_is_subset and stat.S_ISREG(item.mode) and item.get('source') in hardlink_masters: + if target_is_subset and hardlinkable(item.mode) and item.get('source') in hardlink_masters: # master of this hard link is outside the target subset chunks, new_source = hardlink_masters[item.source] if new_source is None: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 26cacba3..002e664a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -48,6 +48,7 @@ from .helpers import prune_within, prune_split from .helpers import to_localtime, timestamp from .helpers import get_cache_dir from .helpers import Manifest +from .helpers import hardlinkable from .helpers import StableDict from .helpers import check_extension_modules from .helpers import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern @@ -634,7 +635,7 @@ class Archiver: hardlink_masters = {} if partial_extract else None def peek_and_store_hardlink_masters(item, matched): - if (partial_extract and not matched and stat.S_ISREG(item.mode) and + if (partial_extract and not matched and hardlinkable(item.mode) and item.get('hardlink_master', True) and 'source' not in item): hardlink_masters[item.get('path')] = (item.get('chunks'), None) @@ -726,7 +727,7 @@ class Archiver: return [None] def has_hardlink_master(item, hardlink_masters): - return stat.S_ISREG(item.mode) and item.get('source') in hardlink_masters + return hardlinkable(item.mode) and item.get('source') in hardlink_masters def compare_link(item1, item2): # These are the simple link cases. For special cases, e.g. if a @@ -822,7 +823,7 @@ class Archiver: def compare_archives(archive1, archive2, matcher): def hardlink_master_seen(item): - return 'source' not in item or not stat.S_ISREG(item.mode) or item.source in hardlink_masters + return 'source' not in item or not hardlinkable(item.mode) or item.source in hardlink_masters def is_hardlink_master(item): return item.get('hardlink_master', True) and 'source' not in item diff --git a/src/borg/fuse.py b/src/borg/fuse.py index fc19e6e0..20782544 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -16,7 +16,7 @@ from .logger import create_logger logger = create_logger() from .archive import Archive -from .helpers import daemonize +from .helpers import daemonize, hardlinkable from .item import Item from .lrucache import LRUCache @@ -193,7 +193,7 @@ class FuseOperations(llfuse.Operations): path = item.path del item.path # safe some space - if 'source' in item and stat.S_ISREG(item.mode): + if 'source' in item and hardlinkable(item.mode): # a hardlink, no contents, is the hardlink master source = os.fsencode(os.path.normpath(item.source)) if self.versions: diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 90213243..c1da4729 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1974,6 +1974,11 @@ def file_status(mode): return '?' +def hardlinkable(mode): + """return True if we support hardlinked items of this type""" + return stat.S_ISREG(mode) or stat.S_ISBLK(mode) or stat.S_ISCHR(mode) or stat.S_ISFIFO(mode) + + def chunkit(it, size): """ Chunk an iterator into pieces of . From cc24fa20645c3d57a7987658bbcf80a25ba8866b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 5 Apr 2017 12:19:56 +0200 Subject: [PATCH 0791/1387] format_line: whitelist instead of checking against blacklist --- src/borg/helpers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 2d265fc2..1dd07d09 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -787,12 +787,10 @@ def format_line(format, data): for _, key, _, conversion in Formatter().parse(format): if not key: continue - if '.' in key or '__' in key or conversion: + if conversion or key not in data: raise InvalidPlaceholder(key, format) try: - return format.format(**data) - except KeyError as ke: - raise InvalidPlaceholder(ke.args[0], format) + return format.format_map(data) except Exception as e: raise PlaceholderError(format, data, e.__class__.__name__, str(e)) From 155f38c2333b3892527796d96e999149d253616e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 5 Apr 2017 13:54:58 +0200 Subject: [PATCH 0792/1387] remove comment about strange hardlink_masters term (maybe revisit this later, this is not in scope of the generic hardlinks refactor) --- src/borg/archive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 27917ed9..cdbd483e 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -519,7 +519,7 @@ Utilization of max. archive size: {csize_max:.0%} # assign chunks to this item, since the item which had the chunks was not extracted item.chunks = chunks yield hardlink_set - if not hardlink_set and hardlink_masters: # 2nd term, is it correct/needed? + if not hardlink_set and hardlink_masters: # Update master entry with extracted item path, so that following hardlinks don't extract twice. hardlink_masters[item.get('source') or original_path] = (None, path) From c7256abd84bf5a067f9941d9289e837f673977dc Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 6 Apr 2017 01:03:24 +0200 Subject: [PATCH 0793/1387] borg rename: expand placeholders, fixes #2386 --- src/borg/archiver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 0bb21137..af21e060 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -59,6 +59,7 @@ from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm from .helpers import ErrorIgnoringTextIOWrapper from .helpers import ProgressIndicatorPercent from .helpers import basic_json_data, json_print +from .helpers import replace_placeholders from .item import Item from .key import key_creator, tam_required_file, tam_required, RepoKey, PassphraseKey from .keymanager import KeyManager @@ -905,7 +906,8 @@ class Archiver: @with_archive def do_rename(self, args, repository, manifest, key, cache, archive): """Rename an existing archive""" - archive.rename(args.name) + name = replace_placeholders(args.name) + archive.rename(name) manifest.write() repository.commit() cache.commit() From 03ed4d31ca1ecf9d69cb1121d068be871db17041 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 6 Apr 2017 11:36:50 +0200 Subject: [PATCH 0794/1387] recreate: expand placeholders --- src/borg/archiver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index af21e060..f999cb97 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1347,10 +1347,11 @@ class Archiver: if args.location.archive: name = args.location.archive + target = replace_placeholders(args.target) if args.target else None if recreater.is_temporary_archive(name): self.print_error('Refusing to work on temporary archive of prior recreate: %s', name) return self.exit_code - recreater.recreate(name, args.comment, args.target) + recreater.recreate(name, args.comment, target) else: if args.target is not None: self.print_error('--target: Need to specify single archive') From 8c2064cc5a63fc4f5b25eed30edb681b44b0fb08 Mon Sep 17 00:00:00 2001 From: Radu Ciorba Date: Mon, 20 Feb 2017 14:58:41 +0200 Subject: [PATCH 0795/1387] add extra test for the hashindex Insert a few keys, delete some of them, check we still have the values we expect, check the deleted ones aren't there. --- src/borg/testsuite/hashindex.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 27747b42..4820c38c 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -125,6 +125,31 @@ class HashIndexTestCase(BaseTestCase): assert unique_chunks == 3 +class HashIndexExtraTestCase(BaseTestCase): + """These tests are separate because they should not become part of the selftest + """ + def test_chunk_indexer(self): + # see _hashindex.c hash_sizes, we want to be close to the max fill rate + # because interesting errors happen there + max_key = int(65537 * 0.75) - 10 + index = ChunkIndex(max_key) + deleted_keys = [ + hashlib.sha256(H(k)).digest() + for k in range(-1, -(max_key//3), -1)] + keys = [hashlib.sha256(H(k)).digest() for k in range(max_key)] + for i, key in enumerate(keys): + index[key] = (i, i, i) + for i, key in enumerate(deleted_keys): + index[key] = (i, i, i) + + for key in deleted_keys: + del index[key] + for i, key in enumerate(keys): + assert index[key] == (i, i, i) + for key in deleted_keys: + assert index.get(key) is None + + class HashIndexSizeTestCase(BaseTestCase): def test_size_on_disk(self): idx = ChunkIndex() From 9f31dba7b50f3c0ba4b42257f91e6fb0ffb35f19 Mon Sep 17 00:00:00 2001 From: Radu Ciorba Date: Fri, 7 Apr 2017 15:56:13 +0300 Subject: [PATCH 0796/1387] cleanup the test and add more checks --- src/borg/testsuite/hashindex.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 4820c38c..11639907 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -126,29 +126,35 @@ class HashIndexTestCase(BaseTestCase): class HashIndexExtraTestCase(BaseTestCase): - """These tests are separate because they should not become part of the selftest + """These tests are separate because they should not become part of the selftest. """ def test_chunk_indexer(self): - # see _hashindex.c hash_sizes, we want to be close to the max fill rate - # because interesting errors happen there - max_key = int(65537 * 0.75) - 10 - index = ChunkIndex(max_key) - deleted_keys = [ - hashlib.sha256(H(k)).digest() - for k in range(-1, -(max_key//3), -1)] - keys = [hashlib.sha256(H(k)).digest() for k in range(max_key)] + # see _hashindex.c hash_sizes, we want to be close to the max. load + # because interesting errors happen there. + key_count = int(65537 * ChunkIndex.MAX_LOAD_FACTOR) - 10 + index = ChunkIndex(key_count) + all_keys = [hashlib.sha256(H(k)).digest() for k in range(key_count)] + # we're gonna delete 1/3 of all_keys, so let's split them 2/3 and 1/3: + keys, to_delete_keys = all_keys[0:(2*key_count//3)], all_keys[(2*key_count//3):] + for i, key in enumerate(keys): index[key] = (i, i, i) - for i, key in enumerate(deleted_keys): + for i, key in enumerate(to_delete_keys): index[key] = (i, i, i) - for key in deleted_keys: + for key in to_delete_keys: del index[key] for i, key in enumerate(keys): assert index[key] == (i, i, i) - for key in deleted_keys: + for key in to_delete_keys: assert index.get(key) is None + # now delete every key still in the index + for key in keys: + del index[key] + # the index should now be empty + assert list(index.iteritems()) == [] + class HashIndexSizeTestCase(BaseTestCase): def test_size_on_disk(self): From f64f432e513d35b82e03ca47b84f46e591e6d8fa Mon Sep 17 00:00:00 2001 From: Patrick Goering Date: Sat, 8 Apr 2017 17:10:02 +0200 Subject: [PATCH 0797/1387] catch exception for os.link when hardlinks are not supported --- src/borg/repository.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 47a52015..b6340c4a 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -190,7 +190,7 @@ class Repository: try: os.link(config_path, old_config_path) except OSError as e: - if e.errno in (errno.EMLINK, errno.EPERM): + if e.errno in (errno.EMLINK, errno.ENOSYS, errno.EPERM): logger.warning("Hardlink failed, cannot securely erase old config file") else: raise From ca9551654003f52fd569b10e40bf416cb5513a28 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 9 Apr 2017 21:20:58 +0200 Subject: [PATCH 0798/1387] update CHANGES (master) --- docs/changes.rst | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 686ac9b7..e9dc0c20 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -134,6 +134,46 @@ Version 1.1.0b5 (not released) Compatibility notes: - BORG_HOSTNAME_IS_UNIQUE is now on by default. +- remove --compression-from feature +- recreate: add --recompress flag, unify --always-recompress and + --recompress + +Fixes: + +- catch exception for os.link when hardlinks are not supported +- borg rename / recreate: expand placeholders, fixes #2386 +- generic support for hardlinks (files, devices, FIFOs) +- extract: also create parent dir for device files, if needed. +- extract: if a hardlink master is not in the to-be-extracted subset, + the "x" status was not displayed for it. + +Other changes: + +- refactor compression decision stuff +- change global compression default to lz4 as well, to be consistent + with --compression defaults. +- placeholders: deny access to internals and other unspecified stuff +- clearer error message for unrecognized placeholder +- docs: + + - placeholders: document escaping + - serve: env vars in original commands are ignored + - tell what kind of hardlinks we support + - more docs about compression + - LICENSE: use canonical formulation + ("copyright holders and contributors" instead of "author") +- tests: + + - enhance travis setuptools_scm situation + - add extra test for the hashindex + +These belong to 1.1.0b4 release, but did not make it into changelog by then: + +- vagrant: increase memory for parallel testing +- lz4 compress: lower max. buffer size, exception handling +- add docstring to do_benchmark_crud +- patterns help: mention path full-match in intro + Version 1.1.0b4 (2017-03-27) ---------------------------- From 798127f6361e58bab92f6a39986116732b7abc48 Mon Sep 17 00:00:00 2001 From: Mark Edgington Date: Mon, 20 Mar 2017 18:04:45 -0400 Subject: [PATCH 0799/1387] allow excluding parent and including child, fixes #2314 This fixes the problem raised by issue #2314 by requiring that each root subtree be fully traversed. The problem occurs when a patterns file excludes a parent directory P later in the file, but earlier in the file a subdirectory S of P is included. Because a tree is processed recursively with a depth-first search, P is processed before S is. Previously, if P was excluded, then S would not even be considered. Now, it is possible to recurse into P nonetheless, while not adding P (as a directory entry) to the archive. With this commit, a `-` in a patterns-file will allow an excluded directory to be searched for matching descendants. If the old behavior is desired, it can be achieved by using a `!` in place of the `-`. The following is a list of specific changes made by this commit: * renamed InclExclPattern named-tuple -> CmdTuple (with names 'val' and 'cmd'), since it is used more generally for commands, and not only for representing patterns. * represent commands as IECommand enum types (RootPath, PatternStyle, Include, Exclude, ExcludeNoRecurse) * archiver: Archiver.build_matcher() paths arg renamed -> include_paths to prevent confusion as to whether the list of paths are to be included or excluded. * helpers: PatternMatcher has recurse_dir attribute that is used to communicate whether an excluded dir should be recursed (used by Archiver._process()) * archiver: Archiver.build_matcher() now only returns a PatternMatcher instance, and not an include_patterns list -- this list is now created and housed within the PatternMatcher instance, and can be accessed from there. * moved operation of finding unmatched patterns from Archiver to PatternMatcher.get_unmatched_include_patterns() * added / modified some documentation of code * renamed _PATTERN_STYLES -> _PATTERN_CLASSES since "style" is ambiguous and this helps clarify that the set contains classes and not instances. * have PatternBase subclass instances store whether excluded dirs are to be recursed. Because PatternBase objs are created corresponding to each +, -, ! command it is necessary to differentiate - from ! within these objects. * add test for '!' exclusion rule (which doesn't recurse) --- src/borg/archive.py | 12 +- src/borg/archiver.py | 56 +++++----- src/borg/helpers.py | 198 +++++++++++++++++++++++---------- src/borg/testsuite/archiver.py | 37 +++++- src/borg/testsuite/helpers.py | 16 ++- 5 files changed, 219 insertions(+), 100 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 720596ee..4dedc0f3 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -36,7 +36,7 @@ from .helpers import StableDict from .helpers import bin_to_hex from .helpers import safe_ns from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi -from .helpers import PathPrefixPattern, FnmatchPattern +from .helpers import PathPrefixPattern, FnmatchPattern, IECommand from .item import Item, ArchiveItem from .key import key_factory from .platform import acl_get, acl_set, set_flags, get_flags, swidth @@ -1721,10 +1721,10 @@ class ArchiveRecreater: """Add excludes to the matcher created by exclude_cache and exclude_if_present.""" def exclude(dir, tag_item): if self.keep_exclude_tags: - tag_files.append(PathPrefixPattern(tag_item.path)) - tagged_dirs.append(FnmatchPattern(dir + '/')) + tag_files.append(PathPrefixPattern(tag_item.path, recurse_dir=False)) + tagged_dirs.append(FnmatchPattern(dir + '/', recurse_dir=False)) else: - tagged_dirs.append(PathPrefixPattern(dir)) + tagged_dirs.append(PathPrefixPattern(dir, recurse_dir=False)) matcher = self.matcher tag_files = [] @@ -1747,8 +1747,8 @@ class ArchiveRecreater: file = open_item(archive, cachedir_masters[item.source]) if file.read(len(CACHE_TAG_CONTENTS)).startswith(CACHE_TAG_CONTENTS): exclude(dir, item) - matcher.add(tag_files, True) - matcher.add(tagged_dirs, False) + matcher.add(tag_files, IECommand.Include) + matcher.add(tagged_dirs, IECommand.ExcludeNoRecurse) def create_target(self, archive, target_name=None): """Create target archive.""" diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f999cb97..f2c26cf9 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -54,7 +54,7 @@ from .helpers import check_extension_modules from .helpers import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern from .helpers import dir_is_tagged, is_slow_msgpack, yes, sysinfo from .helpers import log_multi -from .helpers import parse_pattern, PatternMatcher, PathPrefixPattern +from .helpers import PatternMatcher from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm from .helpers import ErrorIgnoringTextIOWrapper from .helpers import ProgressIndicatorPercent @@ -190,16 +190,11 @@ class Archiver: bi += slicelen @staticmethod - def build_matcher(inclexcl_patterns, paths): + def build_matcher(inclexcl_patterns, include_paths): matcher = PatternMatcher() - if inclexcl_patterns: - matcher.add_inclexcl(inclexcl_patterns) - include_patterns = [] - if paths: - include_patterns.extend(parse_pattern(i, PathPrefixPattern) for i in paths) - matcher.add(include_patterns, True) - matcher.fallback = not include_patterns - return matcher, include_patterns + matcher.add_inclexcl(inclexcl_patterns) + matcher.add_includepaths(include_paths) + return matcher def do_serve(self, args): """Start in server mode. This command is usually not used manually.""" @@ -493,13 +488,20 @@ class Archiver: This should only raise on critical errors. Per-item errors must be handled within this method. """ + if st is None: + with backup_io('stat'): + st = os.lstat(path) + + recurse_excluded_dir = False if not matcher.match(path): self.print_file_status('x', path) - return + + if stat.S_ISDIR(st.st_mode) and matcher.recurse_dir: + recurse_excluded_dir = True + else: + return + try: - if st is None: - with backup_io('stat'): - st = os.lstat(path) if (st.st_ino, st.st_dev) in skip_inodes: return # if restrict_dev is given, we do not want to recurse into a new filesystem, @@ -527,7 +529,8 @@ class Archiver: read_special=read_special, dry_run=dry_run) return if not dry_run: - status = archive.process_dir(path, st) + if not recurse_excluded_dir: + status = archive.process_dir(path, st) if recurse: with backup_io('scandir'): entries = helpers.scandir_inorder(path) @@ -590,7 +593,9 @@ class Archiver: status = '?' # need to add a status code somewhere else: status = '-' # dry run, item was not backed up - self.print_file_status(status, path) + + if not recurse_excluded_dir: + self.print_file_status(status, path) @staticmethod def build_filter(matcher, peek_and_store_hardlink_masters, strip_components): @@ -616,7 +621,7 @@ class Archiver: if sys.platform.startswith(('linux', 'freebsd', 'netbsd', 'openbsd', 'darwin', )): logger.warning('Hint: You likely need to fix your locale setup. E.g. install locales and use: LANG=en_US.UTF-8') - matcher, include_patterns = self.build_matcher(args.patterns, args.paths) + matcher = self.build_matcher(args.patterns, args.paths) progress = args.progress output_list = args.output_list @@ -681,9 +686,8 @@ class Archiver: archive.extract_item(dir_item) except BackupOSError as e: self.print_warning('%s: %s', remove_surrogates(dir_item.path), e) - for pattern in include_patterns: - if pattern.match_count == 0: - self.print_warning("Include pattern '%s' never matched.", pattern) + for pattern in matcher.get_unmatched_include_patterns(): + self.print_warning("Include pattern '%s' never matched.", pattern) if pi: # clear progress output pi.finish() @@ -893,13 +897,13 @@ class Archiver: 'If you know for certain that they are the same, pass --same-chunker-params ' 'to override this check.') - matcher, include_patterns = self.build_matcher(args.patterns, args.paths) + matcher = self.build_matcher(args.patterns, args.paths) compare_archives(archive1, archive2, matcher) - for pattern in include_patterns: - if pattern.match_count == 0: - self.print_warning("Include pattern '%s' never matched.", pattern) + for pattern in matcher.get_unmatched_include_patterns(): + self.print_warning("Include pattern '%s' never matched.", pattern) + return self.exit_code @with_repository(exclusive=True, cache=True) @@ -1048,7 +1052,7 @@ class Archiver: return self._list_repository(args, manifest, write) def _list_archive(self, args, repository, manifest, key, write): - matcher, _ = self.build_matcher(args.patterns, args.paths) + matcher = self.build_matcher(args.patterns, args.paths) if args.format is not None: format = args.format elif args.short: @@ -1330,7 +1334,7 @@ class Archiver: env_var_override='BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING'): return EXIT_ERROR - matcher, include_patterns = self.build_matcher(args.patterns, args.paths) + matcher = self.build_matcher(args.patterns, args.paths) self.output_list = args.output_list self.output_filter = args.output_filter recompress = args.recompress != 'never' diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 2766c041..a52ff26b 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -23,6 +23,7 @@ import uuid from binascii import hexlify from collections import namedtuple, deque, abc, Counter from datetime import datetime, timezone, timedelta +from enum import Enum from fnmatch import translate from functools import wraps, partial, lru_cache from itertools import islice @@ -388,23 +389,24 @@ def parse_timestamp(timestamp): return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc) -def parse_add_pattern(patternstr, roots, patterns, fallback): - """Parse a pattern string and add it to roots or patterns depending on the pattern type.""" - pattern = parse_inclexcl_pattern(patternstr, fallback=fallback) - if pattern.ptype is RootPath: - roots.append(pattern.pattern) - elif pattern.ptype is PatternStyle: - fallback = pattern.pattern +def parse_patternfile_line(line, roots, ie_commands, fallback): + """Parse a pattern-file line and act depending on which command it represents.""" + ie_command = parse_inclexcl_command(line, fallback=fallback) + if ie_command.cmd is IECommand.RootPath: + roots.append(ie_command.val) + elif ie_command.cmd is IECommand.PatternStyle: + fallback = ie_command.val else: - patterns.append(pattern) + # it is some kind of include/exclude command + ie_commands.append(ie_command) return fallback -def load_pattern_file(fileobj, roots, patterns, fallback=None): +def load_pattern_file(fileobj, roots, ie_commands, fallback=None): if fallback is None: fallback = ShellPattern # ShellPattern is defined later in this module - for patternstr in clean_lines(fileobj): - fallback = parse_add_pattern(patternstr, roots, patterns, fallback) + for line in clean_lines(fileobj): + fallback = parse_patternfile_line(line, roots, ie_commands, fallback) def load_exclude_file(fileobj, patterns): @@ -417,7 +419,7 @@ class ArgparsePatternAction(argparse.Action): super().__init__(nargs=nargs, **kw) def __call__(self, parser, args, values, option_string=None): - parse_add_pattern(values[0], args.paths, args.patterns, ShellPattern) + parse_patternfile_line(values[0], args.paths, args.patterns, ShellPattern) class ArgparsePatternFileAction(argparse.Action): @@ -442,6 +444,11 @@ class ArgparseExcludeFileAction(ArgparsePatternFileAction): class PatternMatcher: + """Represents a collection of pattern objects to match paths against. + + *fallback* is a boolean value that *match()* returns if no matching patterns are found. + + """ def __init__(self, fallback=None): self._items = [] @@ -451,42 +458,88 @@ class PatternMatcher: # optimizations self._path_full_patterns = {} # full path -> return value + # indicates whether the last match() call ended on a pattern for which + # we should recurse into any matching folder. Will be set to True or + # False when calling match(). + self.recurse_dir = None + + # whether to recurse into directories when no match is found + # TODO: allow modification as a config option? + self.recurse_dir_default = True + + self.include_patterns = [] + + # TODO: move this info to parse_inclexcl_command and store in PatternBase subclass? + self.is_include_cmd = { + IECommand.Exclude: False, + IECommand.ExcludeNoRecurse: False, + IECommand.Include: True + } + def empty(self): return not len(self._items) and not len(self._path_full_patterns) - def _add(self, pattern, value): + def _add(self, pattern, cmd): + """*cmd* is an IECommand value. + """ if isinstance(pattern, PathFullPattern): key = pattern.pattern # full, normalized path - self._path_full_patterns[key] = value + self._path_full_patterns[key] = cmd else: - self._items.append((pattern, value)) + self._items.append((pattern, cmd)) - def add(self, patterns, value): - """Add list of patterns to internal list. The given value is returned from the match function when one of the - given patterns matches. + def add(self, patterns, cmd): + """Add list of patterns to internal list. *cmd* indicates whether the + pattern is an include/exclude pattern, and whether recursion should be + done on excluded folders. """ for pattern in patterns: - self._add(pattern, value) + self._add(pattern, cmd) + + def add_includepaths(self, include_paths): + """Used to add inclusion-paths from args.paths (from commandline). + """ + include_patterns = [parse_pattern(p, PathPrefixPattern) for p in include_paths] + self.add(include_patterns, IECommand.Include) + self.fallback = not include_patterns + self.include_patterns = include_patterns + + def get_unmatched_include_patterns(self): + "Note that this only returns patterns added via *add_includepaths*." + return [p for p in self.include_patterns if p.match_count == 0] def add_inclexcl(self, patterns): - """Add list of patterns (of type InclExclPattern) to internal list. The patterns ptype member is returned from - the match function when one of the given patterns matches. + """Add list of patterns (of type CmdTuple) to internal list. """ - for pattern, pattern_type in patterns: - self._add(pattern, pattern_type) + for pattern, cmd in patterns: + self._add(pattern, cmd) def match(self, path): + """Return True or False depending on whether *path* is matched. + + If no match is found among the patterns in this matcher, then the value + in self.fallback is returned (defaults to None). + + """ path = normalize_path(path) # do a fast lookup for full path matches (note: we do not count such matches): non_existent = object() value = self._path_full_patterns.get(path, non_existent) + if value is not non_existent: # we have a full path match! + # TODO: get from pattern; don't hard-code + self.recurse_dir = True return value + # this is the slow way, if we have many patterns in self._items: - for (pattern, value) in self._items: + for (pattern, cmd) in self._items: if pattern.match(path, normalize=False): - return value + self.recurse_dir = pattern.recurse_dir + return self.is_include_cmd[cmd] + + # by default we will recurse if there is no match + self.recurse_dir = self.recurse_dir_default return self.fallback @@ -502,14 +555,15 @@ class PatternBase: """ PREFIX = NotImplemented - def __init__(self, pattern): + def __init__(self, pattern, recurse_dir=False): self.pattern_orig = pattern self.match_count = 0 pattern = normalize_path(pattern) self._prepare(pattern) + self.recurse_dir = recurse_dir def match(self, path, normalize=True): - """match the given path against this pattern. + """Return a boolean indicating whether *path* is matched by this pattern. If normalize is True (default), the path will get normalized using normalize_path(), otherwise it is assumed that it already is normalized using that function. @@ -528,6 +582,7 @@ class PatternBase: return self.pattern_orig def _prepare(self, pattern): + "Should set the value of self.pattern" raise NotImplementedError def _match(self, path): @@ -625,7 +680,7 @@ class RegexPattern(PatternBase): return (self.regex.search(path) is not None) -_PATTERN_STYLES = set([ +_PATTERN_CLASSES = set([ FnmatchPattern, PathFullPattern, PathPrefixPattern, @@ -633,65 +688,86 @@ _PATTERN_STYLES = set([ ShellPattern, ]) -_PATTERN_STYLE_BY_PREFIX = dict((i.PREFIX, i) for i in _PATTERN_STYLES) +_PATTERN_CLASS_BY_PREFIX = dict((i.PREFIX, i) for i in _PATTERN_CLASSES) -InclExclPattern = namedtuple('InclExclPattern', 'pattern ptype') -RootPath = object() -PatternStyle = object() +CmdTuple = namedtuple('CmdTuple', 'val cmd') -def get_pattern_style(prefix): +class IECommand(Enum): + """A command that an InclExcl file line can represent. + """ + RootPath = 1 + PatternStyle = 2 + Include = 3 + Exclude = 4 + ExcludeNoRecurse = 5 + + +def get_pattern_class(prefix): try: - return _PATTERN_STYLE_BY_PREFIX[prefix] + return _PATTERN_CLASS_BY_PREFIX[prefix] except KeyError: raise ValueError("Unknown pattern style: {}".format(prefix)) from None -def parse_pattern(pattern, fallback=FnmatchPattern): +def parse_pattern(pattern, fallback=FnmatchPattern, recurse_dir=True): """Read pattern from string and return an instance of the appropriate implementation class. + """ if len(pattern) > 2 and pattern[2] == ":" and pattern[:2].isalnum(): (style, pattern) = (pattern[:2], pattern[3:]) - cls = get_pattern_style(style) + cls = get_pattern_class(style) else: cls = fallback - return cls(pattern) + return cls(pattern, recurse_dir) -def parse_exclude_pattern(pattern, fallback=FnmatchPattern): +def parse_exclude_pattern(pattern_str, fallback=FnmatchPattern): """Read pattern from string and return an instance of the appropriate implementation class. """ - epattern = parse_pattern(pattern, fallback) - return InclExclPattern(epattern, False) + epattern_obj = parse_pattern(pattern_str, fallback) + return CmdTuple(epattern_obj, IECommand.Exclude) -def parse_inclexcl_pattern(pattern, fallback=ShellPattern): - """Read pattern from string and return a InclExclPattern object.""" - type_prefix_map = { - '-': False, - '+': True, - 'R': RootPath, - 'r': RootPath, - 'P': PatternStyle, - 'p': PatternStyle, +def parse_inclexcl_command(cmd_line_str, fallback=ShellPattern): + """Read a --patterns-from command from string and return a CmdTuple object.""" + + cmd_prefix_map = { + '-': IECommand.Exclude, + '!': IECommand.ExcludeNoRecurse, + '+': IECommand.Include, + 'R': IECommand.RootPath, + 'r': IECommand.RootPath, + 'P': IECommand.PatternStyle, + 'p': IECommand.PatternStyle, } + try: - ptype = type_prefix_map[pattern[0]] - pattern = pattern[1:].lstrip() - if not pattern: - raise ValueError("Missing pattern!") + cmd = cmd_prefix_map[cmd_line_str[0]] + + # remaining text on command-line following the command character + remainder_str = cmd_line_str[1:].lstrip() + + if not remainder_str: + raise ValueError("Missing pattern/information!") except (IndexError, KeyError, ValueError): - raise argparse.ArgumentTypeError("Unable to parse pattern: {}".format(pattern)) - if ptype is RootPath: - pobj = pattern - elif ptype is PatternStyle: + raise argparse.ArgumentTypeError("Unable to parse pattern/command: {}".format(cmd_line_str)) + + if cmd is IECommand.RootPath: + # TODO: validate string? + val = remainder_str + elif cmd is IECommand.PatternStyle: + # then remainder_str is something like 're' or 'sh' try: - pobj = get_pattern_style(pattern) + val = get_pattern_class(remainder_str) except ValueError: - raise argparse.ArgumentTypeError("Unable to parse pattern: {}".format(pattern)) + raise argparse.ArgumentTypeError("Invalid pattern style: {}".format(remainder_str)) else: - pobj = parse_pattern(pattern, fallback) - return InclExclPattern(pobj, ptype) + # determine recurse_dir based on command type + recurse_dir = cmd not in [IECommand.ExcludeNoRecurse] + val = parse_pattern(remainder_str, fallback, recurse_dir) + + return CmdTuple(val, cmd) def timestamp(s): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index dc291940..366900d3 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -37,6 +37,7 @@ from ..helpers import PatternMatcher, parse_pattern, Location, get_security_dir from ..helpers import Manifest from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from ..helpers import bin_to_hex +from ..helpers import IECommand from ..item import Item from ..key import KeyfileKeyBase, RepoKey, KeyfileKey, Passphrase, TAMRequiredError from ..keymanager import RepoIdMismatch, NotABorgKeyFile @@ -929,6 +930,40 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('x input/file2', output) self.assert_in('x input/otherfile', output) + def test_create_pattern_exclude_folder_but_recurse(self): + """test when patterns exclude a parent folder, but include a child""" + self.patterns_file_path2 = os.path.join(self.tmpdir, 'patterns2') + with open(self.patterns_file_path2, 'wb') as fd: + fd.write(b'+ input/x/b\n- input/x*\n') + + self.cmd('init', '--encryption=repokey', self.repository_location) + self.create_regular_file('x/a/foo_a', size=1024 * 80) + self.create_regular_file('x/b/foo_b', size=1024 * 80) + self.create_regular_file('y/foo_y', size=1024 * 80) + output = self.cmd('create', '-v', '--list', + '--patterns-from=' + self.patterns_file_path2, + self.repository_location + '::test', 'input') + self.assert_in('x input/x/a/foo_a', output) + self.assert_in("A input/x/b/foo_b", output) + self.assert_in('A input/y/foo_y', output) + + def test_create_pattern_exclude_folder_no_recurse(self): + """test when patterns exclude a parent folder and, but include a child""" + self.patterns_file_path2 = os.path.join(self.tmpdir, 'patterns2') + with open(self.patterns_file_path2, 'wb') as fd: + fd.write(b'+ input/x/b\n! input/x*\n') + + self.cmd('init', '--encryption=repokey', self.repository_location) + self.create_regular_file('x/a/foo_a', size=1024 * 80) + self.create_regular_file('x/b/foo_b', size=1024 * 80) + self.create_regular_file('y/foo_y', size=1024 * 80) + output = self.cmd('create', '-v', '--list', + '--patterns-from=' + self.patterns_file_path2, + self.repository_location + '::test', 'input') + self.assert_not_in('input/x/a/foo_a', output) + self.assert_not_in('input/x/a', output) + self.assert_in('A input/y/foo_y', output) + def test_extract_pattern_opt(self): self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file1', size=1024 * 80) @@ -2889,7 +2924,7 @@ class TestBuildFilter: def test_basic(self): matcher = PatternMatcher() - matcher.add([parse_pattern('included')], True) + matcher.add([parse_pattern('included')], IECommand.Include) filter = Archiver.build_filter(matcher, self.peek_and_store_hardlink_masters, 0) assert filter(Item(path='included')) assert filter(Item(path='included/file')) diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 3938b722..f5db0992 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -557,12 +557,12 @@ def test_switch_patterns_style(): roots, patterns = [], [] load_pattern_file(pattern_file, roots, patterns) assert len(patterns) == 6 - assert isinstance(patterns[0].pattern, ShellPattern) - assert isinstance(patterns[1].pattern, FnmatchPattern) - assert isinstance(patterns[2].pattern, RegexPattern) - assert isinstance(patterns[3].pattern, RegexPattern) - assert isinstance(patterns[4].pattern, PathPrefixPattern) - assert isinstance(patterns[5].pattern, ShellPattern) + assert isinstance(patterns[0].val, ShellPattern) + assert isinstance(patterns[1].val, FnmatchPattern) + assert isinstance(patterns[2].val, RegexPattern) + assert isinstance(patterns[3].val, RegexPattern) + assert isinstance(patterns[4].val, PathPrefixPattern) + assert isinstance(patterns[5].val, ShellPattern) @pytest.mark.parametrize("lines", [ @@ -682,6 +682,10 @@ def test_pattern_matcher(): for i in ["", "foo", "bar"]: assert pm.match(i) is None + # add extra entries to aid in testing + for target in ["A", "B", "Empty", "FileNotFound"]: + pm.is_include_cmd[target] = target + pm.add([RegexPattern("^a")], "A") pm.add([RegexPattern("^b"), RegexPattern("^z")], "B") pm.add([RegexPattern("^$")], "Empty") From 5ddf7e7922357f59f61ba6bf08c6a7a742e4f286 Mon Sep 17 00:00:00 2001 From: rugk Date: Sat, 15 Apr 2017 12:38:58 +0200 Subject: [PATCH 0800/1387] Add --one-file-system note for root backup --- docs/faq.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index 6f7c39ed..f46d31d1 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -358,7 +358,8 @@ Can I backup my root partition (/) with Borg? Backing up your entire root partition works just fine, but remember to exclude directories that make no sense to backup, such as /dev, /proc, -/sys, /tmp and /run. +/sys, /tmp and /run, and to use --one-file-system if you only want to +backup the root partition (and not any mounted devices e.g.). If it crashes with a UnicodeError, what can I do? ------------------------------------------------- From 7b519e47697e2eedf9d01a77da162866d4d011ad Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 19 Apr 2017 11:31:40 +0200 Subject: [PATCH 0801/1387] platform.linux: get rid of "resource" module --- src/borg/platform/linux.pyx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/borg/platform/linux.pyx b/src/borg/platform/linux.pyx index e87983c7..6bd1bb4e 100644 --- a/src/borg/platform/linux.pyx +++ b/src/borg/platform/linux.pyx @@ -1,6 +1,5 @@ import os import re -import resource import stat import subprocess @@ -54,6 +53,10 @@ cdef extern from "linux/fs.h": cdef extern from "sys/ioctl.h": int ioctl(int fildes, int request, ...) +cdef extern from "unistd.h": + int _SC_PAGESIZE + long sysconf(int name) + cdef extern from "string.h": char *strerror(int errnum) @@ -219,7 +222,7 @@ cdef _sync_file_range(fd, offset, length, flags): raise OSError(errno.errno, os.strerror(errno.errno)) safe_fadvise(fd, offset, length, 'DONTNEED') -cdef unsigned PAGE_MASK = resource.getpagesize() - 1 +cdef unsigned PAGE_MASK = sysconf(_SC_PAGESIZE) - 1 class SyncFile(BaseSyncFile): From 697942cd01bd46adae6523c0338c7a6f00fe442a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 20 Apr 2017 19:08:33 +0200 Subject: [PATCH 0802/1387] be more clear that this is a "beyond repair" case, fixes #2427 --- src/borg/repository.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index b6340c4a..b5c2e193 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -254,7 +254,13 @@ class Repository: index_transaction_id = self.get_index_transaction_id() segments_transaction_id = self.io.get_segments_transaction_id() if index_transaction_id is not None and segments_transaction_id is None: - raise self.CheckNeeded(self.path) + # we have a transaction id from the index, but we did not find *any* + # commit in the segment files (thus no segments transaction id). + # this can happen if a lot of segment files are lost, e.g. due to a + # filesystem or hardware malfunction. it means we have no identifiable + # valid (committed) state of the repo which we could use. + msg = '%s" - although likely this is "beyond repair' % self.path # dirty hack + raise self.CheckNeeded(msg) # Attempt to automatically rebuild index if we crashed between commit # tag write and index save if index_transaction_id != segments_transaction_id: From 605f281b37db63ee238fb2aaf5c0563effce4619 Mon Sep 17 00:00:00 2001 From: Steve Groesz Date: Wed, 5 Apr 2017 20:29:04 -0500 Subject: [PATCH 0803/1387] Document repository file system requirements #2080 --- docs/installation.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index 26a200af..ef5762c7 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -20,6 +20,37 @@ There are different ways to install |project_name|: have the latest code or use revision control (each release is tagged). +.. _installation-requirements: + +Pre-Installation Considerations +------------------------------- + +Repository File System +~~~~~~~~~~~~~~~~~~~~~~ +:ref:data-structures-and-file-formats +- |project_name| stores data only 3 directory levels deep and uses short file and + directory names. +- |project_name| requires read and write permissions on the repository file system. +- |project_name| stores backup metadata and data into so-called segment files. The + target size of these files and also the count of these files per directory is set + in the :ref:config-file. +- |project_name| uses a generic and very portable mkdir-based `locking Date: Sun, 23 Apr 2017 05:52:44 -0500 Subject: [PATCH 0804/1387] FAQ to explain warning when running --repair (#2393) FAQ to explain warning when running --repair, fixes #2341 --- docs/faq.rst | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index f46d31d1..786d527b 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -347,7 +347,7 @@ How can I backup huge file(s) over a unstable connection? This is not a problem any more, see previous FAQ item. -How can I restore huge file(s) over a unstable connection? +How can I restore huge file(s) over an unstable connection? ---------------------------------------------------------- If you can not manage to extract the whole big file in one go, you can extract @@ -382,7 +382,7 @@ If you run into that, try this: .. _a_status_oddity: -I am seeing 'A' (added) status for a unchanged file!? +I am seeing 'A' (added) status for an unchanged file!? ----------------------------------------------------- The files cache is used to determine whether |project_name| already @@ -494,6 +494,28 @@ maybe open an issue in their issue tracker. Do not file an issue in the If you can reproduce the issue with the proven filesystem, please file an issue in the |project_name| issue tracker about that. + +Why does running 'borg check --repair' warn about data loss? +------------------------------------------------------------ + +Repair usually works for recovering data in a corrupted archive. However, +it's impossible to predict all modes of corruption. In some very rare +instances, such as malfunctioning storage hardware, additional repo +corruption may occur. If you can't afford to lose the repo, it's strongly +recommended that you perform repair on a copy of the repo. + +In other words, the warning is there to emphasize that |project_name|: + - Will perform automated routines that modify your backup repository + - Might not actually fix the problem you are experiencing + - Might, in very rare cases, further corrupt your repository + +In the case of malfunctioning hardware, such as a drive or USB hub +corrupting data when read or written, it's best to diagnose and fix the +cause of the initial corruption before attempting to repair the repo. If +the corruption is caused by a one time event such as a power outage, +running `borg check --repair` will fix most problems. + + Miscellaneous ############# From de76a6b8218bf0f20aa31e92ca92edab37483f34 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 8 Apr 2017 15:46:24 +0200 Subject: [PATCH 0805/1387] embrace y2038 issue to support 32bit platforms --- src/borg/helpers.py | 28 ++++++++++++++++++++---- src/borg/testsuite/archiver.py | 8 ++----- src/borg/testsuite/helpers.py | 40 +++++++++++++++++++++++----------- 3 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index a52ff26b..8bc9959e 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -905,10 +905,30 @@ def SortBySpec(text): # Not too rarely, we get crappy timestamps from the fs, that overflow some computations. -# As they are crap anyway, nothing is lost if we just clamp them to the max valid value. -# msgpack can only pack uint64. datetime is limited to year 9999. -MAX_NS = 18446744073000000000 # less than 2**64 - 1 ns. also less than y9999. -MAX_S = MAX_NS // 1000000000 +# As they are crap anyway (valid filesystem timestamps always refer to the past up to +# the present, but never to the future), nothing is lost if we just clamp them to the +# maximum value we can support. +# As long as people are using borg on 32bit platforms to access borg archives, we must +# keep this value True. But we can expect that we can stop supporting 32bit platforms +# well before coming close to the year 2038, so this will never be a practical problem. +SUPPORT_32BIT_PLATFORMS = True # set this to False before y2038. + +if SUPPORT_32BIT_PLATFORMS: + # second timestamps will fit into a signed int32 (platform time_t limit). + # nanosecond timestamps thus will naturally fit into a signed int64. + # subtract last 48h to avoid any issues that could be caused by tz calculations. + # this is in the year 2038, so it is also less than y9999 (which is a datetime internal limit). + # msgpack can pack up to uint64. + MAX_S = 2**31-1 - 48*3600 + MAX_NS = MAX_S * 1000000000 +else: + # nanosecond timestamps will fit into a signed int64. + # subtract last 48h to avoid any issues that could be caused by tz calculations. + # this is in the year 2262, so it is also less than y9999 (which is a datetime internal limit). + # round down to 1e9 multiple, so MAX_NS corresponds precisely to a integer MAX_S. + # msgpack can pack up to uint64. + MAX_NS = (2**63-1 - 48*3600*1000000000) // 1000000000 * 1000000000 + MAX_S = MAX_NS // 1000000000 def safe_s(ts): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 366900d3..c065feb1 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -38,6 +38,7 @@ from ..helpers import Manifest from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from ..helpers import bin_to_hex from ..helpers import IECommand +from ..helpers import MAX_S from ..item import Item from ..key import KeyfileKeyBase, RepoKey, KeyfileKey, Passphrase, TAMRequiredError from ..keymanager import RepoIdMismatch, NotABorgKeyFile @@ -293,12 +294,7 @@ class ArchiverTestCaseBase(BaseTestCase): """ # File self.create_regular_file('empty', size=0) - # next code line raises OverflowError on 32bit cpu (raspberry pi 2): - # 2600-01-01 > 2**64 ns - # os.utime('input/empty', (19880895600, 19880895600)) - # thus, we better test with something not that far in future: - # 2038-01-19 (1970 + 2^31 - 1 seconds) is the 32bit "deadline": - os.utime('input/empty', (2**31 - 1, 2**31 - 1)) + os.utime('input/empty', (MAX_S, MAX_S)) self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('flagfile', size=1024) # Directory diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index f5db0992..047e41c8 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -28,7 +28,7 @@ from ..helpers import parse_pattern, PatternMatcher from ..helpers import PathFullPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, RegexPattern from ..helpers import swidth_slice from ..helpers import chunkit -from ..helpers import safe_ns, safe_s +from ..helpers import safe_ns, safe_s, SUPPORT_32BIT_PLATFORMS from . import BaseTestCase, FakeInputs @@ -1250,15 +1250,29 @@ def test_swidth_slice_mixed_characters(): def test_safe_timestamps(): - # ns fit into uint64 - assert safe_ns(2 ** 64) < 2 ** 64 - assert safe_ns(-1) == 0 - # s are so that their ns conversion fits into uint64 - assert safe_s(2 ** 64) * 1000000000 < 2 ** 64 - assert safe_s(-1) == 0 - # datetime won't fall over its y10k problem - beyond_y10k = 2 ** 100 - with pytest.raises(OverflowError): - datetime.utcfromtimestamp(beyond_y10k) - assert datetime.utcfromtimestamp(safe_s(beyond_y10k)) > datetime(2500, 12, 31) - assert datetime.utcfromtimestamp(safe_ns(beyond_y10k) / 1000000000) > datetime(2500, 12, 31) + if SUPPORT_32BIT_PLATFORMS: + # ns fit into int64 + assert safe_ns(2 ** 64) <= 2 ** 63 - 1 + assert safe_ns(-1) == 0 + # s fit into int32 + assert safe_s(2 ** 64) <= 2 ** 31 - 1 + assert safe_s(-1) == 0 + # datetime won't fall over its y10k problem + beyond_y10k = 2 ** 100 + with pytest.raises(OverflowError): + datetime.utcfromtimestamp(beyond_y10k) + assert datetime.utcfromtimestamp(safe_s(beyond_y10k)) > datetime(2038, 1, 1) + assert datetime.utcfromtimestamp(safe_ns(beyond_y10k) / 1000000000) > datetime(2038, 1, 1) + else: + # ns fit into int64 + assert safe_ns(2 ** 64) <= 2 ** 63 - 1 + assert safe_ns(-1) == 0 + # s are so that their ns conversion fits into int64 + assert safe_s(2 ** 64) * 1000000000 <= 2 ** 63 - 1 + assert safe_s(-1) == 0 + # datetime won't fall over its y10k problem + beyond_y10k = 2 ** 100 + with pytest.raises(OverflowError): + datetime.utcfromtimestamp(beyond_y10k) + assert datetime.utcfromtimestamp(safe_s(beyond_y10k)) > datetime(2262, 1, 1) + assert datetime.utcfromtimestamp(safe_ns(beyond_y10k) / 1000000000) > datetime(2262, 1, 1) From 28b0700437e097635ceb9bad32c739376d63bf03 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 25 Apr 2017 15:48:16 +0200 Subject: [PATCH 0806/1387] verify_data: fix IntegrityError handling for defect chunks, fixes #2442 just getting data from the repo can already raise IntegrityErrors in LoggedIO, so we need to catch them also. see also the code a few lines above where this is done in the same way. --- src/borg/archive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 4dedc0f3..fc209b26 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1254,8 +1254,8 @@ class ArchiveChecker: # local repo (fs): as chunks.iteritems loop usually pumps a lot of data through, # a defect chunk is likely not in the fs cache any more and really gets re-read # from the underlying media. - encrypted_data = self.repository.get(defect_chunk) try: + encrypted_data = self.repository.get(defect_chunk) _chunk_id = None if defect_chunk == Manifest.MANIFEST_ID else defect_chunk self.key.decrypt(_chunk_id, encrypted_data) except IntegrityError: From ca6257dd4824a3367e129b722d0487716fff951e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 25 Apr 2017 22:21:58 +0200 Subject: [PATCH 0807/1387] clarify borg upgrade docs, fixes #2436 --- src/borg/archiver.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f2c26cf9..606aa3c0 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2842,7 +2842,20 @@ class Archiver: help='repository to prune') upgrade_epilog = process_epilog(""" - Upgrade an existing Borg repository. + Upgrade an existing, local Borg repository. + + When you do not need borg upgrade + +++++++++++++++++++++++++++++++++ + + Not every change requires that you run ``borg upgrade``. + + You do **not** need to run it when: + + - moving your repository to a different place + - upgrading to another point release (like 1.0.x to 1.0.y), + except when noted otherwise in the changelog + - upgrading from 1.0.x to 1.1.x, + except when noted otherwise in the changelog Borg 1.x.y upgrades +++++++++++++++++++ From f0188449c345d80924f043661e58b86a26dec815 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 25 Apr 2017 22:47:18 +0200 Subject: [PATCH 0808/1387] add hint about chunker params to borg upgrade docs, fixes #2421 --- src/borg/archiver.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 606aa3c0..70ba07f1 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2887,8 +2887,13 @@ class Archiver: Currently, only LOCAL repositories can be upgraded (issue #465). - It will change the magic strings in the repository's segments - to match the new Borg magic strings. The keyfiles found in + Please note that ``borg create`` (since 1.0.0) uses bigger chunks by + default than old borg or attic did, so the new chunks won't deduplicate + with the old chunks in the upgraded repository. + See ``--chunker-params`` option of ``borg create`` and ``borg recreate``. + + ``borg upgrade`` will change the magic strings in the repository's + segments to match the new Borg magic strings. The keyfiles found in $ATTIC_KEYS_DIR or ~/.attic/keys/ will also be converted and copied to $BORG_KEYS_DIR or ~/.config/borg/keys. From bf69b049e9dca6d66e5a30b7addf70aea86510e8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 25 Apr 2017 23:38:55 +0200 Subject: [PATCH 0809/1387] be clear about what buzhash is used for, fixes #2390 and want it is not used for (deduplication). also say already in the readme that we use a cryptohash for dedupe, so people don't worry. --- README.rst | 4 ++++ docs/internals/data-structures.rst | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/README.rst b/README.rst index 41765b80..ba7c735f 100644 --- a/README.rst +++ b/README.rst @@ -27,6 +27,10 @@ Main features of bytes stored: each file is split into a number of variable length chunks and only chunks that have never been seen before are added to the repository. + A chunk is considered duplicate if its id_hash value is identical. + A cryptographically strong hash or MAC function is used as id_hash, e.g. + (hmac-)sha256. + To deduplicate, all the chunks in the same repository are considered, no matter whether they come from different machines, from previous backups, from the same backup or even from the same single file. diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index a76f14e1..339338a3 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -69,6 +69,9 @@ Normally the keys are computed like this:: The id_hash function depends on the :ref:`encryption mode `. +As the id / key is used for deduplication, id_hash must be a cryptographically +strong hash or MAC. + Segments ~~~~~~~~ @@ -243,6 +246,11 @@ The |project_name| chunker uses a rolling hash computed by the Buzhash_ algorith It triggers (chunks) when the last HASH_MASK_BITS bits of the hash are zero, producing chunks of 2^HASH_MASK_BITS Bytes on average. +Buzhash is **only** used for cutting the chunks at places defined by the +content, the buzhash value is **not** used as the deduplication criteria (we +use a cryptographically strong hash/MAC over the chunk contents for this, the +id_hash). + ``borg create --chunker-params CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE`` can be used to tune the chunker parameters, the default is: From ba20d8d1310139c2e781544c25a77428c223cbd0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 26 Apr 2017 03:16:12 +0200 Subject: [PATCH 0810/1387] document borg init behaviour via append-only borg serve, fixes #2440 --- docs/usage.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index a4250aea..42bda553 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -677,6 +677,11 @@ in ``.ssh/authorized_keys`` :: command="borg serve --append-only ..." ssh-rsa command="borg serve ..." ssh-rsa +Please note that if you run ``borg init`` via a ``borg serve --append-only`` +server, the repository config will be created with a ``append_only=1`` entry. +This behaviour is subject to change in a later borg version. So, be aware of +it for now, but do not rely on it. + Example +++++++ From 5ffb66fdd520bba83a2cc63792d0ce0cc6022921 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 27 Apr 2017 01:34:53 +0200 Subject: [PATCH 0811/1387] upgrade FUSE for macOS to 3.5.8, fixes #2346 --- Vagrantfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 0a3e4af9..0f9ddc1e 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -67,9 +67,9 @@ def packages_darwin sudo softwareupdate --ignore iTunes sudo softwareupdate --install --all # get osxfuse 3.x release code from github: - curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.5.4/osxfuse-3.5.4.dmg >osxfuse.dmg + curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.5.8/osxfuse-3.5.8.dmg >osxfuse.dmg MOUNTDIR=$(echo `hdiutil mount osxfuse.dmg | tail -1 | awk '{$1="" ; print $0}'` | xargs -0 echo) \ - && sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for macOS 3.5.4.pkg" -target / + && sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for macOS 3.5.8.pkg" -target / sudo chown -R vagrant /usr/local # brew must be able to create stuff here ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew update From ff4120184c5a586c3dd6efc54e8fde4b07054646 Mon Sep 17 00:00:00 2001 From: Steve Groesz Date: Wed, 26 Apr 2017 22:25:15 -0500 Subject: [PATCH 0812/1387] Fix formatting in pre-installation section Some documentation was improperly formatted with #2392. This corrects those errors. --- docs/installation.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index ef5762c7..1cb71556 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -27,14 +27,14 @@ Pre-Installation Considerations Repository File System ~~~~~~~~~~~~~~~~~~~~~~ -:ref:data-structures-and-file-formats +:ref:`data-structures-and-file-formats` - |project_name| stores data only 3 directory levels deep and uses short file and directory names. - |project_name| requires read and write permissions on the repository file system. - |project_name| stores backup metadata and data into so-called segment files. The target size of these files and also the count of these files per directory is set - in the :ref:config-file. -- |project_name| uses a generic and very portable mkdir-based `locking Date: Fri, 28 Apr 2017 10:39:32 +0200 Subject: [PATCH 0813/1387] docs: faq: fix title underlines --- docs/faq.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 786d527b..4ff646dd 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -348,7 +348,7 @@ How can I backup huge file(s) over a unstable connection? This is not a problem any more, see previous FAQ item. How can I restore huge file(s) over an unstable connection? ----------------------------------------------------------- +----------------------------------------------------------- If you can not manage to extract the whole big file in one go, you can extract all the part files (see above) and manually concatenate them together. @@ -383,7 +383,7 @@ If you run into that, try this: .. _a_status_oddity: I am seeing 'A' (added) status for an unchanged file!? ------------------------------------------------------ +------------------------------------------------------ The files cache is used to determine whether |project_name| already "knows" / has backed up a file and if so, to skip the file from From e036ebc340fac8b4cac21f17fe9cbbd98619eee0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 28 Apr 2017 10:40:21 +0200 Subject: [PATCH 0814/1387] docs: installation: pre-install considerations, fix crossrefs posix_fadvise -> only used when available; not part of the filesystem. sync -> fsync --- docs/installation.rst | 9 +++++---- docs/internals/data-structures.rst | 4 ++++ docs/usage_general.rst.inc | 2 ++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 1cb71556..3ae5bc8c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -27,7 +27,7 @@ Pre-Installation Considerations Repository File System ~~~~~~~~~~~~~~~~~~~~~~ -:ref:`data-structures-and-file-formats` + - |project_name| stores data only 3 directory levels deep and uses short file and directory names. - |project_name| requires read and write permissions on the repository file system. @@ -42,13 +42,14 @@ Repository File System - A journaling file system is strongly recommended. More information can be found in :ref:`file-systems`. - |project_name| requires the following file system operations: + - create, open, read, write, seek, close, rename, delete - link - when upgrading an Attic repo in-place - listdir, stat - - posix_fadvise - to not flood the operating system's cache - - sync on files and directories to ensure data is written onto storage media + - fsync on files and directories to ensure data is written onto storage media + (some file systems do not support fsync on directories, which Borg accomodates for) -:ref:`data-structures-and-file-formats` contains additional information about how |project_name| +:ref:`data-structures` contains additional information about how |project_name| manages data. .. _locking: https://en.wikipedia.org/wiki/File_locking#Lock_files diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index a76f14e1..cc7f0f1b 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -1,6 +1,8 @@ .. include:: ../global.rst.inc .. highlight:: none +.. _data-structures: + Data structures and file formats ================================ @@ -40,6 +42,8 @@ called segments_. Each segment is a series of log entries. The segment number to entry relative to its segment start establishes an ordering of the log entries. This is the "definition" of time for the purposes of the log. +.. _config-file: + Config file ~~~~~~~~~~~ diff --git a/docs/usage_general.rst.inc b/docs/usage_general.rst.inc index addadc5b..2b8c630e 100644 --- a/docs/usage_general.rst.inc +++ b/docs/usage_general.rst.inc @@ -207,6 +207,8 @@ Please note: .. _INI: https://docs.python.org/3.4/library/logging.config.html#configuration-file-format +.. _file-systems: + File systems ~~~~~~~~~~~~ From d949d6bc7cfa10099fe53d5ea20259bebf08225d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 29 Apr 2017 03:00:59 +0200 Subject: [PATCH 0815/1387] fix invalid param issue in benchmarks fixes CID1431887 --- src/borg/testsuite/benchmark.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/borg/testsuite/benchmark.py b/src/borg/testsuite/benchmark.py index b3400c2f..dcc71b01 100644 --- a/src/borg/testsuite/benchmark.py +++ b/src/borg/testsuite/benchmark.py @@ -40,9 +40,11 @@ def testdata(request, tmpdir_factory): # do not use a binary zero (\0) to avoid sparse detection def data(size): return b'0' * size - if data_type == 'random': + elif data_type == 'random': def data(size): return os.urandom(size) + else: + raise ValueError("data_type must be 'random' or 'zeros'.") for i in range(count): with open(str(p.join(str(i))), "wb") as f: f.write(data(size)) From a5ccaf62f8b6abe0c12b3a02bd4a72363b01c05f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 30 Apr 2017 00:39:44 +0200 Subject: [PATCH 0816/1387] update CHANGES --- docs/changes.rst | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index e9dc0c20..5b8bd892 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -134,18 +134,22 @@ Version 1.1.0b5 (not released) Compatibility notes: - BORG_HOSTNAME_IS_UNIQUE is now on by default. -- remove --compression-from feature +- removed --compression-from feature - recreate: add --recompress flag, unify --always-recompress and --recompress Fixes: -- catch exception for os.link when hardlinks are not supported -- borg rename / recreate: expand placeholders, fixes #2386 -- generic support for hardlinks (files, devices, FIFOs) -- extract: also create parent dir for device files, if needed. +- catch exception for os.link when hardlinks are not supported, #2405 +- borg rename / recreate: expand placeholders, #2386 +- generic support for hardlinks (files, devices, FIFOs), #2324 +- extract: also create parent dir for device files, if needed, #2358 - extract: if a hardlink master is not in the to-be-extracted subset, - the "x" status was not displayed for it. + the "x" status was not displayed for it, #2351 +- embrace y2038 issue to support 32bit platforms: clamp timestamps to int32, + #2347 +- verify_data: fix IntegrityError handling for defect chunks, #2442 +- allow excluding parent and including child, #2314 Other changes: @@ -154,6 +158,9 @@ Other changes: with --compression defaults. - placeholders: deny access to internals and other unspecified stuff - clearer error message for unrecognized placeholder +- more clear exception if borg check does not help, #2427 +- vagrant: upgrade FUSE for macOS to 3.5.8, #2346 +- linux binary builds: get rid of glibc 2.13 dependency, #2430 - docs: - placeholders: document escaping @@ -162,10 +169,19 @@ Other changes: - more docs about compression - LICENSE: use canonical formulation ("copyright holders and contributors" instead of "author") + - document borg init behaviour via append-only borg serve, #2440 + - be clear about what buzhash is used for, #2390 + - add hint about chunker params, #2421 + - clarify borg upgrade docs, #2436 + - FAQ to explain warning when running borg check --repair, #2341 + - repository file system requirements, #2080 + - pre-install considerations + - misc. formatting / crossref fixes - tests: - enhance travis setuptools_scm situation - add extra test for the hashindex + - fix invalid param issue in benchmarks These belong to 1.1.0b4 release, but did not make it into changelog by then: From 78c455c31f8d4d845970153cbe36cb25d28deae5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 30 Apr 2017 01:29:17 +0200 Subject: [PATCH 0817/1387] update CHANGES (release date) --- docs/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 5b8bd892..ec226a3a 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -128,8 +128,8 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= -Version 1.1.0b5 (not released) ------------------------------- +Version 1.1.0b5 (2017-04-30) +---------------------------- Compatibility notes: From 3c279dbf78c5703ab1e43a71b43ec2727579d507 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 30 Apr 2017 01:31:20 +0200 Subject: [PATCH 0818/1387] ran build_usage --- docs/usage/create.rst.inc | 2 -- docs/usage/help.rst.inc | 36 +++++------------------------------- docs/usage/recreate.rst.inc | 10 ++++------ docs/usage/upgrade.rst.inc | 24 +++++++++++++++++++++--- 4 files changed, 30 insertions(+), 42 deletions(-) diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 97e05306..d3aa2be1 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -72,8 +72,6 @@ Archive options | specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: 19,23,21,4095 ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the "borg help compression" command for details. - ``--compression-from COMPRESSIONCONFIG`` - | read compression patterns from COMPRESSIONCONFIG, see the output of the "borg help compression" command for details. Description ~~~~~~~~~~~ diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index dc2072d6..8b3f8ded 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -196,6 +196,10 @@ placeholders: The version of borg, only major, minor and patch version, e.g.: 1.0.8 +If literal curly braces need to be used, double them for escaping:: + + borg create /path/to/repo::{{literal_text}} + Examples:: borg create /path/to/repo::{hostname}-{user}-{utcnow} ... @@ -245,43 +249,13 @@ auto,C[,L] For compressible data, it uses the given C[,L] compression - with C[,L] being any valid compression specifier. -The decision about which compression to use is done by borg like this: - -1. find a compression specifier (per file): - match the path/filename against all patterns in all --compression-from - files (if any). If a pattern matches, use the compression spec given for - that pattern. If no pattern matches (and also if you do not give any - --compression-from option), default to the compression spec given by - --compression. See docs/misc/compression.conf for an example config. - -2. if the found compression spec is not "auto", the decision is taken: - use the found compression spec. - -3. if the found compression spec is "auto", test compressibility of each - chunk using lz4. - If it is compressible, use the C,[L] compression spec given within the - "auto" specifier. If it is not compressible, use no compression. - Examples:: borg create --compression lz4 REPO::ARCHIVE data borg create --compression zlib REPO::ARCHIVE data borg create --compression zlib,1 REPO::ARCHIVE data borg create --compression auto,lzma,6 REPO::ARCHIVE data - borg create --compression-from compression.conf --compression auto,lzma ... - -compression.conf has entries like:: - - # example config file for --compression-from option - # - # Format of non-comment / non-empty lines: - # : - # compression-spec is same format as for --compression option - # path/filename pattern is same format as for --exclude option - none:*.gz - none:*.zip - none:*.mp3 - none:*.ogg + borg create --compression auto,lzma ... General remarks: diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index 0e37696b..a84c9faf 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -56,10 +56,8 @@ Archive options | manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). alternatively, give a reference file/directory. ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the "borg help compression" command for details. - ``--always-recompress`` - | always recompress chunks, don't skip chunks already compressed with the same algorithm. - ``--compression-from COMPRESSIONCONFIG`` - | read compression patterns from COMPRESSIONCONFIG, see the output of the "borg help compression" command for details. + ``--recompress`` + | recompress data chunks according to --compression if "if-different". When "always", chunks that are already compressed that way are not skipped, but compressed again. Only the algorithm is considered for "if-different", not the compression level (if any). ``--chunker-params PARAMS`` | specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or "default" to use the current defaults. default: 19,23,21,4095 @@ -75,9 +73,9 @@ have the exact same semantics as in "borg create". If PATHs are specified the resulting archive will only contain files from these PATHs. Note that all paths in an archive are relative, therefore absolute patterns/paths -will *not* match (--exclude, --exclude-from, --compression-from, PATHs). +will *not* match (--exclude, --exclude-from, PATHs). ---compression: all chunks seen will be stored using the given method. +--recompress allows to change the compression of existing data in archives. Due to how Borg stores compressed size information this might display incorrect information for archives that were not recreated at the same time. There is no risk of data loss by this. diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index 8fd443c8..4048ae5a 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -33,7 +33,20 @@ optional arguments Description ~~~~~~~~~~~ -Upgrade an existing Borg repository. +Upgrade an existing, local Borg repository. + +When you do not need borg upgrade ++++++++++++++++++++++++++++++++++ + +Not every change requires that you run ``borg upgrade``. + +You do **not** need to run it when: + +- moving your repository to a different place +- upgrading to another point release (like 1.0.x to 1.0.y), + except when noted otherwise in the changelog +- upgrading from 1.0.x to 1.1.x, + except when noted otherwise in the changelog Borg 1.x.y upgrades +++++++++++++++++++ @@ -65,8 +78,13 @@ helps with converting Borg 0.xx to 1.0. Currently, only LOCAL repositories can be upgraded (issue #465). -It will change the magic strings in the repository's segments -to match the new Borg magic strings. The keyfiles found in +Please note that ``borg create`` (since 1.0.0) uses bigger chunks by +default than old borg or attic did, so the new chunks won't deduplicate +with the old chunks in the upgraded repository. +See ``--chunker-params`` option of ``borg create`` and ``borg recreate``. + +``borg upgrade`` will change the magic strings in the repository's +segments to match the new Borg magic strings. The keyfiles found in $ATTIC_KEYS_DIR or ~/.attic/keys/ will also be converted and copied to $BORG_KEYS_DIR or ~/.config/borg/keys. From b00700ae9d2f4ff1d33d2f07e8c4df53668c7911 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 30 Apr 2017 01:32:26 +0200 Subject: [PATCH 0819/1387] ran build_man --- docs/man/borg-break-lock.1 | 2 +- docs/man/borg-change-passphrase.1 | 2 +- docs/man/borg-check.1 | 2 +- docs/man/borg-common.1 | 2 +- docs/man/borg-compression.1 | 44 ++------------------------ docs/man/borg-create.1 | 5 +-- docs/man/borg-delete.1 | 2 +- docs/man/borg-diff.1 | 2 +- docs/man/borg-extract.1 | 2 +- docs/man/borg-info.1 | 2 +- docs/man/borg-init.1 | 2 +- docs/man/borg-key-change-passphrase.1 | 2 +- docs/man/borg-key-export.1 | 2 +- docs/man/borg-key-import.1 | 2 +- docs/man/borg-key-migrate-to-repokey.1 | 2 +- docs/man/borg-key.1 | 2 +- docs/man/borg-list.1 | 2 +- docs/man/borg-mount.1 | 2 +- docs/man/borg-patterns.1 | 2 +- docs/man/borg-placeholders.1 | 14 +++++++- docs/man/borg-prune.1 | 2 +- docs/man/borg-recreate.1 | 15 ++++----- docs/man/borg-rename.1 | 2 +- docs/man/borg-serve.1 | 12 ++++++- docs/man/borg-umount.1 | 2 +- docs/man/borg-upgrade.1 | 28 +++++++++++++--- docs/man/borg-with-lock.1 | 2 +- docs/man/borg.1 | 7 ++-- 28 files changed, 82 insertions(+), 85 deletions(-) diff --git a/docs/man/borg-break-lock.1 b/docs/man/borg-break-lock.1 index 6f9f73e0..c83b2d6d 100644 --- a/docs/man/borg-break-lock.1 +++ b/docs/man/borg-break-lock.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-BREAK-LOCK 1 "2017-03-26" "" "borg backup tool" +.TH BORG-BREAK-LOCK 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-break-lock \- Break the repository lock (e.g. in case it was left by a dead borg. . diff --git a/docs/man/borg-change-passphrase.1 b/docs/man/borg-change-passphrase.1 index 2c8f90c5..1a73f339 100644 --- a/docs/man/borg-change-passphrase.1 +++ b/docs/man/borg-change-passphrase.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CHANGE-PASSPHRASE 1 "2017-03-26" "" "borg backup tool" +.TH BORG-CHANGE-PASSPHRASE 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-change-passphrase \- Change repository key file passphrase . diff --git a/docs/man/borg-check.1 b/docs/man/borg-check.1 index 3545a4d6..ea4ef558 100644 --- a/docs/man/borg-check.1 +++ b/docs/man/borg-check.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CHECK 1 "2017-03-26" "" "borg backup tool" +.TH BORG-CHECK 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-check \- Check repository consistency . diff --git a/docs/man/borg-common.1 b/docs/man/borg-common.1 index a10838f4..3ca2d1f9 100644 --- a/docs/man/borg-common.1 +++ b/docs/man/borg-common.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-COMMON 1 "2017-03-26" "" "borg backup tool" +.TH BORG-COMMON 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-common \- Common options of Borg commands . diff --git a/docs/man/borg-compression.1 b/docs/man/borg-compression.1 index e0a98ed1..da3c8487 100644 --- a/docs/man/borg-compression.1 +++ b/docs/man/borg-compression.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-COMPRESSION 1 "2017-03-26" "" "borg backup tool" +.TH BORG-COMPRESSION 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-compression \- Details regarding compression . @@ -84,25 +84,6 @@ being any valid compression specifier. .UNINDENT .UNINDENT .sp -The decision about which compression to use is done by borg like this: -.INDENT 0.0 -.IP 1. 3 -find a compression specifier (per file): -match the path/filename against all patterns in all \-\-compression\-from -files (if any). If a pattern matches, use the compression spec given for -that pattern. If no pattern matches (and also if you do not give any -\-\-compression\-from option), default to the compression spec given by -\-\-compression. See docs/misc/compression.conf for an example config. -.IP 2. 3 -if the found compression spec is not "auto", the decision is taken: -use the found compression spec. -.IP 3. 3 -if the found compression spec is "auto", test compressibility of each -chunk using lz4. -If it is compressible, use the C,[L] compression spec given within the -"auto" specifier. If it is not compressible, use no compression. -.UNINDENT -.sp Examples: .INDENT 0.0 .INDENT 3.5 @@ -113,28 +94,7 @@ borg create \-\-compression lz4 REPO::ARCHIVE data borg create \-\-compression zlib REPO::ARCHIVE data borg create \-\-compression zlib,1 REPO::ARCHIVE data borg create \-\-compression auto,lzma,6 REPO::ARCHIVE data -borg create \-\-compression\-from compression.conf \-\-compression auto,lzma ... -.ft P -.fi -.UNINDENT -.UNINDENT -.sp -compression.conf has entries like: -.INDENT 0.0 -.INDENT 3.5 -.sp -.nf -.ft C -# example config file for \-\-compression\-from option -# -# Format of non\-comment / non\-empty lines: -# : -# compression\-spec is same format as for \-\-compression option -# path/filename pattern is same format as for \-\-exclude option -none:*.gz -none:*.zip -none:*.mp3 -none:*.ogg +borg create \-\-compression auto,lzma ... .ft P .fi .UNINDENT diff --git a/docs/man/borg-create.1 b/docs/man/borg-create.1 index c0a7ab96..8196befa 100644 --- a/docs/man/borg-create.1 +++ b/docs/man/borg-create.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CREATE 1 "2017-03-26" "" "borg backup tool" +.TH BORG-CREATE 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-create \- Create new archive . @@ -160,9 +160,6 @@ specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HA .TP .BI \-C \ COMPRESSION\fP,\fB \ \-\-compression \ COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. -.TP -.BI \-\-compression\-from \ COMPRESSIONCONFIG -read compression patterns from COMPRESSIONCONFIG, see the output of the "borg help compression" command for details. .UNINDENT .SH EXAMPLES .INDENT 0.0 diff --git a/docs/man/borg-delete.1 b/docs/man/borg-delete.1 index 01e2167d..c911ec82 100644 --- a/docs/man/borg-delete.1 +++ b/docs/man/borg-delete.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-DELETE 1 "2017-03-26" "" "borg backup tool" +.TH BORG-DELETE 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-delete \- Delete an existing repository or archives . diff --git a/docs/man/borg-diff.1 b/docs/man/borg-diff.1 index dd0ec88c..c01f70f4 100644 --- a/docs/man/borg-diff.1 +++ b/docs/man/borg-diff.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-DIFF 1 "2017-03-26" "" "borg backup tool" +.TH BORG-DIFF 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-diff \- Diff contents of two archives . diff --git a/docs/man/borg-extract.1 b/docs/man/borg-extract.1 index 089635c8..9e8d113d 100644 --- a/docs/man/borg-extract.1 +++ b/docs/man/borg-extract.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-EXTRACT 1 "2017-03-26" "" "borg backup tool" +.TH BORG-EXTRACT 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-extract \- Extract archive contents . diff --git a/docs/man/borg-info.1 b/docs/man/borg-info.1 index b75e44ff..cc165d6d 100644 --- a/docs/man/borg-info.1 +++ b/docs/man/borg-info.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-INFO 1 "2017-03-26" "" "borg backup tool" +.TH BORG-INFO 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-info \- Show archive details such as disk space used . diff --git a/docs/man/borg-init.1 b/docs/man/borg-init.1 index 2b489ff9..c5eb14dc 100644 --- a/docs/man/borg-init.1 +++ b/docs/man/borg-init.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-INIT 1 "2017-03-26" "" "borg backup tool" +.TH BORG-INIT 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-init \- Initialize an empty repository . diff --git a/docs/man/borg-key-change-passphrase.1 b/docs/man/borg-key-change-passphrase.1 index 7b1a71fa..6903cf4a 100644 --- a/docs/man/borg-key-change-passphrase.1 +++ b/docs/man/borg-key-change-passphrase.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-CHANGE-PASSPHRASE 1 "2017-03-26" "" "borg backup tool" +.TH BORG-KEY-CHANGE-PASSPHRASE 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-key-change-passphrase \- Change repository key file passphrase . diff --git a/docs/man/borg-key-export.1 b/docs/man/borg-key-export.1 index 8cfe1505..6210505f 100644 --- a/docs/man/borg-key-export.1 +++ b/docs/man/borg-key-export.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-EXPORT 1 "2017-03-26" "" "borg backup tool" +.TH BORG-KEY-EXPORT 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-key-export \- Export the repository key for backup . diff --git a/docs/man/borg-key-import.1 b/docs/man/borg-key-import.1 index 0a5a3245..a6c0abef 100644 --- a/docs/man/borg-key-import.1 +++ b/docs/man/borg-key-import.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-IMPORT 1 "2017-03-26" "" "borg backup tool" +.TH BORG-KEY-IMPORT 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-key-import \- Import the repository key from backup . diff --git a/docs/man/borg-key-migrate-to-repokey.1 b/docs/man/borg-key-migrate-to-repokey.1 index efda66e1..ea3eed03 100644 --- a/docs/man/borg-key-migrate-to-repokey.1 +++ b/docs/man/borg-key-migrate-to-repokey.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-MIGRATE-TO-REPOKEY 1 "2017-03-26" "" "borg backup tool" +.TH BORG-KEY-MIGRATE-TO-REPOKEY 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-key-migrate-to-repokey \- Migrate passphrase -> repokey . diff --git a/docs/man/borg-key.1 b/docs/man/borg-key.1 index e0c878d6..b05452e2 100644 --- a/docs/man/borg-key.1 +++ b/docs/man/borg-key.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY 1 "2017-03-26" "" "borg backup tool" +.TH BORG-KEY 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-key \- Manage a keyfile or repokey of a repository . diff --git a/docs/man/borg-list.1 b/docs/man/borg-list.1 index be02614e..de919db3 100644 --- a/docs/man/borg-list.1 +++ b/docs/man/borg-list.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-LIST 1 "2017-03-26" "" "borg backup tool" +.TH BORG-LIST 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-list \- List archive or repository contents . diff --git a/docs/man/borg-mount.1 b/docs/man/borg-mount.1 index cb8ad26b..89a3ee24 100644 --- a/docs/man/borg-mount.1 +++ b/docs/man/borg-mount.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-MOUNT 1 "2017-03-26" "" "borg backup tool" +.TH BORG-MOUNT 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-mount \- Mount archive or an entire repository as a FUSE filesystem . diff --git a/docs/man/borg-patterns.1 b/docs/man/borg-patterns.1 index c250a4b9..e2d12055 100644 --- a/docs/man/borg-patterns.1 +++ b/docs/man/borg-patterns.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PATTERNS 1 "2017-03-26" "" "borg backup tool" +.TH BORG-PATTERNS 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-patterns \- Details regarding patterns . diff --git a/docs/man/borg-placeholders.1 b/docs/man/borg-placeholders.1 index aa62bb9b..c906f8dc 100644 --- a/docs/man/borg-placeholders.1 +++ b/docs/man/borg-placeholders.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PLACEHOLDERS 1 "2017-03-26" "" "borg backup tool" +.TH BORG-PLACEHOLDERS 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-placeholders \- Details regarding placeholders . @@ -107,6 +107,18 @@ The version of borg, only major, minor and patch version, e.g.: 1.0.8 .UNINDENT .UNINDENT .sp +If literal curly braces need to be used, double them for escaping: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +borg create /path/to/repo::{{literal_text}} +.ft P +.fi +.UNINDENT +.UNINDENT +.sp Examples: .INDENT 0.0 .INDENT 3.5 diff --git a/docs/man/borg-prune.1 b/docs/man/borg-prune.1 index 13541544..55031228 100644 --- a/docs/man/borg-prune.1 +++ b/docs/man/borg-prune.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PRUNE 1 "2017-03-26" "" "borg backup tool" +.TH BORG-PRUNE 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-prune \- Prune repository archives according to specified rules . diff --git a/docs/man/borg-recreate.1 b/docs/man/borg-recreate.1 index c7e30420..8f2bf554 100644 --- a/docs/man/borg-recreate.1 +++ b/docs/man/borg-recreate.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-RECREATE 1 "2017-03-26" "" "borg backup tool" +.TH BORG-RECREATE 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-recreate \- Re-create archives . @@ -44,9 +44,9 @@ have the exact same semantics as in "borg create". If PATHs are specified the resulting archive will only contain files from these PATHs. .sp Note that all paths in an archive are relative, therefore absolute patterns/paths -will \fInot\fP match (\-\-exclude, \-\-exclude\-from, \-\-compression\-from, PATHs). +will \fInot\fP match (\-\-exclude, \-\-exclude\-from, PATHs). .sp -\-\-compression: all chunks seen will be stored using the given method. +\-\-recompress allows to change the compression of existing data in archives. Due to how Borg stores compressed size information this might display incorrect information for archives that were not recreated at the same time. There is no risk of data loss by this. @@ -143,11 +143,8 @@ manually specify the archive creation date/time (UTC, yyyy\-mm\-ddThh:mm:ss form .BI \-C \ COMPRESSION\fP,\fB \ \-\-compression \ COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. .TP -.B \-\-always\-recompress -always recompress chunks, don\(aqt skip chunks already compressed with the same algorithm. -.TP -.BI \-\-compression\-from \ COMPRESSIONCONFIG -read compression patterns from COMPRESSIONCONFIG, see the output of the "borg help compression" command for details. +.B \-\-recompress +recompress data chunks according to \-\-compression if "if\-different". When "always", chunks that are already compressed that way are not skipped, but compressed again. Only the algorithm is considered for "if\-different", not the compression level (if any). .TP .BI \-\-chunker\-params \ PARAMS specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or "default" to use the current defaults. default: 19,23,21,4095 @@ -166,7 +163,7 @@ $ borg recreate /mnt/backup \-\-chunker\-params default \-\-progress $ borg create /mnt/backup::archive /some/files \-\-compression lz4 # Then compress it \- this might take longer, but the backup has already completed, so no inconsistencies # from a long\-running backup job. -$ borg recreate /mnt/backup::archive \-\-compression zlib,9 +$ borg recreate /mnt/backup::archive \-\-recompress \-\-compression zlib,9 # Remove unwanted files from all archives in a repository $ borg recreate /mnt/backup \-e /home/icke/Pictures/drunk_photos diff --git a/docs/man/borg-rename.1 b/docs/man/borg-rename.1 index 764ec91e..6a85b8d1 100644 --- a/docs/man/borg-rename.1 +++ b/docs/man/borg-rename.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-RENAME 1 "2017-03-26" "" "borg backup tool" +.TH BORG-RENAME 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-rename \- Rename an existing archive . diff --git a/docs/man/borg-serve.1 b/docs/man/borg-serve.1 index d6a6fbcd..90957e8b 100644 --- a/docs/man/borg-serve.1 +++ b/docs/man/borg-serve.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-SERVE 1 "2017-03-26" "" "borg backup tool" +.TH BORG-SERVE 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-serve \- Start in server mode. This command is usually not used manually. . @@ -53,10 +53,16 @@ only allow appending to repository segment files borg serve has special support for ssh forced commands (see \fBauthorized_keys\fP example below): it will detect that you use such a forced command and extract the value of the \fB\-\-restrict\-to\-path\fP option(s). +.sp It will then parse the original command that came from the client, makes sure that it is also \fBborg serve\fP and enforce path restriction(s) as given by the forced command. That way, other options given by the client (like \fB\-\-info\fP or \fB\-\-umask\fP) are preserved (and are not fixed by the forced command). +.sp +Environment variables (such as BORG_HOSTNAME_IS_UNIQUE) contained in the original +command sent by the client are \fInot\fP interpreted, but ignored. If BORG_XXX environment +variables should be set on the \fBborg serve\fP side, then these must be set in system\-specific +locations like \fB/etc/environment\fP or in the forced command itself (example below). .INDENT 0.0 .INDENT 3.5 .sp @@ -67,6 +73,10 @@ forced command. That way, other options given by the client (like \fB\-\-info\fP # This will help to secure an automated remote backup system. $ cat ~/.ssh/authorized_keys command="borg serve \-\-restrict\-to\-path /path/to/repo",no\-pty,no\-agent\-forwarding,no\-port\-forwarding,no\-X11\-forwarding,no\-user\-rc ssh\-rsa AAAAB3[...] + +# Set a BORG_XXX environment variable on the "borg serve" side +$ cat ~/.ssh/authorized_keys +command="export BORG_XXX=value; borg serve [...]",restrict ssh\-rsa [...] .ft P .fi .UNINDENT diff --git a/docs/man/borg-umount.1 b/docs/man/borg-umount.1 index c8cea6f4..98be9f53 100644 --- a/docs/man/borg-umount.1 +++ b/docs/man/borg-umount.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-UMOUNT 1 "2017-03-26" "" "borg backup tool" +.TH BORG-UMOUNT 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-umount \- un-mount the FUSE filesystem . diff --git a/docs/man/borg-upgrade.1 b/docs/man/borg-upgrade.1 index c8111a4e..b91f693d 100644 --- a/docs/man/borg-upgrade.1 +++ b/docs/man/borg-upgrade.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-UPGRADE 1 "2017-03-26" "" "borg backup tool" +.TH BORG-UPGRADE 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-upgrade \- upgrade a repository from a previous version . @@ -35,7 +35,22 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] borg upgrade REPOSITORY .SH DESCRIPTION .sp -Upgrade an existing Borg repository. +Upgrade an existing, local Borg repository. +.SS When you do not need borg upgrade +.sp +Not every change requires that you run \fBborg upgrade\fP\&. +.sp +You do \fBnot\fP need to run it when: +.INDENT 0.0 +.IP \(bu 2 +moving your repository to a different place +.IP \(bu 2 +upgrading to another point release (like 1.0.x to 1.0.y), +except when noted otherwise in the changelog +.IP \(bu 2 +upgrading from 1.0.x to 1.1.x, +except when noted otherwise in the changelog +.UNINDENT .SS Borg 1.x.y upgrades .sp Use \fBborg upgrade \-\-tam REPO\fP to require manifest authentication @@ -63,8 +78,13 @@ helps with converting Borg 0.xx to 1.0. .sp Currently, only LOCAL repositories can be upgraded (issue #465). .sp -It will change the magic strings in the repository\(aqs segments -to match the new Borg magic strings. The keyfiles found in +Please note that \fBborg create\fP (since 1.0.0) uses bigger chunks by +default than old borg or attic did, so the new chunks won\(aqt deduplicate +with the old chunks in the upgraded repository. +See \fB\-\-chunker\-params\fP option of \fBborg create\fP and \fBborg recreate\fP\&. +.sp +\fBborg upgrade\fP will change the magic strings in the repository\(aqs +segments to match the new Borg magic strings. The keyfiles found in $ATTIC_KEYS_DIR or ~/.attic/keys/ will also be converted and copied to $BORG_KEYS_DIR or ~/.config/borg/keys. .sp diff --git a/docs/man/borg-with-lock.1 b/docs/man/borg-with-lock.1 index 7ed9d53e..6fe30706 100644 --- a/docs/man/borg-with-lock.1 +++ b/docs/man/borg-with-lock.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-WITH-LOCK 1 "2017-03-26" "" "borg backup tool" +.TH BORG-WITH-LOCK 1 "2017-04-29" "" "borg backup tool" .SH NAME borg-with-lock \- run a user specified command with the repository lock held . diff --git a/docs/man/borg.1 b/docs/man/borg.1 index 7dffe320..8e924745 100644 --- a/docs/man/borg.1 +++ b/docs/man/borg.1 @@ -336,9 +336,10 @@ Main usecase for this is to fully automate \fBborg change\-passphrase\fP\&. .B BORG_DISPLAY_PASSPHRASE When set, use the value to answer the "display the passphrase for verification" question when defining a new passphrase for encrypted repositories. .TP -.B BORG_HOSTNAME_IS_UNIQUE=yes -Use this to assert that your hostname is unique. -Borg will then automatically remove locks that it could determine to be stale. +.B BORG_HOSTNAME_IS_UNIQUE=no +Borg assumes that it can derive a unique hostname / identity (see \fBborg debug info\fP). +If this is not the case or you do not want Borg to automatically remove stale locks, +set this to \fIno\fP\&. .TP .B BORG_LOGGING_CONF When set, use the given filename as \fI\%INI\fP\-style logging configuration. From 580496b59263ced0ad496c543752f417b411216c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 1 May 2017 16:58:29 +0200 Subject: [PATCH 0820/1387] create patterns module --- src/borg/archive.py | 2 +- src/borg/archiver.py | 4 +- src/borg/helpers.py | 387 +-------------------------- src/borg/patterns.py | 392 +++++++++++++++++++++++++++ src/borg/testsuite/archiver.py | 4 +- src/borg/testsuite/helpers.py | 460 -------------------------------- src/borg/testsuite/patterns.py | 467 +++++++++++++++++++++++++++++++++ 7 files changed, 865 insertions(+), 851 deletions(-) create mode 100644 src/borg/patterns.py create mode 100644 src/borg/testsuite/patterns.py diff --git a/src/borg/archive.py b/src/borg/archive.py index fc209b26..0a0bd9e5 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -36,7 +36,7 @@ from .helpers import StableDict from .helpers import bin_to_hex from .helpers import safe_ns from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi -from .helpers import PathPrefixPattern, FnmatchPattern, IECommand +from .patterns import PathPrefixPattern, FnmatchPattern, IECommand from .item import Item, ArchiveItem from .key import key_factory from .platform import acl_get, acl_set, set_flags, get_flags, swidth diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 70ba07f1..c2d97202 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -51,15 +51,15 @@ from .helpers import Manifest from .helpers import hardlinkable from .helpers import StableDict from .helpers import check_extension_modules -from .helpers import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern from .helpers import dir_is_tagged, is_slow_msgpack, yes, sysinfo from .helpers import log_multi -from .helpers import PatternMatcher from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm from .helpers import ErrorIgnoringTextIOWrapper from .helpers import ProgressIndicatorPercent from .helpers import basic_json_data, json_print from .helpers import replace_placeholders +from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern +from .patterns import PatternMatcher from .item import Item from .key import key_creator, tam_required_file, tam_required, RepoKey, PassphraseKey from .keymanager import KeyManager diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 8bc9959e..05e277e1 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -18,14 +18,11 @@ import sys import textwrap import threading import time -import unicodedata import uuid from binascii import hexlify from collections import namedtuple, deque, abc, Counter from datetime import datetime, timezone, timedelta -from enum import Enum -from fnmatch import translate -from functools import wraps, partial, lru_cache +from functools import partial, lru_cache from itertools import islice from operator import attrgetter from string import Formatter @@ -42,7 +39,6 @@ from . import __version_tuple__ as borg_version_tuple from . import chunker from . import crypto from . import hashindex -from . import shellpattern from .constants import * # NOQA @@ -389,387 +385,6 @@ def parse_timestamp(timestamp): return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc) -def parse_patternfile_line(line, roots, ie_commands, fallback): - """Parse a pattern-file line and act depending on which command it represents.""" - ie_command = parse_inclexcl_command(line, fallback=fallback) - if ie_command.cmd is IECommand.RootPath: - roots.append(ie_command.val) - elif ie_command.cmd is IECommand.PatternStyle: - fallback = ie_command.val - else: - # it is some kind of include/exclude command - ie_commands.append(ie_command) - return fallback - - -def load_pattern_file(fileobj, roots, ie_commands, fallback=None): - if fallback is None: - fallback = ShellPattern # ShellPattern is defined later in this module - for line in clean_lines(fileobj): - fallback = parse_patternfile_line(line, roots, ie_commands, fallback) - - -def load_exclude_file(fileobj, patterns): - for patternstr in clean_lines(fileobj): - patterns.append(parse_exclude_pattern(patternstr)) - - -class ArgparsePatternAction(argparse.Action): - def __init__(self, nargs=1, **kw): - super().__init__(nargs=nargs, **kw) - - def __call__(self, parser, args, values, option_string=None): - parse_patternfile_line(values[0], args.paths, args.patterns, ShellPattern) - - -class ArgparsePatternFileAction(argparse.Action): - def __init__(self, nargs=1, **kw): - super().__init__(nargs=nargs, **kw) - - def __call__(self, parser, args, values, option_string=None): - """Load and parse patterns from a file. - Lines empty or starting with '#' after stripping whitespace on both line ends are ignored. - """ - filename = values[0] - with open(filename) as f: - self.parse(f, args) - - def parse(self, fobj, args): - load_pattern_file(fobj, args.paths, args.patterns) - - -class ArgparseExcludeFileAction(ArgparsePatternFileAction): - def parse(self, fobj, args): - load_exclude_file(fobj, args.patterns) - - -class PatternMatcher: - """Represents a collection of pattern objects to match paths against. - - *fallback* is a boolean value that *match()* returns if no matching patterns are found. - - """ - def __init__(self, fallback=None): - self._items = [] - - # Value to return from match function when none of the patterns match. - self.fallback = fallback - - # optimizations - self._path_full_patterns = {} # full path -> return value - - # indicates whether the last match() call ended on a pattern for which - # we should recurse into any matching folder. Will be set to True or - # False when calling match(). - self.recurse_dir = None - - # whether to recurse into directories when no match is found - # TODO: allow modification as a config option? - self.recurse_dir_default = True - - self.include_patterns = [] - - # TODO: move this info to parse_inclexcl_command and store in PatternBase subclass? - self.is_include_cmd = { - IECommand.Exclude: False, - IECommand.ExcludeNoRecurse: False, - IECommand.Include: True - } - - def empty(self): - return not len(self._items) and not len(self._path_full_patterns) - - def _add(self, pattern, cmd): - """*cmd* is an IECommand value. - """ - if isinstance(pattern, PathFullPattern): - key = pattern.pattern # full, normalized path - self._path_full_patterns[key] = cmd - else: - self._items.append((pattern, cmd)) - - def add(self, patterns, cmd): - """Add list of patterns to internal list. *cmd* indicates whether the - pattern is an include/exclude pattern, and whether recursion should be - done on excluded folders. - """ - for pattern in patterns: - self._add(pattern, cmd) - - def add_includepaths(self, include_paths): - """Used to add inclusion-paths from args.paths (from commandline). - """ - include_patterns = [parse_pattern(p, PathPrefixPattern) for p in include_paths] - self.add(include_patterns, IECommand.Include) - self.fallback = not include_patterns - self.include_patterns = include_patterns - - def get_unmatched_include_patterns(self): - "Note that this only returns patterns added via *add_includepaths*." - return [p for p in self.include_patterns if p.match_count == 0] - - def add_inclexcl(self, patterns): - """Add list of patterns (of type CmdTuple) to internal list. - """ - for pattern, cmd in patterns: - self._add(pattern, cmd) - - def match(self, path): - """Return True or False depending on whether *path* is matched. - - If no match is found among the patterns in this matcher, then the value - in self.fallback is returned (defaults to None). - - """ - path = normalize_path(path) - # do a fast lookup for full path matches (note: we do not count such matches): - non_existent = object() - value = self._path_full_patterns.get(path, non_existent) - - if value is not non_existent: - # we have a full path match! - # TODO: get from pattern; don't hard-code - self.recurse_dir = True - return value - - # this is the slow way, if we have many patterns in self._items: - for (pattern, cmd) in self._items: - if pattern.match(path, normalize=False): - self.recurse_dir = pattern.recurse_dir - return self.is_include_cmd[cmd] - - # by default we will recurse if there is no match - self.recurse_dir = self.recurse_dir_default - return self.fallback - - -def normalize_path(path): - """normalize paths for MacOS (but do nothing on other platforms)""" - # HFS+ converts paths to a canonical form, so users shouldn't be required to enter an exact match. - # Windows and Unix filesystems allow different forms, so users always have to enter an exact match. - return unicodedata.normalize('NFD', path) if sys.platform == 'darwin' else path - - -class PatternBase: - """Shared logic for inclusion/exclusion patterns. - """ - PREFIX = NotImplemented - - def __init__(self, pattern, recurse_dir=False): - self.pattern_orig = pattern - self.match_count = 0 - pattern = normalize_path(pattern) - self._prepare(pattern) - self.recurse_dir = recurse_dir - - def match(self, path, normalize=True): - """Return a boolean indicating whether *path* is matched by this pattern. - - If normalize is True (default), the path will get normalized using normalize_path(), - otherwise it is assumed that it already is normalized using that function. - """ - if normalize: - path = normalize_path(path) - matches = self._match(path) - if matches: - self.match_count += 1 - return matches - - def __repr__(self): - return '%s(%s)' % (type(self), self.pattern) - - def __str__(self): - return self.pattern_orig - - def _prepare(self, pattern): - "Should set the value of self.pattern" - raise NotImplementedError - - def _match(self, path): - raise NotImplementedError - - -class PathFullPattern(PatternBase): - """Full match of a path.""" - PREFIX = "pf" - - def _prepare(self, pattern): - self.pattern = os.path.normpath(pattern) - - def _match(self, path): - return path == self.pattern - - -# For PathPrefixPattern, FnmatchPattern and ShellPattern, we require that the pattern either match the whole path -# or an initial segment of the path up to but not including a path separator. To unify the two cases, we add a path -# separator to the end of the path before matching. - - -class PathPrefixPattern(PatternBase): - """Literal files or directories listed on the command line - for some operations (e.g. extract, but not create). - If a directory is specified, all paths that start with that - path match as well. A trailing slash makes no difference. - """ - PREFIX = "pp" - - def _prepare(self, pattern): - self.pattern = os.path.normpath(pattern).rstrip(os.path.sep) + os.path.sep - - def _match(self, path): - return (path + os.path.sep).startswith(self.pattern) - - -class FnmatchPattern(PatternBase): - """Shell glob patterns to exclude. A trailing slash means to - exclude the contents of a directory, but not the directory itself. - """ - PREFIX = "fm" - - def _prepare(self, pattern): - if pattern.endswith(os.path.sep): - pattern = os.path.normpath(pattern).rstrip(os.path.sep) + os.path.sep + '*' + os.path.sep - else: - pattern = os.path.normpath(pattern) + os.path.sep + '*' - - self.pattern = pattern - - # fnmatch and re.match both cache compiled regular expressions. - # Nevertheless, this is about 10 times faster. - self.regex = re.compile(translate(self.pattern)) - - def _match(self, path): - return (self.regex.match(path + os.path.sep) is not None) - - -class ShellPattern(PatternBase): - """Shell glob patterns to exclude. A trailing slash means to - exclude the contents of a directory, but not the directory itself. - """ - PREFIX = "sh" - - def _prepare(self, pattern): - sep = os.path.sep - - if pattern.endswith(sep): - pattern = os.path.normpath(pattern).rstrip(sep) + sep + "**" + sep + "*" + sep - else: - pattern = os.path.normpath(pattern) + sep + "**" + sep + "*" - - self.pattern = pattern - self.regex = re.compile(shellpattern.translate(self.pattern)) - - def _match(self, path): - return (self.regex.match(path + os.path.sep) is not None) - - -class RegexPattern(PatternBase): - """Regular expression to exclude. - """ - PREFIX = "re" - - def _prepare(self, pattern): - self.pattern = pattern - self.regex = re.compile(pattern) - - def _match(self, path): - # Normalize path separators - if os.path.sep != '/': - path = path.replace(os.path.sep, '/') - - return (self.regex.search(path) is not None) - - -_PATTERN_CLASSES = set([ - FnmatchPattern, - PathFullPattern, - PathPrefixPattern, - RegexPattern, - ShellPattern, -]) - -_PATTERN_CLASS_BY_PREFIX = dict((i.PREFIX, i) for i in _PATTERN_CLASSES) - -CmdTuple = namedtuple('CmdTuple', 'val cmd') - - -class IECommand(Enum): - """A command that an InclExcl file line can represent. - """ - RootPath = 1 - PatternStyle = 2 - Include = 3 - Exclude = 4 - ExcludeNoRecurse = 5 - - -def get_pattern_class(prefix): - try: - return _PATTERN_CLASS_BY_PREFIX[prefix] - except KeyError: - raise ValueError("Unknown pattern style: {}".format(prefix)) from None - - -def parse_pattern(pattern, fallback=FnmatchPattern, recurse_dir=True): - """Read pattern from string and return an instance of the appropriate implementation class. - - """ - if len(pattern) > 2 and pattern[2] == ":" and pattern[:2].isalnum(): - (style, pattern) = (pattern[:2], pattern[3:]) - cls = get_pattern_class(style) - else: - cls = fallback - return cls(pattern, recurse_dir) - - -def parse_exclude_pattern(pattern_str, fallback=FnmatchPattern): - """Read pattern from string and return an instance of the appropriate implementation class. - """ - epattern_obj = parse_pattern(pattern_str, fallback) - return CmdTuple(epattern_obj, IECommand.Exclude) - - -def parse_inclexcl_command(cmd_line_str, fallback=ShellPattern): - """Read a --patterns-from command from string and return a CmdTuple object.""" - - cmd_prefix_map = { - '-': IECommand.Exclude, - '!': IECommand.ExcludeNoRecurse, - '+': IECommand.Include, - 'R': IECommand.RootPath, - 'r': IECommand.RootPath, - 'P': IECommand.PatternStyle, - 'p': IECommand.PatternStyle, - } - - try: - cmd = cmd_prefix_map[cmd_line_str[0]] - - # remaining text on command-line following the command character - remainder_str = cmd_line_str[1:].lstrip() - - if not remainder_str: - raise ValueError("Missing pattern/information!") - except (IndexError, KeyError, ValueError): - raise argparse.ArgumentTypeError("Unable to parse pattern/command: {}".format(cmd_line_str)) - - if cmd is IECommand.RootPath: - # TODO: validate string? - val = remainder_str - elif cmd is IECommand.PatternStyle: - # then remainder_str is something like 're' or 'sh' - try: - val = get_pattern_class(remainder_str) - except ValueError: - raise argparse.ArgumentTypeError("Invalid pattern style: {}".format(remainder_str)) - else: - # determine recurse_dir based on command type - recurse_dir = cmd not in [IECommand.ExcludeNoRecurse] - val = parse_pattern(remainder_str, fallback, recurse_dir) - - return CmdTuple(val, cmd) - - def timestamp(s): """Convert a --timestamp=s argument to a datetime object""" try: diff --git a/src/borg/patterns.py b/src/borg/patterns.py new file mode 100644 index 00000000..88cae357 --- /dev/null +++ b/src/borg/patterns.py @@ -0,0 +1,392 @@ +import argparse +import os.path +import re +import sys +import unicodedata +from collections import namedtuple +from enum import Enum +from fnmatch import translate + +from . import shellpattern +from .helpers import clean_lines + + +def parse_patternfile_line(line, roots, ie_commands, fallback): + """Parse a pattern-file line and act depending on which command it represents.""" + ie_command = parse_inclexcl_command(line, fallback=fallback) + if ie_command.cmd is IECommand.RootPath: + roots.append(ie_command.val) + elif ie_command.cmd is IECommand.PatternStyle: + fallback = ie_command.val + else: + # it is some kind of include/exclude command + ie_commands.append(ie_command) + return fallback + + +def load_pattern_file(fileobj, roots, ie_commands, fallback=None): + if fallback is None: + fallback = ShellPattern # ShellPattern is defined later in this module + for line in clean_lines(fileobj): + fallback = parse_patternfile_line(line, roots, ie_commands, fallback) + + +def load_exclude_file(fileobj, patterns): + for patternstr in clean_lines(fileobj): + patterns.append(parse_exclude_pattern(patternstr)) + + +class ArgparsePatternAction(argparse.Action): + def __init__(self, nargs=1, **kw): + super().__init__(nargs=nargs, **kw) + + def __call__(self, parser, args, values, option_string=None): + parse_patternfile_line(values[0], args.paths, args.patterns, ShellPattern) + + +class ArgparsePatternFileAction(argparse.Action): + def __init__(self, nargs=1, **kw): + super().__init__(nargs=nargs, **kw) + + def __call__(self, parser, args, values, option_string=None): + """Load and parse patterns from a file. + Lines empty or starting with '#' after stripping whitespace on both line ends are ignored. + """ + filename = values[0] + with open(filename) as f: + self.parse(f, args) + + def parse(self, fobj, args): + load_pattern_file(fobj, args.paths, args.patterns) + + +class ArgparseExcludeFileAction(ArgparsePatternFileAction): + def parse(self, fobj, args): + load_exclude_file(fobj, args.patterns) + + +class PatternMatcher: + """Represents a collection of pattern objects to match paths against. + + *fallback* is a boolean value that *match()* returns if no matching patterns are found. + + """ + def __init__(self, fallback=None): + self._items = [] + + # Value to return from match function when none of the patterns match. + self.fallback = fallback + + # optimizations + self._path_full_patterns = {} # full path -> return value + + # indicates whether the last match() call ended on a pattern for which + # we should recurse into any matching folder. Will be set to True or + # False when calling match(). + self.recurse_dir = None + + # whether to recurse into directories when no match is found + # TODO: allow modification as a config option? + self.recurse_dir_default = True + + self.include_patterns = [] + + # TODO: move this info to parse_inclexcl_command and store in PatternBase subclass? + self.is_include_cmd = { + IECommand.Exclude: False, + IECommand.ExcludeNoRecurse: False, + IECommand.Include: True + } + + def empty(self): + return not len(self._items) and not len(self._path_full_patterns) + + def _add(self, pattern, cmd): + """*cmd* is an IECommand value. + """ + if isinstance(pattern, PathFullPattern): + key = pattern.pattern # full, normalized path + self._path_full_patterns[key] = cmd + else: + self._items.append((pattern, cmd)) + + def add(self, patterns, cmd): + """Add list of patterns to internal list. *cmd* indicates whether the + pattern is an include/exclude pattern, and whether recursion should be + done on excluded folders. + """ + for pattern in patterns: + self._add(pattern, cmd) + + def add_includepaths(self, include_paths): + """Used to add inclusion-paths from args.paths (from commandline). + """ + include_patterns = [parse_pattern(p, PathPrefixPattern) for p in include_paths] + self.add(include_patterns, IECommand.Include) + self.fallback = not include_patterns + self.include_patterns = include_patterns + + def get_unmatched_include_patterns(self): + "Note that this only returns patterns added via *add_includepaths*." + return [p for p in self.include_patterns if p.match_count == 0] + + def add_inclexcl(self, patterns): + """Add list of patterns (of type CmdTuple) to internal list. + """ + for pattern, cmd in patterns: + self._add(pattern, cmd) + + def match(self, path): + """Return True or False depending on whether *path* is matched. + + If no match is found among the patterns in this matcher, then the value + in self.fallback is returned (defaults to None). + + """ + path = normalize_path(path) + # do a fast lookup for full path matches (note: we do not count such matches): + non_existent = object() + value = self._path_full_patterns.get(path, non_existent) + + if value is not non_existent: + # we have a full path match! + # TODO: get from pattern; don't hard-code + self.recurse_dir = True + return value + + # this is the slow way, if we have many patterns in self._items: + for (pattern, cmd) in self._items: + if pattern.match(path, normalize=False): + self.recurse_dir = pattern.recurse_dir + return self.is_include_cmd[cmd] + + # by default we will recurse if there is no match + self.recurse_dir = self.recurse_dir_default + return self.fallback + + +def normalize_path(path): + """normalize paths for MacOS (but do nothing on other platforms)""" + # HFS+ converts paths to a canonical form, so users shouldn't be required to enter an exact match. + # Windows and Unix filesystems allow different forms, so users always have to enter an exact match. + return unicodedata.normalize('NFD', path) if sys.platform == 'darwin' else path + + +class PatternBase: + """Shared logic for inclusion/exclusion patterns. + """ + PREFIX = NotImplemented + + def __init__(self, pattern, recurse_dir=False): + self.pattern_orig = pattern + self.match_count = 0 + pattern = normalize_path(pattern) + self._prepare(pattern) + self.recurse_dir = recurse_dir + + def match(self, path, normalize=True): + """Return a boolean indicating whether *path* is matched by this pattern. + + If normalize is True (default), the path will get normalized using normalize_path(), + otherwise it is assumed that it already is normalized using that function. + """ + if normalize: + path = normalize_path(path) + matches = self._match(path) + if matches: + self.match_count += 1 + return matches + + def __repr__(self): + return '%s(%s)' % (type(self), self.pattern) + + def __str__(self): + return self.pattern_orig + + def _prepare(self, pattern): + "Should set the value of self.pattern" + raise NotImplementedError + + def _match(self, path): + raise NotImplementedError + + +class PathFullPattern(PatternBase): + """Full match of a path.""" + PREFIX = "pf" + + def _prepare(self, pattern): + self.pattern = os.path.normpath(pattern) + + def _match(self, path): + return path == self.pattern + + +# For PathPrefixPattern, FnmatchPattern and ShellPattern, we require that the pattern either match the whole path +# or an initial segment of the path up to but not including a path separator. To unify the two cases, we add a path +# separator to the end of the path before matching. + + +class PathPrefixPattern(PatternBase): + """Literal files or directories listed on the command line + for some operations (e.g. extract, but not create). + If a directory is specified, all paths that start with that + path match as well. A trailing slash makes no difference. + """ + PREFIX = "pp" + + def _prepare(self, pattern): + self.pattern = os.path.normpath(pattern).rstrip(os.path.sep) + os.path.sep + + def _match(self, path): + return (path + os.path.sep).startswith(self.pattern) + + +class FnmatchPattern(PatternBase): + """Shell glob patterns to exclude. A trailing slash means to + exclude the contents of a directory, but not the directory itself. + """ + PREFIX = "fm" + + def _prepare(self, pattern): + if pattern.endswith(os.path.sep): + pattern = os.path.normpath(pattern).rstrip(os.path.sep) + os.path.sep + '*' + os.path.sep + else: + pattern = os.path.normpath(pattern) + os.path.sep + '*' + + self.pattern = pattern + + # fnmatch and re.match both cache compiled regular expressions. + # Nevertheless, this is about 10 times faster. + self.regex = re.compile(translate(self.pattern)) + + def _match(self, path): + return (self.regex.match(path + os.path.sep) is not None) + + +class ShellPattern(PatternBase): + """Shell glob patterns to exclude. A trailing slash means to + exclude the contents of a directory, but not the directory itself. + """ + PREFIX = "sh" + + def _prepare(self, pattern): + sep = os.path.sep + + if pattern.endswith(sep): + pattern = os.path.normpath(pattern).rstrip(sep) + sep + "**" + sep + "*" + sep + else: + pattern = os.path.normpath(pattern) + sep + "**" + sep + "*" + + self.pattern = pattern + self.regex = re.compile(shellpattern.translate(self.pattern)) + + def _match(self, path): + return (self.regex.match(path + os.path.sep) is not None) + + +class RegexPattern(PatternBase): + """Regular expression to exclude. + """ + PREFIX = "re" + + def _prepare(self, pattern): + self.pattern = pattern + self.regex = re.compile(pattern) + + def _match(self, path): + # Normalize path separators + if os.path.sep != '/': + path = path.replace(os.path.sep, '/') + + return (self.regex.search(path) is not None) + + +_PATTERN_CLASSES = set([ + FnmatchPattern, + PathFullPattern, + PathPrefixPattern, + RegexPattern, + ShellPattern, +]) + +_PATTERN_CLASS_BY_PREFIX = dict((i.PREFIX, i) for i in _PATTERN_CLASSES) + +CmdTuple = namedtuple('CmdTuple', 'val cmd') + + +class IECommand(Enum): + """A command that an InclExcl file line can represent. + """ + RootPath = 1 + PatternStyle = 2 + Include = 3 + Exclude = 4 + ExcludeNoRecurse = 5 + + +def get_pattern_class(prefix): + try: + return _PATTERN_CLASS_BY_PREFIX[prefix] + except KeyError: + raise ValueError("Unknown pattern style: {}".format(prefix)) from None + + +def parse_pattern(pattern, fallback=FnmatchPattern, recurse_dir=True): + """Read pattern from string and return an instance of the appropriate implementation class. + + """ + if len(pattern) > 2 and pattern[2] == ":" and pattern[:2].isalnum(): + (style, pattern) = (pattern[:2], pattern[3:]) + cls = get_pattern_class(style) + else: + cls = fallback + return cls(pattern, recurse_dir) + + +def parse_exclude_pattern(pattern_str, fallback=FnmatchPattern): + """Read pattern from string and return an instance of the appropriate implementation class. + """ + epattern_obj = parse_pattern(pattern_str, fallback) + return CmdTuple(epattern_obj, IECommand.Exclude) + + +def parse_inclexcl_command(cmd_line_str, fallback=ShellPattern): + """Read a --patterns-from command from string and return a CmdTuple object.""" + + cmd_prefix_map = { + '-': IECommand.Exclude, + '!': IECommand.ExcludeNoRecurse, + '+': IECommand.Include, + 'R': IECommand.RootPath, + 'r': IECommand.RootPath, + 'P': IECommand.PatternStyle, + 'p': IECommand.PatternStyle, + } + + try: + cmd = cmd_prefix_map[cmd_line_str[0]] + + # remaining text on command-line following the command character + remainder_str = cmd_line_str[1:].lstrip() + + if not remainder_str: + raise ValueError("Missing pattern/information!") + except (IndexError, KeyError, ValueError): + raise argparse.ArgumentTypeError("Unable to parse pattern/command: {}".format(cmd_line_str)) + + if cmd is IECommand.RootPath: + # TODO: validate string? + val = remainder_str + elif cmd is IECommand.PatternStyle: + # then remainder_str is something like 're' or 'sh' + try: + val = get_pattern_class(remainder_str) + except ValueError: + raise argparse.ArgumentTypeError("Invalid pattern style: {}".format(remainder_str)) + else: + # determine recurse_dir based on command type + recurse_dir = cmd not in [IECommand.ExcludeNoRecurse] + val = parse_pattern(remainder_str, fallback, recurse_dir) + + return CmdTuple(val, cmd) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index c065feb1..3aa2c1b5 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -33,12 +33,12 @@ from ..archiver import Archiver from ..cache import Cache from ..constants import * # NOQA from ..crypto import bytes_to_long, num_aes_blocks -from ..helpers import PatternMatcher, parse_pattern, Location, get_security_dir +from ..helpers import Location, get_security_dir from ..helpers import Manifest from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from ..helpers import bin_to_hex -from ..helpers import IECommand from ..helpers import MAX_S +from ..patterns import IECommand, PatternMatcher, parse_pattern from ..item import Item from ..key import KeyfileKeyBase, RepoKey, KeyfileKey, Passphrase, TAMRequiredError from ..keymanager import RepoIdMismatch, NotABorgKeyFile diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 047e41c8..7eb42116 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -23,9 +23,6 @@ from ..helpers import yes, TRUISH, FALSISH, DEFAULTISH from ..helpers import StableDict, int_to_bigint, bigint_to_int, bin_to_hex from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless -from ..helpers import load_exclude_file, load_pattern_file -from ..helpers import parse_pattern, PatternMatcher -from ..helpers import PathFullPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, RegexPattern from ..helpers import swidth_slice from ..helpers import chunkit from ..helpers import safe_ns, safe_s, SUPPORT_32BIT_PLATFORMS @@ -244,463 +241,6 @@ class FormatTimedeltaTestCase(BaseTestCase): ) -def check_patterns(files, pattern, expected): - """Utility for testing patterns. - """ - assert all([f == os.path.normpath(f) for f in files]), "Pattern matchers expect normalized input paths" - - matched = [f for f in files if pattern.match(f)] - - assert matched == (files if expected is None else expected) - - -@pytest.mark.parametrize("pattern, expected", [ - # "None" means all files, i.e. all match the given pattern - ("/", []), - ("/home", ["/home"]), - ("/home///", ["/home"]), - ("/./home", ["/home"]), - ("/home/user", ["/home/user"]), - ("/home/user2", ["/home/user2"]), - ("/home/user/.bashrc", ["/home/user/.bashrc"]), - ]) -def test_patterns_full(pattern, expected): - files = ["/home", "/home/user", "/home/user2", "/home/user/.bashrc", ] - - check_patterns(files, PathFullPattern(pattern), expected) - - -@pytest.mark.parametrize("pattern, expected", [ - # "None" means all files, i.e. all match the given pattern - ("", []), - ("relative", []), - ("relative/path/", ["relative/path"]), - ("relative/path", ["relative/path"]), - ]) -def test_patterns_full_relative(pattern, expected): - files = ["relative/path", "relative/path2", ] - - check_patterns(files, PathFullPattern(pattern), expected) - - -@pytest.mark.parametrize("pattern, expected", [ - # "None" means all files, i.e. all match the given pattern - ("/", None), - ("/./", None), - ("", []), - ("/home/u", []), - ("/home/user", ["/home/user/.profile", "/home/user/.bashrc"]), - ("/etc", ["/etc/server/config", "/etc/server/hosts"]), - ("///etc//////", ["/etc/server/config", "/etc/server/hosts"]), - ("/./home//..//home/user2", ["/home/user2/.profile", "/home/user2/public_html/index.html"]), - ("/srv", ["/srv/messages", "/srv/dmesg"]), - ]) -def test_patterns_prefix(pattern, expected): - files = [ - "/etc/server/config", "/etc/server/hosts", "/home", "/home/user/.profile", "/home/user/.bashrc", - "/home/user2/.profile", "/home/user2/public_html/index.html", "/srv/messages", "/srv/dmesg", - ] - - check_patterns(files, PathPrefixPattern(pattern), expected) - - -@pytest.mark.parametrize("pattern, expected", [ - # "None" means all files, i.e. all match the given pattern - ("", []), - ("foo", []), - ("relative", ["relative/path1", "relative/two"]), - ("more", ["more/relative"]), - ]) -def test_patterns_prefix_relative(pattern, expected): - files = ["relative/path1", "relative/two", "more/relative"] - - check_patterns(files, PathPrefixPattern(pattern), expected) - - -@pytest.mark.parametrize("pattern, expected", [ - # "None" means all files, i.e. all match the given pattern - ("/*", None), - ("/./*", None), - ("*", None), - ("*/*", None), - ("*///*", None), - ("/home/u", []), - ("/home/*", - ["/home/user/.profile", "/home/user/.bashrc", "/home/user2/.profile", "/home/user2/public_html/index.html", - "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails"]), - ("/home/user/*", ["/home/user/.profile", "/home/user/.bashrc"]), - ("/etc/*", ["/etc/server/config", "/etc/server/hosts"]), - ("*/.pr????e", ["/home/user/.profile", "/home/user2/.profile"]), - ("///etc//////*", ["/etc/server/config", "/etc/server/hosts"]), - ("/./home//..//home/user2/*", ["/home/user2/.profile", "/home/user2/public_html/index.html"]), - ("/srv*", ["/srv/messages", "/srv/dmesg"]), - ("/home/*/.thumbnails", ["/home/foo/.thumbnails", "/home/foo/bar/.thumbnails"]), - ]) -def test_patterns_fnmatch(pattern, expected): - files = [ - "/etc/server/config", "/etc/server/hosts", "/home", "/home/user/.profile", "/home/user/.bashrc", - "/home/user2/.profile", "/home/user2/public_html/index.html", "/srv/messages", "/srv/dmesg", - "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails", - ] - - check_patterns(files, FnmatchPattern(pattern), expected) - - -@pytest.mark.parametrize("pattern, expected", [ - # "None" means all files, i.e. all match the given pattern - ("*", None), - ("**/*", None), - ("/**/*", None), - ("/./*", None), - ("*/*", None), - ("*///*", None), - ("/home/u", []), - ("/home/*", - ["/home/user/.profile", "/home/user/.bashrc", "/home/user2/.profile", "/home/user2/public_html/index.html", - "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails"]), - ("/home/user/*", ["/home/user/.profile", "/home/user/.bashrc"]), - ("/etc/*/*", ["/etc/server/config", "/etc/server/hosts"]), - ("/etc/**/*", ["/etc/server/config", "/etc/server/hosts"]), - ("/etc/**/*/*", ["/etc/server/config", "/etc/server/hosts"]), - ("*/.pr????e", []), - ("**/.pr????e", ["/home/user/.profile", "/home/user2/.profile"]), - ("///etc//////*", ["/etc/server/config", "/etc/server/hosts"]), - ("/./home//..//home/user2/", ["/home/user2/.profile", "/home/user2/public_html/index.html"]), - ("/./home//..//home/user2/**/*", ["/home/user2/.profile", "/home/user2/public_html/index.html"]), - ("/srv*/", ["/srv/messages", "/srv/dmesg", "/srv2/blafasel"]), - ("/srv*", ["/srv", "/srv/messages", "/srv/dmesg", "/srv2", "/srv2/blafasel"]), - ("/srv/*", ["/srv/messages", "/srv/dmesg"]), - ("/srv2/**", ["/srv2", "/srv2/blafasel"]), - ("/srv2/**/", ["/srv2/blafasel"]), - ("/home/*/.thumbnails", ["/home/foo/.thumbnails"]), - ("/home/*/*/.thumbnails", ["/home/foo/bar/.thumbnails"]), - ]) -def test_patterns_shell(pattern, expected): - files = [ - "/etc/server/config", "/etc/server/hosts", "/home", "/home/user/.profile", "/home/user/.bashrc", - "/home/user2/.profile", "/home/user2/public_html/index.html", "/srv", "/srv/messages", "/srv/dmesg", - "/srv2", "/srv2/blafasel", "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails", - ] - - check_patterns(files, ShellPattern(pattern), expected) - - -@pytest.mark.parametrize("pattern, expected", [ - # "None" means all files, i.e. all match the given pattern - ("", None), - (".*", None), - ("^/", None), - ("^abc$", []), - ("^[^/]", []), - ("^(?!/srv|/foo|/opt)", - ["/home", "/home/user/.profile", "/home/user/.bashrc", "/home/user2/.profile", - "/home/user2/public_html/index.html", "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails", ]), - ]) -def test_patterns_regex(pattern, expected): - files = [ - '/srv/data', '/foo/bar', '/home', - '/home/user/.profile', '/home/user/.bashrc', - '/home/user2/.profile', '/home/user2/public_html/index.html', - '/opt/log/messages.txt', '/opt/log/dmesg.txt', - "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails", - ] - - obj = RegexPattern(pattern) - assert str(obj) == pattern - assert obj.pattern == pattern - - check_patterns(files, obj, expected) - - -def test_regex_pattern(): - # The forward slash must match the platform-specific path separator - assert RegexPattern("^/$").match("/") - assert RegexPattern("^/$").match(os.path.sep) - assert not RegexPattern(r"^\\$").match("/") - - -def use_normalized_unicode(): - return sys.platform in ("darwin",) - - -def _make_test_patterns(pattern): - return [PathPrefixPattern(pattern), - FnmatchPattern(pattern), - RegexPattern("^{}/foo$".format(pattern)), - ShellPattern(pattern), - ] - - -@pytest.mark.parametrize("pattern", _make_test_patterns("b\N{LATIN SMALL LETTER A WITH ACUTE}")) -def test_composed_unicode_pattern(pattern): - assert pattern.match("b\N{LATIN SMALL LETTER A WITH ACUTE}/foo") - assert pattern.match("ba\N{COMBINING ACUTE ACCENT}/foo") == use_normalized_unicode() - - -@pytest.mark.parametrize("pattern", _make_test_patterns("ba\N{COMBINING ACUTE ACCENT}")) -def test_decomposed_unicode_pattern(pattern): - assert pattern.match("b\N{LATIN SMALL LETTER A WITH ACUTE}/foo") == use_normalized_unicode() - assert pattern.match("ba\N{COMBINING ACUTE ACCENT}/foo") - - -@pytest.mark.parametrize("pattern", _make_test_patterns(str(b"ba\x80", "latin1"))) -def test_invalid_unicode_pattern(pattern): - assert not pattern.match("ba/foo") - assert pattern.match(str(b"ba\x80/foo", "latin1")) - - -@pytest.mark.parametrize("lines, expected", [ - # "None" means all files, i.e. none excluded - ([], None), - (["# Comment only"], None), - (["*"], []), - (["# Comment", - "*/something00.txt", - " *whitespace* ", - # Whitespace before comment - " #/ws*", - # Empty line - "", - "# EOF"], - ["/more/data", "/home", " #/wsfoobar"]), - (["re:.*"], []), - (["re:\s"], ["/data/something00.txt", "/more/data", "/home"]), - ([r"re:(.)(\1)"], ["/more/data", "/home", "\tstart/whitespace", "/whitespace/end\t"]), - (["", "", "", - "# This is a test with mixed pattern styles", - # Case-insensitive pattern - "re:(?i)BAR|ME$", - "", - "*whitespace*", - "fm:*/something00*"], - ["/more/data"]), - ([r" re:^\s "], ["/data/something00.txt", "/more/data", "/home", "/whitespace/end\t"]), - ([r" re:\s$ "], ["/data/something00.txt", "/more/data", "/home", " #/wsfoobar", "\tstart/whitespace"]), - (["pp:./"], None), - (["pp:/"], [" #/wsfoobar", "\tstart/whitespace"]), - (["pp:aaabbb"], None), - (["pp:/data", "pp: #/", "pp:\tstart", "pp:/whitespace"], ["/more/data", "/home"]), - (["/nomatch", "/more/*"], - ['/data/something00.txt', '/home', ' #/wsfoobar', '\tstart/whitespace', '/whitespace/end\t']), - # the order of exclude patterns shouldn't matter - (["/more/*", "/nomatch"], - ['/data/something00.txt', '/home', ' #/wsfoobar', '\tstart/whitespace', '/whitespace/end\t']), - ]) -def test_exclude_patterns_from_file(tmpdir, lines, expected): - files = [ - '/data/something00.txt', '/more/data', '/home', - ' #/wsfoobar', - '\tstart/whitespace', - '/whitespace/end\t', - ] - - def evaluate(filename): - patterns = [] - load_exclude_file(open(filename, "rt"), patterns) - matcher = PatternMatcher(fallback=True) - matcher.add_inclexcl(patterns) - return [path for path in files if matcher.match(path)] - - exclfile = tmpdir.join("exclude.txt") - - with exclfile.open("wt") as fh: - fh.write("\n".join(lines)) - - assert evaluate(str(exclfile)) == (files if expected is None else expected) - - -@pytest.mark.parametrize("lines, expected_roots, expected_numpatterns", [ - # "None" means all files, i.e. none excluded - ([], [], 0), - (["# Comment only"], [], 0), - (["- *"], [], 1), - (["+fm:*/something00.txt", - "-/data"], [], 2), - (["R /"], ["/"], 0), - (["R /", - "# comment"], ["/"], 0), - (["# comment", - "- /data", - "R /home"], ["/home"], 1), -]) -def test_load_patterns_from_file(tmpdir, lines, expected_roots, expected_numpatterns): - def evaluate(filename): - roots = [] - inclexclpatterns = [] - load_pattern_file(open(filename, "rt"), roots, inclexclpatterns) - return roots, len(inclexclpatterns) - patternfile = tmpdir.join("patterns.txt") - - with patternfile.open("wt") as fh: - fh.write("\n".join(lines)) - - roots, numpatterns = evaluate(str(patternfile)) - assert roots == expected_roots - assert numpatterns == expected_numpatterns - - -def test_switch_patterns_style(): - patterns = """\ - +0_initial_default_is_shell - p fm - +1_fnmatch - P re - +2_regex - +3_more_regex - P pp - +4_pathprefix - p fm - p sh - +5_shell - """ - pattern_file = io.StringIO(patterns) - roots, patterns = [], [] - load_pattern_file(pattern_file, roots, patterns) - assert len(patterns) == 6 - assert isinstance(patterns[0].val, ShellPattern) - assert isinstance(patterns[1].val, FnmatchPattern) - assert isinstance(patterns[2].val, RegexPattern) - assert isinstance(patterns[3].val, RegexPattern) - assert isinstance(patterns[4].val, PathPrefixPattern) - assert isinstance(patterns[5].val, ShellPattern) - - -@pytest.mark.parametrize("lines", [ - (["X /data"]), # illegal pattern type prefix - (["/data"]), # need a pattern type prefix -]) -def test_load_invalid_patterns_from_file(tmpdir, lines): - patternfile = tmpdir.join("patterns.txt") - with patternfile.open("wt") as fh: - fh.write("\n".join(lines)) - filename = str(patternfile) - with pytest.raises(argparse.ArgumentTypeError): - roots = [] - inclexclpatterns = [] - load_pattern_file(open(filename, "rt"), roots, inclexclpatterns) - - -@pytest.mark.parametrize("lines, expected", [ - # "None" means all files, i.e. none excluded - ([], None), - (["# Comment only"], None), - (["- *"], []), - # default match type is sh: for patterns -> * doesn't match a / - (["-*/something0?.txt"], - ['/data', '/data/something00.txt', '/data/subdir/something01.txt', - '/home', '/home/leo', '/home/leo/t', '/home/other']), - (["-fm:*/something00.txt"], - ['/data', '/data/subdir/something01.txt', '/home', '/home/leo', '/home/leo/t', '/home/other']), - (["-fm:*/something0?.txt"], - ["/data", '/home', '/home/leo', '/home/leo/t', '/home/other']), - (["+/*/something0?.txt", - "-/data"], - ["/data/something00.txt", '/home', '/home/leo', '/home/leo/t', '/home/other']), - (["+fm:*/something00.txt", - "-/data"], - ["/data/something00.txt", '/home', '/home/leo', '/home/leo/t', '/home/other']), - # include /home/leo and exclude the rest of /home: - (["+/home/leo", - "-/home/*"], - ['/data', '/data/something00.txt', '/data/subdir/something01.txt', '/home', '/home/leo', '/home/leo/t']), - # wrong order, /home/leo is already excluded by -/home/*: - (["-/home/*", - "+/home/leo"], - ['/data', '/data/something00.txt', '/data/subdir/something01.txt', '/home']), - (["+fm:/home/leo", - "-/home/"], - ['/data', '/data/something00.txt', '/data/subdir/something01.txt', '/home', '/home/leo', '/home/leo/t']), -]) -def test_inclexcl_patterns_from_file(tmpdir, lines, expected): - files = [ - '/data', '/data/something00.txt', '/data/subdir/something01.txt', - '/home', '/home/leo', '/home/leo/t', '/home/other' - ] - - def evaluate(filename): - matcher = PatternMatcher(fallback=True) - roots = [] - inclexclpatterns = [] - load_pattern_file(open(filename, "rt"), roots, inclexclpatterns) - matcher.add_inclexcl(inclexclpatterns) - return [path for path in files if matcher.match(path)] - - patternfile = tmpdir.join("patterns.txt") - - with patternfile.open("wt") as fh: - fh.write("\n".join(lines)) - - assert evaluate(str(patternfile)) == (files if expected is None else expected) - - -@pytest.mark.parametrize("pattern, cls", [ - ("", FnmatchPattern), - - # Default style - ("*", FnmatchPattern), - ("/data/*", FnmatchPattern), - - # fnmatch style - ("fm:", FnmatchPattern), - ("fm:*", FnmatchPattern), - ("fm:/data/*", FnmatchPattern), - ("fm:fm:/data/*", FnmatchPattern), - - # Regular expression - ("re:", RegexPattern), - ("re:.*", RegexPattern), - ("re:^/something/", RegexPattern), - ("re:re:^/something/", RegexPattern), - - # Path prefix - ("pp:", PathPrefixPattern), - ("pp:/", PathPrefixPattern), - ("pp:/data/", PathPrefixPattern), - ("pp:pp:/data/", PathPrefixPattern), - - # Shell-pattern style - ("sh:", ShellPattern), - ("sh:*", ShellPattern), - ("sh:/data/*", ShellPattern), - ("sh:sh:/data/*", ShellPattern), - ]) -def test_parse_pattern(pattern, cls): - assert isinstance(parse_pattern(pattern), cls) - - -@pytest.mark.parametrize("pattern", ["aa:", "fo:*", "00:", "x1:abc"]) -def test_parse_pattern_error(pattern): - with pytest.raises(ValueError): - parse_pattern(pattern) - - -def test_pattern_matcher(): - pm = PatternMatcher() - - assert pm.fallback is None - - for i in ["", "foo", "bar"]: - assert pm.match(i) is None - - # add extra entries to aid in testing - for target in ["A", "B", "Empty", "FileNotFound"]: - pm.is_include_cmd[target] = target - - pm.add([RegexPattern("^a")], "A") - pm.add([RegexPattern("^b"), RegexPattern("^z")], "B") - pm.add([RegexPattern("^$")], "Empty") - pm.fallback = "FileNotFound" - - assert pm.match("") == "Empty" - assert pm.match("aaa") == "A" - assert pm.match("bbb") == "B" - assert pm.match("ccc") == "FileNotFound" - assert pm.match("xyz") == "FileNotFound" - assert pm.match("z") == "B" - - assert PatternMatcher(fallback="hey!").fallback == "hey!" - - def test_chunkerparams(): assert ChunkerParams('19,23,21,4095') == (19, 23, 21, 4095) assert ChunkerParams('10,23,16,4095') == (10, 23, 16, 4095) diff --git a/src/borg/testsuite/patterns.py b/src/borg/testsuite/patterns.py new file mode 100644 index 00000000..ff447888 --- /dev/null +++ b/src/borg/testsuite/patterns.py @@ -0,0 +1,467 @@ +import argparse +import io +import os.path +import sys + +import pytest + +from ..patterns import PathFullPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, RegexPattern +from ..patterns import load_exclude_file, load_pattern_file +from ..patterns import parse_pattern, PatternMatcher + + +def check_patterns(files, pattern, expected): + """Utility for testing patterns. + """ + assert all([f == os.path.normpath(f) for f in files]), "Pattern matchers expect normalized input paths" + + matched = [f for f in files if pattern.match(f)] + + assert matched == (files if expected is None else expected) + + +@pytest.mark.parametrize("pattern, expected", [ + # "None" means all files, i.e. all match the given pattern + ("/", []), + ("/home", ["/home"]), + ("/home///", ["/home"]), + ("/./home", ["/home"]), + ("/home/user", ["/home/user"]), + ("/home/user2", ["/home/user2"]), + ("/home/user/.bashrc", ["/home/user/.bashrc"]), + ]) +def test_patterns_full(pattern, expected): + files = ["/home", "/home/user", "/home/user2", "/home/user/.bashrc", ] + + check_patterns(files, PathFullPattern(pattern), expected) + + +@pytest.mark.parametrize("pattern, expected", [ + # "None" means all files, i.e. all match the given pattern + ("", []), + ("relative", []), + ("relative/path/", ["relative/path"]), + ("relative/path", ["relative/path"]), + ]) +def test_patterns_full_relative(pattern, expected): + files = ["relative/path", "relative/path2", ] + + check_patterns(files, PathFullPattern(pattern), expected) + + +@pytest.mark.parametrize("pattern, expected", [ + # "None" means all files, i.e. all match the given pattern + ("/", None), + ("/./", None), + ("", []), + ("/home/u", []), + ("/home/user", ["/home/user/.profile", "/home/user/.bashrc"]), + ("/etc", ["/etc/server/config", "/etc/server/hosts"]), + ("///etc//////", ["/etc/server/config", "/etc/server/hosts"]), + ("/./home//..//home/user2", ["/home/user2/.profile", "/home/user2/public_html/index.html"]), + ("/srv", ["/srv/messages", "/srv/dmesg"]), + ]) +def test_patterns_prefix(pattern, expected): + files = [ + "/etc/server/config", "/etc/server/hosts", "/home", "/home/user/.profile", "/home/user/.bashrc", + "/home/user2/.profile", "/home/user2/public_html/index.html", "/srv/messages", "/srv/dmesg", + ] + + check_patterns(files, PathPrefixPattern(pattern), expected) + + +@pytest.mark.parametrize("pattern, expected", [ + # "None" means all files, i.e. all match the given pattern + ("", []), + ("foo", []), + ("relative", ["relative/path1", "relative/two"]), + ("more", ["more/relative"]), + ]) +def test_patterns_prefix_relative(pattern, expected): + files = ["relative/path1", "relative/two", "more/relative"] + + check_patterns(files, PathPrefixPattern(pattern), expected) + + +@pytest.mark.parametrize("pattern, expected", [ + # "None" means all files, i.e. all match the given pattern + ("/*", None), + ("/./*", None), + ("*", None), + ("*/*", None), + ("*///*", None), + ("/home/u", []), + ("/home/*", + ["/home/user/.profile", "/home/user/.bashrc", "/home/user2/.profile", "/home/user2/public_html/index.html", + "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails"]), + ("/home/user/*", ["/home/user/.profile", "/home/user/.bashrc"]), + ("/etc/*", ["/etc/server/config", "/etc/server/hosts"]), + ("*/.pr????e", ["/home/user/.profile", "/home/user2/.profile"]), + ("///etc//////*", ["/etc/server/config", "/etc/server/hosts"]), + ("/./home//..//home/user2/*", ["/home/user2/.profile", "/home/user2/public_html/index.html"]), + ("/srv*", ["/srv/messages", "/srv/dmesg"]), + ("/home/*/.thumbnails", ["/home/foo/.thumbnails", "/home/foo/bar/.thumbnails"]), + ]) +def test_patterns_fnmatch(pattern, expected): + files = [ + "/etc/server/config", "/etc/server/hosts", "/home", "/home/user/.profile", "/home/user/.bashrc", + "/home/user2/.profile", "/home/user2/public_html/index.html", "/srv/messages", "/srv/dmesg", + "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails", + ] + + check_patterns(files, FnmatchPattern(pattern), expected) + + +@pytest.mark.parametrize("pattern, expected", [ + # "None" means all files, i.e. all match the given pattern + ("*", None), + ("**/*", None), + ("/**/*", None), + ("/./*", None), + ("*/*", None), + ("*///*", None), + ("/home/u", []), + ("/home/*", + ["/home/user/.profile", "/home/user/.bashrc", "/home/user2/.profile", "/home/user2/public_html/index.html", + "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails"]), + ("/home/user/*", ["/home/user/.profile", "/home/user/.bashrc"]), + ("/etc/*/*", ["/etc/server/config", "/etc/server/hosts"]), + ("/etc/**/*", ["/etc/server/config", "/etc/server/hosts"]), + ("/etc/**/*/*", ["/etc/server/config", "/etc/server/hosts"]), + ("*/.pr????e", []), + ("**/.pr????e", ["/home/user/.profile", "/home/user2/.profile"]), + ("///etc//////*", ["/etc/server/config", "/etc/server/hosts"]), + ("/./home//..//home/user2/", ["/home/user2/.profile", "/home/user2/public_html/index.html"]), + ("/./home//..//home/user2/**/*", ["/home/user2/.profile", "/home/user2/public_html/index.html"]), + ("/srv*/", ["/srv/messages", "/srv/dmesg", "/srv2/blafasel"]), + ("/srv*", ["/srv", "/srv/messages", "/srv/dmesg", "/srv2", "/srv2/blafasel"]), + ("/srv/*", ["/srv/messages", "/srv/dmesg"]), + ("/srv2/**", ["/srv2", "/srv2/blafasel"]), + ("/srv2/**/", ["/srv2/blafasel"]), + ("/home/*/.thumbnails", ["/home/foo/.thumbnails"]), + ("/home/*/*/.thumbnails", ["/home/foo/bar/.thumbnails"]), + ]) +def test_patterns_shell(pattern, expected): + files = [ + "/etc/server/config", "/etc/server/hosts", "/home", "/home/user/.profile", "/home/user/.bashrc", + "/home/user2/.profile", "/home/user2/public_html/index.html", "/srv", "/srv/messages", "/srv/dmesg", + "/srv2", "/srv2/blafasel", "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails", + ] + + check_patterns(files, ShellPattern(pattern), expected) + + +@pytest.mark.parametrize("pattern, expected", [ + # "None" means all files, i.e. all match the given pattern + ("", None), + (".*", None), + ("^/", None), + ("^abc$", []), + ("^[^/]", []), + ("^(?!/srv|/foo|/opt)", + ["/home", "/home/user/.profile", "/home/user/.bashrc", "/home/user2/.profile", + "/home/user2/public_html/index.html", "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails", ]), + ]) +def test_patterns_regex(pattern, expected): + files = [ + '/srv/data', '/foo/bar', '/home', + '/home/user/.profile', '/home/user/.bashrc', + '/home/user2/.profile', '/home/user2/public_html/index.html', + '/opt/log/messages.txt', '/opt/log/dmesg.txt', + "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails", + ] + + obj = RegexPattern(pattern) + assert str(obj) == pattern + assert obj.pattern == pattern + + check_patterns(files, obj, expected) + + +def test_regex_pattern(): + # The forward slash must match the platform-specific path separator + assert RegexPattern("^/$").match("/") + assert RegexPattern("^/$").match(os.path.sep) + assert not RegexPattern(r"^\\$").match("/") + + +def use_normalized_unicode(): + return sys.platform in ("darwin",) + + +def _make_test_patterns(pattern): + return [PathPrefixPattern(pattern), + FnmatchPattern(pattern), + RegexPattern("^{}/foo$".format(pattern)), + ShellPattern(pattern), + ] + + +@pytest.mark.parametrize("pattern", _make_test_patterns("b\N{LATIN SMALL LETTER A WITH ACUTE}")) +def test_composed_unicode_pattern(pattern): + assert pattern.match("b\N{LATIN SMALL LETTER A WITH ACUTE}/foo") + assert pattern.match("ba\N{COMBINING ACUTE ACCENT}/foo") == use_normalized_unicode() + + +@pytest.mark.parametrize("pattern", _make_test_patterns("ba\N{COMBINING ACUTE ACCENT}")) +def test_decomposed_unicode_pattern(pattern): + assert pattern.match("b\N{LATIN SMALL LETTER A WITH ACUTE}/foo") == use_normalized_unicode() + assert pattern.match("ba\N{COMBINING ACUTE ACCENT}/foo") + + +@pytest.mark.parametrize("pattern", _make_test_patterns(str(b"ba\x80", "latin1"))) +def test_invalid_unicode_pattern(pattern): + assert not pattern.match("ba/foo") + assert pattern.match(str(b"ba\x80/foo", "latin1")) + + +@pytest.mark.parametrize("lines, expected", [ + # "None" means all files, i.e. none excluded + ([], None), + (["# Comment only"], None), + (["*"], []), + (["# Comment", + "*/something00.txt", + " *whitespace* ", + # Whitespace before comment + " #/ws*", + # Empty line + "", + "# EOF"], + ["/more/data", "/home", " #/wsfoobar"]), + (["re:.*"], []), + (["re:\s"], ["/data/something00.txt", "/more/data", "/home"]), + ([r"re:(.)(\1)"], ["/more/data", "/home", "\tstart/whitespace", "/whitespace/end\t"]), + (["", "", "", + "# This is a test with mixed pattern styles", + # Case-insensitive pattern + "re:(?i)BAR|ME$", + "", + "*whitespace*", + "fm:*/something00*"], + ["/more/data"]), + ([r" re:^\s "], ["/data/something00.txt", "/more/data", "/home", "/whitespace/end\t"]), + ([r" re:\s$ "], ["/data/something00.txt", "/more/data", "/home", " #/wsfoobar", "\tstart/whitespace"]), + (["pp:./"], None), + (["pp:/"], [" #/wsfoobar", "\tstart/whitespace"]), + (["pp:aaabbb"], None), + (["pp:/data", "pp: #/", "pp:\tstart", "pp:/whitespace"], ["/more/data", "/home"]), + (["/nomatch", "/more/*"], + ['/data/something00.txt', '/home', ' #/wsfoobar', '\tstart/whitespace', '/whitespace/end\t']), + # the order of exclude patterns shouldn't matter + (["/more/*", "/nomatch"], + ['/data/something00.txt', '/home', ' #/wsfoobar', '\tstart/whitespace', '/whitespace/end\t']), + ]) +def test_exclude_patterns_from_file(tmpdir, lines, expected): + files = [ + '/data/something00.txt', '/more/data', '/home', + ' #/wsfoobar', + '\tstart/whitespace', + '/whitespace/end\t', + ] + + def evaluate(filename): + patterns = [] + load_exclude_file(open(filename, "rt"), patterns) + matcher = PatternMatcher(fallback=True) + matcher.add_inclexcl(patterns) + return [path for path in files if matcher.match(path)] + + exclfile = tmpdir.join("exclude.txt") + + with exclfile.open("wt") as fh: + fh.write("\n".join(lines)) + + assert evaluate(str(exclfile)) == (files if expected is None else expected) + + +@pytest.mark.parametrize("lines, expected_roots, expected_numpatterns", [ + # "None" means all files, i.e. none excluded + ([], [], 0), + (["# Comment only"], [], 0), + (["- *"], [], 1), + (["+fm:*/something00.txt", + "-/data"], [], 2), + (["R /"], ["/"], 0), + (["R /", + "# comment"], ["/"], 0), + (["# comment", + "- /data", + "R /home"], ["/home"], 1), +]) +def test_load_patterns_from_file(tmpdir, lines, expected_roots, expected_numpatterns): + def evaluate(filename): + roots = [] + inclexclpatterns = [] + load_pattern_file(open(filename, "rt"), roots, inclexclpatterns) + return roots, len(inclexclpatterns) + patternfile = tmpdir.join("patterns.txt") + + with patternfile.open("wt") as fh: + fh.write("\n".join(lines)) + + roots, numpatterns = evaluate(str(patternfile)) + assert roots == expected_roots + assert numpatterns == expected_numpatterns + + +def test_switch_patterns_style(): + patterns = """\ + +0_initial_default_is_shell + p fm + +1_fnmatch + P re + +2_regex + +3_more_regex + P pp + +4_pathprefix + p fm + p sh + +5_shell + """ + pattern_file = io.StringIO(patterns) + roots, patterns = [], [] + load_pattern_file(pattern_file, roots, patterns) + assert len(patterns) == 6 + assert isinstance(patterns[0].val, ShellPattern) + assert isinstance(patterns[1].val, FnmatchPattern) + assert isinstance(patterns[2].val, RegexPattern) + assert isinstance(patterns[3].val, RegexPattern) + assert isinstance(patterns[4].val, PathPrefixPattern) + assert isinstance(patterns[5].val, ShellPattern) + + +@pytest.mark.parametrize("lines", [ + (["X /data"]), # illegal pattern type prefix + (["/data"]), # need a pattern type prefix +]) +def test_load_invalid_patterns_from_file(tmpdir, lines): + patternfile = tmpdir.join("patterns.txt") + with patternfile.open("wt") as fh: + fh.write("\n".join(lines)) + filename = str(patternfile) + with pytest.raises(argparse.ArgumentTypeError): + roots = [] + inclexclpatterns = [] + load_pattern_file(open(filename, "rt"), roots, inclexclpatterns) + + +@pytest.mark.parametrize("lines, expected", [ + # "None" means all files, i.e. none excluded + ([], None), + (["# Comment only"], None), + (["- *"], []), + # default match type is sh: for patterns -> * doesn't match a / + (["-*/something0?.txt"], + ['/data', '/data/something00.txt', '/data/subdir/something01.txt', + '/home', '/home/leo', '/home/leo/t', '/home/other']), + (["-fm:*/something00.txt"], + ['/data', '/data/subdir/something01.txt', '/home', '/home/leo', '/home/leo/t', '/home/other']), + (["-fm:*/something0?.txt"], + ["/data", '/home', '/home/leo', '/home/leo/t', '/home/other']), + (["+/*/something0?.txt", + "-/data"], + ["/data/something00.txt", '/home', '/home/leo', '/home/leo/t', '/home/other']), + (["+fm:*/something00.txt", + "-/data"], + ["/data/something00.txt", '/home', '/home/leo', '/home/leo/t', '/home/other']), + # include /home/leo and exclude the rest of /home: + (["+/home/leo", + "-/home/*"], + ['/data', '/data/something00.txt', '/data/subdir/something01.txt', '/home', '/home/leo', '/home/leo/t']), + # wrong order, /home/leo is already excluded by -/home/*: + (["-/home/*", + "+/home/leo"], + ['/data', '/data/something00.txt', '/data/subdir/something01.txt', '/home']), + (["+fm:/home/leo", + "-/home/"], + ['/data', '/data/something00.txt', '/data/subdir/something01.txt', '/home', '/home/leo', '/home/leo/t']), +]) +def test_inclexcl_patterns_from_file(tmpdir, lines, expected): + files = [ + '/data', '/data/something00.txt', '/data/subdir/something01.txt', + '/home', '/home/leo', '/home/leo/t', '/home/other' + ] + + def evaluate(filename): + matcher = PatternMatcher(fallback=True) + roots = [] + inclexclpatterns = [] + load_pattern_file(open(filename, "rt"), roots, inclexclpatterns) + matcher.add_inclexcl(inclexclpatterns) + return [path for path in files if matcher.match(path)] + + patternfile = tmpdir.join("patterns.txt") + + with patternfile.open("wt") as fh: + fh.write("\n".join(lines)) + + assert evaluate(str(patternfile)) == (files if expected is None else expected) + + +@pytest.mark.parametrize("pattern, cls", [ + ("", FnmatchPattern), + + # Default style + ("*", FnmatchPattern), + ("/data/*", FnmatchPattern), + + # fnmatch style + ("fm:", FnmatchPattern), + ("fm:*", FnmatchPattern), + ("fm:/data/*", FnmatchPattern), + ("fm:fm:/data/*", FnmatchPattern), + + # Regular expression + ("re:", RegexPattern), + ("re:.*", RegexPattern), + ("re:^/something/", RegexPattern), + ("re:re:^/something/", RegexPattern), + + # Path prefix + ("pp:", PathPrefixPattern), + ("pp:/", PathPrefixPattern), + ("pp:/data/", PathPrefixPattern), + ("pp:pp:/data/", PathPrefixPattern), + + # Shell-pattern style + ("sh:", ShellPattern), + ("sh:*", ShellPattern), + ("sh:/data/*", ShellPattern), + ("sh:sh:/data/*", ShellPattern), + ]) +def test_parse_pattern(pattern, cls): + assert isinstance(parse_pattern(pattern), cls) + + +@pytest.mark.parametrize("pattern", ["aa:", "fo:*", "00:", "x1:abc"]) +def test_parse_pattern_error(pattern): + with pytest.raises(ValueError): + parse_pattern(pattern) + + +def test_pattern_matcher(): + pm = PatternMatcher() + + assert pm.fallback is None + + for i in ["", "foo", "bar"]: + assert pm.match(i) is None + + # add extra entries to aid in testing + for target in ["A", "B", "Empty", "FileNotFound"]: + pm.is_include_cmd[target] = target + + pm.add([RegexPattern("^a")], "A") + pm.add([RegexPattern("^b"), RegexPattern("^z")], "B") + pm.add([RegexPattern("^$")], "Empty") + pm.fallback = "FileNotFound" + + assert pm.match("") == "Empty" + assert pm.match("aaa") == "A" + assert pm.match("bbb") == "B" + assert pm.match("ccc") == "FileNotFound" + assert pm.match("xyz") == "FileNotFound" + assert pm.match("z") == "B" + + assert PatternMatcher(fallback="hey!").fallback == "hey!" From c5e3232187aa4c665325a88d9d4de7c40da50065 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 1 May 2017 17:02:10 +0200 Subject: [PATCH 0821/1387] patterns: explicate translate source --- src/borg/patterns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/patterns.py b/src/borg/patterns.py index 88cae357..afa19583 100644 --- a/src/borg/patterns.py +++ b/src/borg/patterns.py @@ -1,11 +1,11 @@ import argparse +import fnmatch import os.path import re import sys import unicodedata from collections import namedtuple from enum import Enum -from fnmatch import translate from . import shellpattern from .helpers import clean_lines @@ -258,7 +258,7 @@ class FnmatchPattern(PatternBase): # fnmatch and re.match both cache compiled regular expressions. # Nevertheless, this is about 10 times faster. - self.regex = re.compile(translate(self.pattern)) + self.regex = re.compile(fnmatch.translate(self.pattern)) def _match(self, path): return (self.regex.match(path + os.path.sep) is not None) From ef4cdfacaef4c1547999710e8aa50765f9094dff Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 1 May 2017 17:03:49 +0200 Subject: [PATCH 0822/1387] patterns: use set literal instead of set([]) --- src/borg/patterns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/patterns.py b/src/borg/patterns.py index afa19583..d93c4166 100644 --- a/src/borg/patterns.py +++ b/src/borg/patterns.py @@ -302,13 +302,13 @@ class RegexPattern(PatternBase): return (self.regex.search(path) is not None) -_PATTERN_CLASSES = set([ +_PATTERN_CLASSES = { FnmatchPattern, PathFullPattern, PathPrefixPattern, RegexPattern, ShellPattern, -]) +} _PATTERN_CLASS_BY_PREFIX = dict((i.PREFIX, i) for i in _PATTERN_CLASSES) From fcce0ca2bc854549b85a5497ac44ad6158953adb Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 1 May 2017 22:31:18 +0200 Subject: [PATCH 0823/1387] docs: disable smartypants --- docs/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d1d64f9f..784c4022 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -148,7 +148,9 @@ html_last_updated_fmt = '%Y-%m-%d' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# +# This is disabled to avoid mangling --options-that-appear-in-texts. +html_use_smartypants = False # Custom sidebar templates, maps document names to template names. html_sidebars = { From fa381ffcbe02edb3b7c605973075a03d7dc16225 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 2 May 2017 18:48:28 +0200 Subject: [PATCH 0824/1387] create package borg.algorithms with borg.algorithms.crc32 module --- .gitignore | 2 +- setup.py | 8 ++++---- src/borg/{ => algorithms}/crc32.pyx | 2 +- src/borg/{_crc32/clmul.c => algorithms/crc32_clmul.c} | 0 src/borg/{_crc32/crc32.c => algorithms/crc32_dispatch.c} | 4 ++-- .../slice_by_8.c => algorithms/crc32_slice_by_8.c} | 0 src/borg/archiver.py | 2 +- src/borg/repository.py | 2 +- src/borg/testsuite/crc32.py | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) rename src/borg/{ => algorithms}/crc32.pyx (96%) rename src/borg/{_crc32/clmul.c => algorithms/crc32_clmul.c} (100%) rename src/borg/{_crc32/crc32.c => algorithms/crc32_dispatch.c} (97%) rename src/borg/{_crc32/slice_by_8.c => algorithms/crc32_slice_by_8.c} (100%) diff --git a/.gitignore b/.gitignore index e5ec9ae7..69cf3f75 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ chunker.c compress.c crypto.c item.c -src/borg/crc32.c +src/borg/algorithms/crc32.c src/borg/platform/darwin.c src/borg/platform/freebsd.c src/borg/platform/linux.c diff --git a/setup.py b/setup.py index 15ebb8ed..4ebf10fc 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ crypto_source = 'src/borg/crypto.pyx' chunker_source = 'src/borg/chunker.pyx' hashindex_source = 'src/borg/hashindex.pyx' item_source = 'src/borg/item.pyx' -crc32_source = 'src/borg/crc32.pyx' +crc32_source = 'src/borg/algorithms/crc32.pyx' platform_posix_source = 'src/borg/platform/posix.pyx' platform_linux_source = 'src/borg/platform/linux.pyx' platform_darwin_source = 'src/borg/platform/darwin.pyx' @@ -91,8 +91,8 @@ try: 'src/borg/chunker.c', 'src/borg/_chunker.c', 'src/borg/hashindex.c', 'src/borg/_hashindex.c', 'src/borg/item.c', - 'src/borg/crc32.c', - 'src/borg/_crc32/crc32.c', 'src/borg/_crc32/clmul.c', 'src/borg/_crc32/slice_by_8.c', + 'src/borg/algorithms/crc32.c', + 'src/borg/algorithms/crc32_dispatch.c', 'src/borg/algorithms/crc32_clmul.c', 'src/borg/algorithms/crc32_slice_by_8.c', 'src/borg/platform/posix.c', 'src/borg/platform/linux.c', 'src/borg/platform/freebsd.c', @@ -582,7 +582,7 @@ if not on_rtd: Extension('borg.chunker', [chunker_source]), Extension('borg.hashindex', [hashindex_source]), Extension('borg.item', [item_source]), - Extension('borg.crc32', [crc32_source]), + Extension('borg.algorithms.crc32', [crc32_source]), ] if not sys.platform.startswith(('win32', )): ext_modules.append(Extension('borg.platform.posix', [platform_posix_source])) diff --git a/src/borg/crc32.pyx b/src/borg/algorithms/crc32.pyx similarity index 96% rename from src/borg/crc32.pyx rename to src/borg/algorithms/crc32.pyx index 4854e38f..07c8560f 100644 --- a/src/borg/crc32.pyx +++ b/src/borg/algorithms/crc32.pyx @@ -3,7 +3,7 @@ from libc.stdint cimport uint32_t from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release -cdef extern from "_crc32/crc32.c": +cdef extern from "crc32_dispatch.c": uint32_t _crc32_slice_by_8 "crc32_slice_by_8"(const void* data, size_t length, uint32_t initial_crc) uint32_t _crc32_clmul "crc32_clmul"(const void* data, size_t length, uint32_t initial_crc) diff --git a/src/borg/_crc32/clmul.c b/src/borg/algorithms/crc32_clmul.c similarity index 100% rename from src/borg/_crc32/clmul.c rename to src/borg/algorithms/crc32_clmul.c diff --git a/src/borg/_crc32/crc32.c b/src/borg/algorithms/crc32_dispatch.c similarity index 97% rename from src/borg/_crc32/crc32.c rename to src/borg/algorithms/crc32_dispatch.c index 23959259..19c7ebe9 100644 --- a/src/borg/_crc32/crc32.c +++ b/src/borg/algorithms/crc32_dispatch.c @@ -1,6 +1,6 @@ /* always compile slice by 8 as a runtime fallback */ -#include "slice_by_8.c" +#include "crc32_slice_by_8.c" #ifdef __GNUC__ /* @@ -69,7 +69,7 @@ #endif /* ifdef __GNUC__ */ #ifdef FOLDING_CRC -#include "clmul.c" +#include "crc32_clmul.c" #else static uint32_t diff --git a/src/borg/_crc32/slice_by_8.c b/src/borg/algorithms/crc32_slice_by_8.c similarity index 100% rename from src/borg/_crc32/slice_by_8.c rename to src/borg/algorithms/crc32_slice_by_8.c diff --git a/src/borg/archiver.py b/src/borg/archiver.py index c2d97202..d7bf3742 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -31,12 +31,12 @@ import msgpack import borg from . import __version__ from . import helpers +from .algorithms.crc32 import crc32 from .archive import Archive, ArchiveChecker, ArchiveRecreater, Statistics, is_special from .archive import BackupOSError, backup_io from .cache import Cache from .constants import * # NOQA from .compress import CompressionSpec -from .crc32 import crc32 from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from .helpers import Error, NoManifestError, set_ec from .helpers import location_validator, archivename_validator, ChunkerParams diff --git a/src/borg/repository.py b/src/borg/repository.py index b5c2e193..d7a7f449 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -23,7 +23,7 @@ from .locking import Lock, LockError, LockErrorT from .logger import create_logger from .lrucache import LRUCache from .platform import SaveFile, SyncFile, sync_dir, safe_fadvise -from .crc32 import crc32 +from .algorithms.crc32 import crc32 logger = create_logger(__name__) diff --git a/src/borg/testsuite/crc32.py b/src/borg/testsuite/crc32.py index 4faed809..4eb59fa8 100644 --- a/src/borg/testsuite/crc32.py +++ b/src/borg/testsuite/crc32.py @@ -3,7 +3,7 @@ import zlib import pytest -from .. import crc32 +from ..algorithms import crc32 crc32_implementations = [crc32.crc32_slice_by_8] if crc32.have_clmul: From 390aa76c725190b4078b5cb6c7cc80de0f935c31 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 2 May 2017 18:49:15 +0200 Subject: [PATCH 0825/1387] move blake2 to borg/algorithms --- src/borg/{ => algorithms}/blake2-libselect.h | 0 src/borg/{ => algorithms}/blake2/COPYING | 0 src/borg/{ => algorithms}/blake2/README.md | 0 src/borg/{ => algorithms}/blake2/blake2-impl.h | 0 src/borg/{ => algorithms}/blake2/blake2.h | 0 src/borg/{ => algorithms}/blake2/blake2b-ref.c | 0 src/borg/crypto.pyx | 2 +- 7 files changed, 1 insertion(+), 1 deletion(-) rename src/borg/{ => algorithms}/blake2-libselect.h (100%) rename src/borg/{ => algorithms}/blake2/COPYING (100%) rename src/borg/{ => algorithms}/blake2/README.md (100%) rename src/borg/{ => algorithms}/blake2/blake2-impl.h (100%) rename src/borg/{ => algorithms}/blake2/blake2.h (100%) rename src/borg/{ => algorithms}/blake2/blake2b-ref.c (100%) diff --git a/src/borg/blake2-libselect.h b/src/borg/algorithms/blake2-libselect.h similarity index 100% rename from src/borg/blake2-libselect.h rename to src/borg/algorithms/blake2-libselect.h diff --git a/src/borg/blake2/COPYING b/src/borg/algorithms/blake2/COPYING similarity index 100% rename from src/borg/blake2/COPYING rename to src/borg/algorithms/blake2/COPYING diff --git a/src/borg/blake2/README.md b/src/borg/algorithms/blake2/README.md similarity index 100% rename from src/borg/blake2/README.md rename to src/borg/algorithms/blake2/README.md diff --git a/src/borg/blake2/blake2-impl.h b/src/borg/algorithms/blake2/blake2-impl.h similarity index 100% rename from src/borg/blake2/blake2-impl.h rename to src/borg/algorithms/blake2/blake2-impl.h diff --git a/src/borg/blake2/blake2.h b/src/borg/algorithms/blake2/blake2.h similarity index 100% rename from src/borg/blake2/blake2.h rename to src/borg/algorithms/blake2/blake2.h diff --git a/src/borg/blake2/blake2b-ref.c b/src/borg/algorithms/blake2/blake2b-ref.c similarity index 100% rename from src/borg/blake2/blake2b-ref.c rename to src/borg/algorithms/blake2/blake2b-ref.c diff --git a/src/borg/crypto.pyx b/src/borg/crypto.pyx index 0ed09141..ae562d1a 100644 --- a/src/borg/crypto.pyx +++ b/src/borg/crypto.pyx @@ -10,7 +10,7 @@ from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release API_VERSION = '1.1_01' -cdef extern from "blake2-libselect.h": +cdef extern from "algorithms/blake2-libselect.h": ctypedef struct blake2b_state: pass From 956b50b29cb6d3aec6b7a02d46325e8ab50bc149 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 2 May 2017 18:52:36 +0200 Subject: [PATCH 0826/1387] move chunker to borg.algorithms --- setup.py | 6 +++--- src/borg/algorithms/__init__.py | 0 src/borg/{_chunker.c => algorithms/buzhash.c} | 0 src/borg/{ => algorithms}/chunker.pyx | 2 +- src/borg/archive.py | 3 ++- src/borg/helpers.py | 9 +++++---- src/borg/testsuite/chunker.py | 2 +- 7 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 src/borg/algorithms/__init__.py rename src/borg/{_chunker.c => algorithms/buzhash.c} (100%) rename src/borg/{ => algorithms}/chunker.pyx (98%) diff --git a/setup.py b/setup.py index 4ebf10fc..066bb451 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ from setuptools.command.sdist import sdist compress_source = 'src/borg/compress.pyx' crypto_source = 'src/borg/crypto.pyx' -chunker_source = 'src/borg/chunker.pyx' +chunker_source = 'src/borg/algorithms/chunker.pyx' hashindex_source = 'src/borg/hashindex.pyx' item_source = 'src/borg/item.pyx' crc32_source = 'src/borg/algorithms/crc32.pyx' @@ -88,7 +88,7 @@ try: self.filelist.extend([ 'src/borg/compress.c', 'src/borg/crypto.c', - 'src/borg/chunker.c', 'src/borg/_chunker.c', + 'src/borg/algorithms/chunker.c', 'src/borg/algorithms/buzhash.c', 'src/borg/hashindex.c', 'src/borg/_hashindex.c', 'src/borg/item.c', 'src/borg/algorithms/crc32.c', @@ -579,9 +579,9 @@ if not on_rtd: ext_modules += [ Extension('borg.compress', [compress_source], libraries=['lz4'], include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), Extension('borg.crypto', [crypto_source], libraries=crypto_libraries, include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), - Extension('borg.chunker', [chunker_source]), Extension('borg.hashindex', [hashindex_source]), Extension('borg.item', [item_source]), + Extension('borg.algorithms.chunker', [chunker_source]), Extension('borg.algorithms.crc32', [crc32_source]), ] if not sys.platform.startswith(('win32', )): diff --git a/src/borg/algorithms/__init__.py b/src/borg/algorithms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/borg/_chunker.c b/src/borg/algorithms/buzhash.c similarity index 100% rename from src/borg/_chunker.c rename to src/borg/algorithms/buzhash.c diff --git a/src/borg/chunker.pyx b/src/borg/algorithms/chunker.pyx similarity index 98% rename from src/borg/chunker.pyx rename to src/borg/algorithms/chunker.pyx index bbe47cec..efe87a49 100644 --- a/src/borg/chunker.pyx +++ b/src/borg/algorithms/chunker.pyx @@ -4,7 +4,7 @@ API_VERSION = '1.1_01' from libc.stdlib cimport free -cdef extern from "_chunker.c": +cdef extern from "buzhash.c": ctypedef int uint32_t ctypedef struct _Chunker "Chunker": pass diff --git a/src/borg/archive.py b/src/borg/archive.py index 0a0bd9e5..791b3070 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -16,11 +16,12 @@ from shutil import get_terminal_size import msgpack from .logger import create_logger + logger = create_logger() from . import xattr from .cache import ChunkListEntry -from .chunker import Chunker +from .algorithms.chunker import Chunker from .compress import Compressor, CompressionSpec from .constants import * # NOQA from .hashindex import ChunkIndex, ChunkIndexEntry diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 05e277e1..18ac7f7a 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1,11 +1,11 @@ import argparse -import contextlib import collections +import contextlib import grp import hashlib -import logging import io import json +import logging import os import os.path import platform @@ -25,20 +25,21 @@ from datetime import datetime, timezone, timedelta from functools import partial, lru_cache from itertools import islice from operator import attrgetter -from string import Formatter from shutil import get_terminal_size +from string import Formatter import msgpack import msgpack.fallback from .logger import create_logger + logger = create_logger() from . import __version__ as borg_version from . import __version_tuple__ as borg_version_tuple -from . import chunker from . import crypto from . import hashindex +from .algorithms import chunker from .constants import * # NOQA diff --git a/src/borg/testsuite/chunker.py b/src/borg/testsuite/chunker.py index 2a14bd60..d18c4d0b 100644 --- a/src/borg/testsuite/chunker.py +++ b/src/borg/testsuite/chunker.py @@ -1,6 +1,6 @@ from io import BytesIO -from ..chunker import Chunker, buzhash, buzhash_update +from ..algorithms.chunker import Chunker, buzhash, buzhash_update from ..constants import * # NOQA from . import BaseTestCase From a976e11a6363559532ba10f9c404f9ec82c3c96e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 2 May 2017 19:05:27 +0200 Subject: [PATCH 0827/1387] create crypto package with key, keymanager, low_level --- .gitignore | 2 +- setup.py | 13 +++++----- src/borg/archive.py | 4 +-- src/borg/archiver.py | 9 ++++--- src/borg/cache.py | 7 ++--- src/borg/crypto/__init__.py | 0 src/borg/{ => crypto}/key.py | 26 +++++++++---------- src/borg/{ => crypto}/keymanager.py | 11 ++++---- src/borg/{crypto.pyx => crypto/low_level.pyx} | 2 +- src/borg/{ => crypto}/nonces.py | 10 +++---- src/borg/helpers.py | 6 ++--- src/borg/testsuite/archive.py | 9 +++---- src/borg/testsuite/archiver.py | 26 +++++++++---------- src/borg/testsuite/crypto.py | 6 ++--- src/borg/testsuite/key.py | 15 ++++++----- src/borg/testsuite/nonces.py | 7 +++-- src/borg/testsuite/upgrader.py | 2 +- src/borg/upgrader.py | 6 ++--- 18 files changed, 82 insertions(+), 79 deletions(-) create mode 100644 src/borg/crypto/__init__.py rename src/borg/{ => crypto}/key.py (98%) rename src/borg/{ => crypto}/keymanager.py (98%) rename src/borg/{crypto.pyx => crypto/low_level.pyx} (99%) rename src/borg/{ => crypto}/nonces.py (95%) diff --git a/.gitignore b/.gitignore index 69cf3f75..0fc91e1c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ borg-env hashindex.c chunker.c compress.c -crypto.c +low_level.c item.c src/borg/algorithms/crc32.c src/borg/platform/darwin.c diff --git a/setup.py b/setup.py index 066bb451..e5bae3fe 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ from setuptools import setup, find_packages, Extension from setuptools.command.sdist import sdist compress_source = 'src/borg/compress.pyx' -crypto_source = 'src/borg/crypto.pyx' +crypto_ll_source = 'src/borg/crypto/low_level.pyx' chunker_source = 'src/borg/algorithms/chunker.pyx' hashindex_source = 'src/borg/hashindex.pyx' item_source = 'src/borg/item.pyx' @@ -62,7 +62,7 @@ platform_freebsd_source = 'src/borg/platform/freebsd.pyx' cython_sources = [ compress_source, - crypto_source, + crypto_ll_source, chunker_source, hashindex_source, item_source, @@ -87,7 +87,7 @@ try: def make_distribution(self): self.filelist.extend([ 'src/borg/compress.c', - 'src/borg/crypto.c', + 'src/borg/crypto/low_level.c', 'src/borg/algorithms/chunker.c', 'src/borg/algorithms/buzhash.c', 'src/borg/hashindex.c', 'src/borg/_hashindex.c', 'src/borg/item.c', @@ -106,7 +106,7 @@ except ImportError: raise Exception('Cython is required to run sdist') compress_source = compress_source.replace('.pyx', '.c') - crypto_source = crypto_source.replace('.pyx', '.c') + crypto_ll_source = crypto_ll_source.replace('.pyx', '.c') chunker_source = chunker_source.replace('.pyx', '.c') hashindex_source = hashindex_source.replace('.pyx', '.c') item_source = item_source.replace('.pyx', '.c') @@ -117,7 +117,7 @@ except ImportError: platform_darwin_source = platform_darwin_source.replace('.pyx', '.c') from distutils.command.build_ext import build_ext if not on_rtd and not all(os.path.exists(path) for path in [ - compress_source, crypto_source, chunker_source, hashindex_source, item_source, crc32_source, + compress_source, crypto_ll_source, chunker_source, hashindex_source, item_source, crc32_source, platform_posix_source, platform_linux_source, platform_freebsd_source, platform_darwin_source]): raise ImportError('The GIT version of Borg needs Cython. Install Cython or use a released version.') @@ -578,7 +578,8 @@ ext_modules = [] if not on_rtd: ext_modules += [ Extension('borg.compress', [compress_source], libraries=['lz4'], include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), - Extension('borg.crypto', [crypto_source], libraries=crypto_libraries, include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), + Extension('borg.crypto', [crypto_ll_source], libraries=crypto_libraries, include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), + Extension('borg.crypto.low_level', [crypto_ll_source], libraries=crypto_libraries, include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), Extension('borg.hashindex', [hashindex_source]), Extension('borg.item', [item_source]), Extension('borg.algorithms.chunker', [chunker_source]), diff --git a/src/borg/archive.py b/src/borg/archive.py index 791b3070..96a9070c 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -20,8 +20,9 @@ from .logger import create_logger logger = create_logger() from . import xattr -from .cache import ChunkListEntry from .algorithms.chunker import Chunker +from .cache import ChunkListEntry +from .crypto.key import key_factory from .compress import Compressor, CompressionSpec from .constants import * # NOQA from .hashindex import ChunkIndex, ChunkIndexEntry @@ -39,7 +40,6 @@ from .helpers import safe_ns from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi from .patterns import PathPrefixPattern, FnmatchPattern, IECommand from .item import Item, ArchiveItem -from .key import key_factory from .platform import acl_get, acl_set, set_flags, get_flags, swidth from .remote import cache_if_remote from .repository import Repository, LIST_SCAN_LIMIT diff --git a/src/borg/archiver.py b/src/borg/archiver.py index d7bf3742..9556c2da 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -24,6 +24,7 @@ from datetime import datetime, timedelta from itertools import zip_longest from .logger import create_logger, setup_logging + logger = create_logger() import msgpack @@ -37,15 +38,17 @@ from .archive import BackupOSError, backup_io from .cache import Cache from .constants import * # NOQA from .compress import CompressionSpec +from .crypto.key import key_creator, tam_required_file, tam_required, RepoKey, PassphraseKey +from .crypto.keymanager import KeyManager from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from .helpers import Error, NoManifestError, set_ec from .helpers import location_validator, archivename_validator, ChunkerParams from .helpers import PrefixSpec, SortBySpec, HUMAN_SORT_KEYS from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter -from .helpers import format_time, format_timedelta, format_file_size, format_archive +from .helpers import format_timedelta, format_file_size, format_archive from .helpers import safe_encode, remove_surrogates, bin_to_hex, prepare_dump_dict from .helpers import prune_within, prune_split -from .helpers import to_localtime, timestamp +from .helpers import timestamp from .helpers import get_cache_dir from .helpers import Manifest from .helpers import hardlinkable @@ -61,8 +64,6 @@ from .helpers import replace_placeholders from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern from .patterns import PatternMatcher from .item import Item -from .key import key_creator, tam_required_file, tam_required, RepoKey, PassphraseKey -from .keymanager import KeyManager from .platform import get_flags, umount, get_process_id, SyncFile from .remote import RepositoryServer, RemoteRepository, cache_if_remote from .repository import Repository, LIST_SCAN_LIMIT diff --git a/src/borg/cache.py b/src/borg/cache.py index c97222db..8f428f8c 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -1,13 +1,14 @@ import configparser import os -import stat import shutil +import stat from binascii import unhexlify from collections import namedtuple import msgpack from .logger import create_logger + logger = create_logger() from .constants import CACHE_README @@ -21,8 +22,8 @@ from .helpers import safe_ns from .helpers import yes, hostname_is_unique from .helpers import remove_surrogates from .helpers import ProgressIndicatorPercent, ProgressIndicatorMessage -from .item import Item, ArchiveItem, ChunkListEntry -from .key import PlaintextKey +from .item import ArchiveItem, ChunkListEntry +from .crypto.key import PlaintextKey from .locking import Lock from .platform import SaveFile from .remote import cache_if_remote diff --git a/src/borg/crypto/__init__.py b/src/borg/crypto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/borg/key.py b/src/borg/crypto/key.py similarity index 98% rename from src/borg/key.py rename to src/borg/crypto/key.py index 6cefba24..b2f66da9 100644 --- a/src/borg/key.py +++ b/src/borg/crypto/key.py @@ -3,27 +3,27 @@ import getpass import os import sys import textwrap -from binascii import a2b_base64, b2a_base64, hexlify, unhexlify +from binascii import a2b_base64, b2a_base64, hexlify from hashlib import sha256, sha512, pbkdf2_hmac from hmac import HMAC, compare_digest import msgpack -from .logger import create_logger +from borg.logger import create_logger + logger = create_logger() -from .constants import * # NOQA -from .compress import Compressor -from .crypto import AES, bytes_to_long, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512 -from .helpers import StableDict -from .helpers import Error, IntegrityError -from .helpers import yes -from .helpers import get_keys_dir, get_security_dir -from .helpers import bin_to_hex -from .item import Key, EncryptedKey -from .platform import SaveFile +from ..constants import * # NOQA +from ..compress import Compressor +from ..helpers import StableDict +from ..helpers import Error, IntegrityError +from ..helpers import yes +from ..helpers import get_keys_dir, get_security_dir +from ..helpers import bin_to_hex +from ..item import Key, EncryptedKey +from ..platform import SaveFile from .nonces import NonceManager - +from .low_level import AES, bytes_to_long, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512 PREFIX = b'\0' * 8 diff --git a/src/borg/keymanager.py b/src/borg/crypto/keymanager.py similarity index 98% rename from src/borg/keymanager.py rename to src/borg/crypto/keymanager.py index c4c1f602..f6564a88 100644 --- a/src/borg/keymanager.py +++ b/src/borg/crypto/keymanager.py @@ -1,12 +1,13 @@ -from binascii import unhexlify, a2b_base64, b2a_base64 import binascii -import textwrap -from hashlib import sha256 import pkgutil +import textwrap +from binascii import unhexlify, a2b_base64, b2a_base64 +from hashlib import sha256 + +from ..helpers import Manifest, NoManifestError, Error, yes, bin_to_hex +from ..repository import Repository from .key import KeyfileKey, KeyfileNotFoundError, KeyBlobStorage, identify_key -from .helpers import Manifest, NoManifestError, Error, yes, bin_to_hex -from .repository import Repository class UnencryptedRepo(Error): diff --git a/src/borg/crypto.pyx b/src/borg/crypto/low_level.pyx similarity index 99% rename from src/borg/crypto.pyx rename to src/borg/crypto/low_level.pyx index ae562d1a..d98228ef 100644 --- a/src/borg/crypto.pyx +++ b/src/borg/crypto/low_level.pyx @@ -10,7 +10,7 @@ from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release API_VERSION = '1.1_01' -cdef extern from "algorithms/blake2-libselect.h": +cdef extern from "../algorithms/blake2-libselect.h": ctypedef struct blake2b_state: pass diff --git a/src/borg/nonces.py b/src/borg/crypto/nonces.py similarity index 95% rename from src/borg/nonces.py rename to src/borg/crypto/nonces.py index e6eb7a2c..7145d5e0 100644 --- a/src/borg/nonces.py +++ b/src/borg/crypto/nonces.py @@ -2,12 +2,12 @@ import os import sys from binascii import unhexlify -from .crypto import bytes_to_long, long_to_bytes -from .helpers import get_security_dir -from .helpers import bin_to_hex -from .platform import SaveFile -from .remote import InvalidRPCMethod +from ..helpers import get_security_dir +from ..helpers import bin_to_hex +from ..platform import SaveFile +from ..remote import InvalidRPCMethod +from .low_level import bytes_to_long, long_to_bytes MAX_REPRESENTABLE_NONCE = 2**64 - 1 NONCE_SPACE_RESERVATION = 2**28 # This in units of AES blocksize (16 bytes) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 18ac7f7a..57dfefa6 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -35,9 +35,9 @@ from .logger import create_logger logger = create_logger() +import borg.crypto.low_level from . import __version__ as borg_version from . import __version_tuple__ as borg_version_tuple -from . import crypto from . import hashindex from .algorithms import chunker from .constants import * # NOQA @@ -120,7 +120,7 @@ def check_extension_modules(): raise ExtensionModuleError if compress.API_VERSION != '1.1_03': raise ExtensionModuleError - if crypto.API_VERSION != '1.1_01': + if borg.crypto.low_level.API_VERSION != '1.1_01': raise ExtensionModuleError if platform.API_VERSION != platform.OS_API_VERSION != '1.1_01': raise ExtensionModuleError @@ -233,7 +233,7 @@ class Manifest: @classmethod def load(cls, repository, key=None, force_tam_not_required=False): from .item import ManifestItem - from .key import key_factory, tam_required_file, tam_required + from .crypto.key import key_factory, tam_required_file, tam_required from .repository import Repository try: cdata = repository.get(cls.MANIFEST_ID) diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index 3d82a4ce..bc113352 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -1,18 +1,17 @@ -import os from collections import OrderedDict from datetime import datetime, timezone from io import StringIO from unittest.mock import Mock -import pytest import msgpack +import pytest +from . import BaseTestCase +from ..crypto.key import PlaintextKey from ..archive import Archive, CacheChunkBuffer, RobustUnpacker, valid_msgpacked_dict, ITEM_KEYS, Statistics from ..archive import BackupOSError, backup_io, backup_io_iter -from ..item import Item, ArchiveItem -from ..key import PlaintextKey from ..helpers import Manifest -from . import BaseTestCase +from ..item import Item, ArchiveItem @pytest.fixture() diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 3aa2c1b5..d5dffe8d 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1,38 +1,40 @@ -from binascii import unhexlify, b2a_base64 -from configparser import ConfigParser import errno -import os -import inspect import json -from datetime import datetime -from datetime import timedelta -from io import StringIO import logging +import os import random +import shutil import socket import stat import subprocess import sys -import shutil import tempfile import time import unittest -from unittest.mock import patch +from binascii import unhexlify, b2a_base64 +from configparser import ConfigParser +from datetime import datetime +from datetime import timedelta from hashlib import sha256 +from io import StringIO +from unittest.mock import patch import msgpack import pytest + try: import llfuse except ImportError: pass from .. import xattr, helpers, platform -from ..archive import Archive, ChunkBuffer, ArchiveRecreater, flags_noatime, flags_normal +from ..archive import Archive, ChunkBuffer, flags_noatime, flags_normal from ..archiver import Archiver from ..cache import Cache from ..constants import * # NOQA -from ..crypto import bytes_to_long, num_aes_blocks +from ..crypto.low_level import bytes_to_long, num_aes_blocks +from ..crypto.key import KeyfileKeyBase, RepoKey, KeyfileKey, Passphrase, TAMRequiredError +from ..crypto.keymanager import RepoIdMismatch, NotABorgKeyFile from ..helpers import Location, get_security_dir from ..helpers import Manifest from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR @@ -40,8 +42,6 @@ from ..helpers import bin_to_hex from ..helpers import MAX_S from ..patterns import IECommand, PatternMatcher, parse_pattern from ..item import Item -from ..key import KeyfileKeyBase, RepoKey, KeyfileKey, Passphrase, TAMRequiredError -from ..keymanager import RepoIdMismatch, NotABorgKeyFile from ..remote import RemoteRepository, PathNotAllowed from ..repository import Repository from . import has_lchflags, has_llfuse diff --git a/src/borg/testsuite/crypto.py b/src/borg/testsuite/crypto.py index 92cb06a4..6406064d 100644 --- a/src/borg/testsuite/crypto.py +++ b/src/borg/testsuite/crypto.py @@ -1,8 +1,8 @@ from binascii import hexlify, unhexlify -from ..crypto import AES, bytes_to_long, bytes_to_int, long_to_bytes, hmac_sha256, blake2b_256 -from ..crypto import increment_iv, bytes16_to_int, int_to_bytes16 -from ..crypto import hkdf_hmac_sha512 +from ..crypto.low_level import AES, bytes_to_long, bytes_to_int, long_to_bytes, hmac_sha256, blake2b_256 +from ..crypto.low_level import increment_iv, bytes16_to_int, int_to_bytes16 +from ..crypto.low_level import hkdf_hmac_sha512 from . import BaseTestCase # Note: these tests are part of the self test, do not use or import py.test functionality here. diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index e92f9f0c..5f0ad367 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -1,20 +1,21 @@ import getpass +import os.path import re import tempfile -import os.path from binascii import hexlify, unhexlify -import pytest import msgpack +import pytest -from ..crypto import bytes_to_long, num_aes_blocks +from ..crypto.key import Passphrase, PasswordRetriesExceeded, bin_to_hex +from ..crypto.key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, \ + AuthenticatedKey +from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError +from ..crypto.low_level import bytes_to_long, num_aes_blocks +from ..helpers import IntegrityError from ..helpers import Location from ..helpers import StableDict -from ..helpers import IntegrityError from ..helpers import get_security_dir -from ..key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, AuthenticatedKey -from ..key import Passphrase, PasswordRetriesExceeded, bin_to_hex -from ..key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError class TestKey: diff --git a/src/borg/testsuite/nonces.py b/src/borg/testsuite/nonces.py index d88d260a..bfdc3cc7 100644 --- a/src/borg/testsuite/nonces.py +++ b/src/borg/testsuite/nonces.py @@ -2,13 +2,12 @@ import os.path import pytest +from ..crypto import nonces +from ..crypto.nonces import NonceManager +from ..crypto.key import bin_to_hex from ..helpers import get_security_dir -from ..key import bin_to_hex -from ..nonces import NonceManager from ..remote import InvalidRPCMethod -from .. import nonces # for monkey patching NONCE_SPACE_RESERVATION - class TestNonceManager: diff --git a/src/borg/testsuite/upgrader.py b/src/borg/testsuite/upgrader.py index 622c15c5..982d01e8 100644 --- a/src/borg/testsuite/upgrader.py +++ b/src/borg/testsuite/upgrader.py @@ -10,9 +10,9 @@ except ImportError: attic = None from ..constants import * # NOQA +from ..crypto.key import KeyfileKey from ..upgrader import AtticRepositoryUpgrader, AtticKeyfileKey from ..helpers import get_keys_dir -from ..key import KeyfileKey from ..repository import Repository from . import are_hardlinks_supported diff --git a/src/borg/upgrader.py b/src/borg/upgrader.py index 28473e07..6c88412f 100644 --- a/src/borg/upgrader.py +++ b/src/borg/upgrader.py @@ -3,13 +3,13 @@ import os import shutil import time +from .crypto.key import KeyfileKey, KeyfileNotFoundError from .constants import REPOSITORY_README -from .helpers import get_home_dir, get_keys_dir, get_cache_dir from .helpers import ProgressIndicatorPercent -from .key import KeyfileKey, KeyfileNotFoundError +from .helpers import get_home_dir, get_keys_dir, get_cache_dir from .locking import Lock -from .repository import Repository, MAGIC from .logger import create_logger +from .repository import Repository, MAGIC logger = create_logger(__name__) From d32eadeb3817d622a6e34c58b896c5aeeba02f10 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 2 May 2017 19:20:19 +0200 Subject: [PATCH 0828/1387] gitignore: complete paths for src/ excludes --- .gitignore | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 0fc91e1c..11bcaade 100644 --- a/.gitignore +++ b/.gitignore @@ -4,23 +4,23 @@ build dist borg-env .tox -hashindex.c -chunker.c -compress.c -low_level.c -item.c +src/borg/compress.c +src/borg/crypto/low_level.c +src/borg/hashindex.c +src/borg/item.c +src/borg/algorithms/chunker.c src/borg/algorithms/crc32.c src/borg/platform/darwin.c src/borg/platform/freebsd.c src/borg/platform/linux.c src/borg/platform/posix.c +src/borg/_version.py *.egg-info *.pyc *.pyo *.so .idea/ .cache/ -src/borg/_version.py borg.build/ borg.dist/ borg.exe From 3e24ed4035af57c5e071b290ac10bb05dd085ae7 Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Wed, 3 May 2017 23:17:26 -0600 Subject: [PATCH 0829/1387] Start fakeroot faked in debug mode --- .travis/run.sh | 2 +- Vagrantfile | 2 +- scripts/faked-debug.sh | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100755 scripts/faked-debug.sh diff --git a/.travis/run.sh b/.travis/run.sh index 7c1e847c..b32de444 100755 --- a/.travis/run.sh +++ b/.travis/run.sh @@ -19,5 +19,5 @@ if [[ "$(uname -s)" == "Darwin" ]]; then # no fakeroot on OS X sudo tox -e $TOXENV -r else - fakeroot -u tox -r + fakeroot -f scripts/faked-debug.sh -u tox -r fi diff --git a/Vagrantfile b/Vagrantfile index 0f9ddc1e..e42f7333 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -318,7 +318,7 @@ def run_tests(boxname) # otherwise: just use the system python if which fakeroot 2> /dev/null; then echo "Running tox WITH fakeroot -u" - fakeroot -u tox --skip-missing-interpreters + fakeroot -f scripts/faked-debug.sh -u tox --skip-missing-interpreters else echo "Running tox WITHOUT fakeroot -u" tox --skip-missing-interpreters diff --git a/scripts/faked-debug.sh b/scripts/faked-debug.sh new file mode 100755 index 00000000..924193c6 --- /dev/null +++ b/scripts/faked-debug.sh @@ -0,0 +1,6 @@ +#!/bin/sh +if which faked; then + faked --debug "$@" +else + faked-sysv --debug "$@" +fi From ba5d6693f9823d9b35307e1d74c283c00cbc4dcc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 5 May 2017 14:38:21 +0200 Subject: [PATCH 0830/1387] --json: fix encryption[mode] not being the cmdline name --- src/borg/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 57dfefa6..30fdd2b9 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1942,7 +1942,7 @@ def basic_json_data(manifest, *, cache=None, extra=None): data.update({ 'repository': BorgJsonEncoder().default(manifest.repository), 'encryption': { - 'mode': key.NAME, + 'mode': key.ARG_NAME, }, }) data['repository']['last_modified'] = format_time(to_localtime(manifest.last_timestamp.replace(tzinfo=timezone.utc))) From c99de523ea01aef3d12d24a89e4f2d1133029abb Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 5 May 2017 14:42:41 +0200 Subject: [PATCH 0831/1387] remove duplicated setup.py line --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index e5bae3fe..274e740d 100644 --- a/setup.py +++ b/setup.py @@ -578,7 +578,6 @@ ext_modules = [] if not on_rtd: ext_modules += [ Extension('borg.compress', [compress_source], libraries=['lz4'], include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), - Extension('borg.crypto', [crypto_ll_source], libraries=crypto_libraries, include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), Extension('borg.crypto.low_level', [crypto_ll_source], libraries=crypto_libraries, include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), Extension('borg.hashindex', [hashindex_source]), Extension('borg.item', [item_source]), From 4d9fd6d13fcb835fed3bfc596dffa34b11f2868a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 5 May 2017 14:49:23 +0200 Subject: [PATCH 0832/1387] fix --exclude and --exclude-from recursing into directories --- src/borg/patterns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/patterns.py b/src/borg/patterns.py index d93c4166..924b51fe 100644 --- a/src/borg/patterns.py +++ b/src/borg/patterns.py @@ -347,8 +347,8 @@ def parse_pattern(pattern, fallback=FnmatchPattern, recurse_dir=True): def parse_exclude_pattern(pattern_str, fallback=FnmatchPattern): """Read pattern from string and return an instance of the appropriate implementation class. """ - epattern_obj = parse_pattern(pattern_str, fallback) - return CmdTuple(epattern_obj, IECommand.Exclude) + epattern_obj = parse_pattern(pattern_str, fallback, recurse_dir=False) + return CmdTuple(epattern_obj, IECommand.ExcludeNoRecurse) def parse_inclexcl_command(cmd_line_str, fallback=ShellPattern): From bd8186f901ca3bb659cf0ffbe6dbd98950498a7c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 5 May 2017 15:12:23 +0200 Subject: [PATCH 0833/1387] make --progress a common option everything that touches the repository can take a long time and display progress information, from compaction to replaying segments. --- src/borg/archiver.py | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 9556c2da..4de52e64 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1900,6 +1900,8 @@ class Archiver: action='append', metavar='TOPIC', default=[], help='enable TOPIC debugging (can be specified multiple times). ' 'The logger path is borg.debug. if TOPIC is not fully qualified.') + common_group.add_argument('-p', '--progress', dest='progress', action='store_true', + help='show progress information') common_group.add_argument('--log-json', dest='log_json', action='store_true', help='Output one JSON object per log line instead of formatted text.') common_group.add_argument('--lock-wait', dest='lock_wait', type=int, metavar='N', default=1, @@ -2105,9 +2107,6 @@ class Archiver: subparser.add_argument('--save-space', dest='save_space', action='store_true', default=False, help='work slower, but using less space') - subparser.add_argument('-p', '--progress', dest='progress', - action='store_true', default=False, - help="""show progress display while checking""") self.add_archives_filters_args(subparser) subparser = subparsers.add_parser('key', parents=[common_parser], add_help=False, @@ -2256,6 +2255,10 @@ class Archiver: is used to determine changed files quickly uses absolute filenames. If this is not possible, consider creating a bind mount to a stable location. + The --progress option shows (from left to right) Original, Compressed and Deduplicated + (O, C and D, respectively), then the Number of files (N) processed so far, followed by + the currently processed path. + See the output of the "borg help patterns" command for more help on exclude patterns. See the output of the "borg help placeholders" command for more help on placeholders. @@ -2329,11 +2332,6 @@ class Archiver: subparser.add_argument('-s', '--stats', dest='stats', action='store_true', default=False, help='print statistics for the created archive') - subparser.add_argument('-p', '--progress', dest='progress', - action='store_true', default=False, - help='show progress display while creating the archive, showing Original, ' - 'Compressed and Deduplicated sizes, followed by the Number of files seen ' - 'and the path being processed, default: %(default)s') subparser.add_argument('--list', dest='output_list', action='store_true', default=False, help='output verbose list of items (files, dirs, ...)') @@ -2425,6 +2423,9 @@ class Archiver: By using ``--dry-run``, you can do all extraction steps except actually writing the output data: reading metadata and data chunks from the repo, checking the hash/hmac, decrypting, decompressing. + + ``--progress`` can be slower than no progress display, since it makes one additional + pass over the archive metadata. """) subparser = subparsers.add_parser('extract', parents=[common_parser], add_help=False, description=self.do_extract.__doc__, @@ -2432,9 +2433,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='extract archive contents') subparser.set_defaults(func=self.do_extract) - subparser.add_argument('-p', '--progress', dest='progress', - action='store_true', default=False, - help='show progress while extracting (may be slower)') subparser.add_argument('--list', dest='output_list', action='store_true', default=False, help='output verbose list of items (files, dirs, ...)') @@ -2563,9 +2561,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='delete archive') subparser.set_defaults(func=self.do_delete) - subparser.add_argument('-p', '--progress', dest='progress', - action='store_true', default=False, - help="""show progress display while deleting a single archive""") subparser.add_argument('-s', '--stats', dest='stats', action='store_true', default=False, help='print statistics for the deleted archive') @@ -2808,9 +2803,6 @@ class Archiver: subparser.add_argument('--force', dest='forced', action='store_true', default=False, help='force pruning of corrupted archives') - subparser.add_argument('-p', '--progress', dest='progress', - action='store_true', default=False, - help='show progress display while deleting archives') subparser.add_argument('-s', '--stats', dest='stats', action='store_true', default=False, help='print statistics for the deleted archive') @@ -2930,9 +2922,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='upgrade repository format') subparser.set_defaults(func=self.do_upgrade) - subparser.add_argument('-p', '--progress', dest='progress', - action='store_true', default=False, - help="""show progress display while upgrading the repository""") subparser.add_argument('-n', '--dry-run', dest='dry_run', default=False, action='store_true', help='do not change repository') @@ -2999,9 +2988,6 @@ class Archiver: help='output verbose list of items (files, dirs, ...)') subparser.add_argument('--filter', dest='output_filter', metavar='STATUSCHARS', help='only display items with the given status characters') - subparser.add_argument('-p', '--progress', dest='progress', - action='store_true', default=False, - help='show progress display while recreating archives') subparser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true', default=False, help='do not change anything') From 2a22f93e440de6396f4ad4baf835b6adba153a16 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 5 May 2017 15:49:56 +0200 Subject: [PATCH 0834/1387] list: JSON lines output for archive contents --- docs/internals/frontends.rst | 39 ++++++++-------------------------- src/borg/archiver.py | 24 +++++++++++++++------ src/borg/helpers.py | 29 +++++-------------------- src/borg/testsuite/archiver.py | 10 ++++----- 4 files changed, 36 insertions(+), 66 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index da6245a2..f082f4ef 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -392,39 +392,18 @@ The same archive with more information (``borg info --last 1 --json``):: .. rubric:: File listings -Listing the contents of an archive can produce *a lot* of JSON. Each item (file, directory, ...) is described -by one object in the *items* array of the :ref:`borg_list` output. Refer to the *borg list* documentation for -the available keys and their meaning. +Listing the contents of an archive can produce *a lot* of JSON. Since many JSON implementations +don't support a streaming mode of operation, which is pretty much required to deal with this amount of +JSON, output is generated in the `JSON lines `_ format, which is simply +a number of JSON objects separated by new lines. + +Each item (file, directory, ...) is described by one object in the :ref:`borg_list` output. +Refer to the *borg list* documentation for the available keys and their meaning. Example (excerpt):: - { - "encryption": { - "mode": "repokey" - }, - "repository": { - "id": "0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23", - "last_modified": "Mon, 2017-02-27 21:21:58", - "location": "/home/user/repository" - }, - "items": [ - { - "type": "d", - "mode": "drwxr-xr-x", - "user": "user", - "group": "user", - "uid": 1000, - "gid": 1000, - "path": "linux", - "healthy": true, - "source": "", - "linktarget": "", - "flags": null, - "isomtime": "Sat, 2016-05-07 19:46:01", - "size": 0 - } - ] - } + {"type": "d", "mode": "drwxr-xr-x", "user": "user", "group": "user", "uid": 1000, "gid": 1000, "path": "linux", "healthy": true, "source": "", "linktarget": "", "flags": null, "isomtime": "Sat, 2016-05-07 19:46:01", "size": 0} + {"type": "d", "mode": "drwxr-xr-x", "user": "user", "group": "user", "uid": 1000, "gid": 1000, "path": "linux/baz", "healthy": true, "source": "", "linktarget": "", "flags": null, "isomtime": "Sat, 2016-05-07 19:46:01", "size": 0} .. _msgid: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 9556c2da..b8e0e8b2 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1048,8 +1048,14 @@ class Archiver: write = sys.stdout.buffer.write if args.location.archive: + if args.json: + self.print_error('The --json option is only valid for listing archives, not archive contents.') + return self.exit_code return self._list_archive(args, repository, manifest, key, write) else: + if args.json_lines: + self.print_error('The --json-lines option is only valid for listing archive contents, not archives.') + return self.exit_code return self._list_repository(args, manifest, write) def _list_archive(self, args, repository, manifest, key, write): @@ -1065,11 +1071,9 @@ class Archiver: archive = Archive(repository, key, manifest, args.location.archive, cache=cache, consider_part_files=args.consider_part_files) - formatter = ItemFormatter(archive, format, json=args.json) - write(safe_encode(formatter.begin())) + formatter = ItemFormatter(archive, format, json_lines=args.json_lines) for item in archive.iter_items(lambda item: matcher.match(item.path)): write(safe_encode(formatter.format_item(item))) - write(safe_encode(formatter.end())) # Only load the cache if it will be used if ItemFormatter.format_needs_cache(format): @@ -2616,9 +2620,17 @@ class Archiver: help="""specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")""") subparser.add_argument('--json', action='store_true', - help='format output as JSON. The form of --format is ignored, but keys used in it ' - 'are added to the JSON output. Some keys are always present. Note: JSON can only ' - 'represent text. A "bpath" key is therefore not available.') + help='Only valid for listing archives. Format output as JSON. ' + 'The form of --format is ignored, ' + 'but keys used in it are added to the JSON output. ' + 'Some keys are always present. Note: JSON can only represent text. ' + 'A "barchive" key is therefore not available.') + subparser.add_argument('--json-lines', action='store_true', + help='Only valid for listing archive contents. Format output as JSON Lines. ' + 'The form of --format is ignored, ' + 'but keys used in it are added to the JSON output. ' + 'Some keys are always present. Note: JSON can only represent text. ' + 'A "bpath" key is therefore not available.') subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='', type=location_validator(), help='repository/archive to list contents of') diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 57dfefa6..0b69125e 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1505,9 +1505,9 @@ class ItemFormatter(BaseFormatter): format_keys = {f[1] for f in Formatter().parse(format)} return any(key in cls.KEYS_REQUIRING_CACHE for key in format_keys) - def __init__(self, archive, format, *, json=False): + def __init__(self, archive, format, *, json_lines=False): self.archive = archive - self.json = json + self.json_lines = json_lines static_keys = { 'archivename': archive.name, 'archiveid': archive.fpr, @@ -1532,33 +1532,14 @@ class ItemFormatter(BaseFormatter): for hash_function in hashlib.algorithms_guaranteed: self.add_key(hash_function, partial(self.hash_item, hash_function)) self.used_call_keys = set(self.call_keys) & self.format_keys - if self.json: + if self.json_lines: self.item_data = {} self.format_item = self.format_item_json - self.first = True else: self.item_data = static_keys - def begin(self): - if not self.json: - return '' - begin = json_dump(basic_json_data(self.archive.manifest)) - begin, _, _ = begin.rpartition('\n}') # remove last closing brace, we want to extend the object - begin += ',\n' - begin += ' "items": [\n' - return begin - - def end(self): - if not self.json: - return '' - return "]}" - def format_item_json(self, item): - if self.first: - self.first = False - return json.dumps(self.get_item_data(item)) - else: - return ',' + json.dumps(self.get_item_data(item)) + return json.dumps(self.get_item_data(item)) + '\n' def add_key(self, key, callable_with_item): self.call_keys[key] = callable_with_item @@ -1585,7 +1566,7 @@ class ItemFormatter(BaseFormatter): item_data['uid'] = item.uid item_data['gid'] = item.gid item_data['path'] = remove_surrogates(item.path) - if self.json: + if self.json_lines: item_data['healthy'] = 'chunks_healthy' not in item else: item_data['bpath'] = item.path diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index d5dffe8d..5c7a1e1f 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1615,17 +1615,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert list_repo['encryption']['mode'] == 'repokey' assert 'keyfile' not in list_repo['encryption'] - list_archive = json.loads(self.cmd('list', '--json', self.repository_location + '::test')) - assert list_repo['repository'] == list_archive['repository'] - items = list_archive['items'] + list_archive = self.cmd('list', '--json-lines', self.repository_location + '::test') + items = [json.loads(s) for s in list_archive.splitlines()] assert len(items) == 2 file1 = items[1] assert file1['path'] == 'input/file1' assert file1['size'] == 81920 - list_archive = json.loads(self.cmd('list', '--json', '--format={sha256}', self.repository_location + '::test')) - assert list_repo['repository'] == list_archive['repository'] - items = list_archive['items'] + list_archive = self.cmd('list', '--json-lines', '--format={sha256}', self.repository_location + '::test') + items = [json.loads(s) for s in list_archive.splitlines()] assert len(items) == 2 file1 = items[1] assert file1['path'] == 'input/file1' From 1418aeadad5b4a0d84106040a3debbeb330bb384 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 5 May 2017 15:52:34 +0200 Subject: [PATCH 0835/1387] docs/frontends: use headlines - you can link to them --- docs/internals/frontends.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index f082f4ef..4000bede 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -270,10 +270,12 @@ Example *borg info* output:: "last_modified": "Mon, 2017-02-27 21:21:58", "location": "/home/user/testrepo" }, - "security_dir": "/home/user/.config/borg/security/0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23" + "security_dir": "/home/user/.config/borg/security/0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23", + "archives": [] } -.. rubric:: Archive formats +Archive formats ++++++++++++++++ :ref:`borg_info` uses an extended format for archives, which is more expensive to retrieve, while :ref:`borg_list` uses a simpler format that is faster to retrieve. Either return archives in an @@ -390,7 +392,8 @@ The same archive with more information (``borg info --last 1 --json``):: } } -.. rubric:: File listings +File listings ++++++++++++++ Listing the contents of an archive can produce *a lot* of JSON. Since many JSON implementations don't support a streaming mode of operation, which is pretty much required to deal with this amount of @@ -400,7 +403,7 @@ a number of JSON objects separated by new lines. Each item (file, directory, ...) is described by one object in the :ref:`borg_list` output. Refer to the *borg list* documentation for the available keys and their meaning. -Example (excerpt):: +Example (excerpt) of ``borg list --json-lines``:: {"type": "d", "mode": "drwxr-xr-x", "user": "user", "group": "user", "uid": 1000, "gid": 1000, "path": "linux", "healthy": true, "source": "", "linktarget": "", "flags": null, "isomtime": "Sat, 2016-05-07 19:46:01", "size": 0} {"type": "d", "mode": "drwxr-xr-x", "user": "user", "group": "user", "uid": 1000, "gid": 1000, "path": "linux/baz", "healthy": true, "source": "", "linktarget": "", "flags": null, "isomtime": "Sat, 2016-05-07 19:46:01", "size": 0} From a7190976114f1d421e28d68a8c0030a99663110d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 5 May 2017 16:05:12 +0200 Subject: [PATCH 0836/1387] add test case for --log-json --- src/borg/testsuite/archiver.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 5c7a1e1f..63bb4c00 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1629,6 +1629,26 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert file1['path'] == 'input/file1' assert file1['sha256'] == 'b2915eb69f260d8d3c25249195f2c8f4f716ea82ec760ae929732c0262442b2b' + def test_log_json(self): + self.create_test_files() + self.cmd('init', '--encryption=repokey', self.repository_location) + log = self.cmd('create', '--log-json', self.repository_location + '::test', 'input', '--list', '--debug') + print(log) + messages = {} # type -> message, one of each kind + for line in log.splitlines(): + msg = json.loads(line) + messages[msg['type']] = msg + + file_status = messages['file_status'] + assert 'status' in file_status + assert file_status['path'].startswith('input') + + log_message = messages['log_message'] + assert isinstance(log_message['time'], float) + assert log_message['levelname'] == 'DEBUG' # there should only be DEBUG messages + assert log_message['name'].startswith('borg.') + assert isinstance(log_message['message'], str) + def _get_sizes(self, compression, compressible, size=10000): if compressible: contents = b'X' * size From be9a94c22c446b67bf62dfe6493bee74cf20950d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 7 May 2017 22:04:25 +0200 Subject: [PATCH 0837/1387] list: add test for handling of --json/--json-lines --- src/borg/archiver.py | 2 +- src/borg/testsuite/archiver.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index b8e0e8b2..61e38c16 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2620,7 +2620,7 @@ class Archiver: help="""specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")""") subparser.add_argument('--json', action='store_true', - help='Only valid for listing archives. Format output as JSON. ' + help='Only valid for listing repository contents. Format output as JSON. ' 'The form of --format is ignored, ' 'but keys used in it are added to the JSON output. ' 'Some keys are always present. Note: JSON can only represent text. ' diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 63bb4c00..9ee4523c 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1629,11 +1629,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert file1['path'] == 'input/file1' assert file1['sha256'] == 'b2915eb69f260d8d3c25249195f2c8f4f716ea82ec760ae929732c0262442b2b' + def test_list_json_args(self): + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('list', '--json-lines', self.repository_location, exit_code=2) + self.cmd('list', '--json', self.repository_location + '::archive', exit_code=2) + def test_log_json(self): self.create_test_files() self.cmd('init', '--encryption=repokey', self.repository_location) log = self.cmd('create', '--log-json', self.repository_location + '::test', 'input', '--list', '--debug') - print(log) messages = {} # type -> message, one of each kind for line in log.splitlines(): msg = json.loads(line) From d964101eb53a4f062e218771eeb6a071c1b97fb1 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 5 May 2017 17:26:24 +0200 Subject: [PATCH 0838/1387] consider repokey w/o passphrase == unencrypted --- docs/changes.rst | 9 +++++++++ src/borg/cache.py | 2 +- src/borg/crypto/key.py | 8 ++++++++ src/borg/testsuite/archiver.py | 23 +++++++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index ec226a3a..113f0909 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -128,6 +128,15 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= +Version 1.1.0b6 (unreleased) +---------------------------- + +Compatibility notes: + +- Repositories in a repokey mode with a blank passphrase are now treated + as unencrypted repositories for security checks + (e.g. BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK). + Version 1.1.0b5 (2017-04-30) ---------------------------- diff --git a/src/borg/cache.py b/src/borg/cache.py index 8f428f8c..aeb9d3d4 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -130,7 +130,7 @@ class SecurityManager: self.save(manifest, key, cache) def assert_access_unknown(self, warn_if_unencrypted, key): - if warn_if_unencrypted and isinstance(key, PlaintextKey) and not self.known(): + if warn_if_unencrypted and not key.passphrase_protected and not self.known(): msg = ("Warning: Attempting to access a previously unknown unencrypted repository!\n" + "Do you want to continue? [yN] ") if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index b2f66da9..9469be29 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -234,6 +234,7 @@ class PlaintextKey(KeyBase): STORAGE = KeyBlobStorage.NO_STORAGE chunk_seed = 0 + passphrase_protected = False def __init__(self, repository): super().__init__(repository) @@ -329,6 +330,8 @@ class AESKeyBase(KeyBase): MAC = hmac_sha256 + passphrase_protected = True + def encrypt(self, chunk): data = self.compressor.compress(chunk) self.nonce_manager.ensure_reservation(num_aes_blocks(len(data))) @@ -700,6 +703,10 @@ class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase): return self.repository def load(self, target, passphrase): + # While the repository is encrypted, we consider a repokey repository with a blank + # passphrase an unencrypted repository. + self.passphrase_protected = passphrase != '' + # what we get in target is just a repo location, but we already have the repo obj: target = self.repository key_data = target.load_key() @@ -710,6 +717,7 @@ class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase): return success def save(self, target, passphrase): + self.passphrase_protected = passphrase != '' key_data = self._save(passphrase) key_data = key_data.encode('utf-8') # remote repo: msgpack issue #99, giving bytes target.save_key(key_data) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index d5dffe8d..64107c5b 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -607,6 +607,29 @@ class ArchiverTestCase(ArchiverTestCaseBase): with pytest.raises(Cache.RepositoryAccessAborted): self.cmd('create', self.repository_location + '_encrypted::test.2', 'input') + def test_repository_swap_detection_repokey_blank_passphrase(self): + # Check that a repokey repo with a blank passphrase is considered like a plaintext repo. + self.create_test_files() + # User initializes her repository with her passphrase + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + # Attacker replaces it with her own repository, which is encrypted but has no passphrase set + shutil.rmtree(self.repository_path) + with environment_variable(BORG_PASSPHRASE=''): + self.cmd('init', '--encryption=repokey', self.repository_location) + # Delete cache & security database, AKA switch to user perspective + self.cmd('delete', '--cache-only', self.repository_location) + repository_id = bin_to_hex(self._extract_repository_id(self.repository_path)) + shutil.rmtree(get_security_dir(repository_id)) + with environment_variable(BORG_PASSPHRASE=None): + # This is the part were the user would be tricked, e.g. she assumes that BORG_PASSPHRASE + # is set, while it isn't. Previously this raised no warning, + # since the repository is, technically, encrypted. + if self.FORK_DEFAULT: + self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR) + else: + self.assert_raises(Cache.CacheInitAbortedError, lambda: self.cmd('create', self.repository_location + '::test.2', 'input')) + def test_repository_move(self): self.cmd('init', '--encryption=repokey', self.repository_location) repository_id = bin_to_hex(self._extract_repository_id(self.repository_path)) From 1daab244c6ceb5ed722ba3447fc1f97f73fec1d4 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 7 May 2017 22:21:40 +0200 Subject: [PATCH 0839/1387] testsuite.archiver: normalise pytest.raises vs. assert_raises --- src/borg/testsuite/archiver.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 64107c5b..baf1c60c 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -560,7 +560,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): if self.FORK_DEFAULT: self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR) else: - self.assert_raises(Cache.EncryptionMethodMismatch, lambda: self.cmd('create', self.repository_location + '::test.2', 'input')) + with pytest.raises(Cache.EncryptionMethodMismatch): + self.cmd('create', self.repository_location + '::test.2', 'input') def test_repository_swap_detection2(self): self.create_test_files() @@ -573,7 +574,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): if self.FORK_DEFAULT: self.cmd('create', self.repository_location + '_encrypted::test.2', 'input', exit_code=EXIT_ERROR) else: - self.assert_raises(Cache.RepositoryAccessAborted, lambda: self.cmd('create', self.repository_location + '_encrypted::test.2', 'input')) + with pytest.raises(Cache.RepositoryAccessAborted): + self.cmd('create', self.repository_location + '_encrypted::test.2', 'input') def test_repository_swap_detection_no_cache(self): self.create_test_files() @@ -589,7 +591,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): if self.FORK_DEFAULT: self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR) else: - self.assert_raises(Cache.EncryptionMethodMismatch, lambda: self.cmd('create', self.repository_location + '::test.2', 'input')) + with pytest.raises(Cache.EncryptionMethodMismatch): + self.cmd('create', self.repository_location + '::test.2', 'input') def test_repository_swap_detection2_no_cache(self): self.create_test_files() @@ -628,7 +631,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): if self.FORK_DEFAULT: self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR) else: - self.assert_raises(Cache.CacheInitAbortedError, lambda: self.cmd('create', self.repository_location + '::test.2', 'input')) + with pytest.raises(Cache.CacheInitAbortedError): + self.cmd('create', self.repository_location + '::test.2', 'input') def test_repository_move(self): self.cmd('init', '--encryption=repokey', self.repository_location) @@ -2255,7 +2259,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): if self.FORK_DEFAULT: self.cmd('key', 'import', self.repository_location, export_file, exit_code=2) else: - self.assert_raises(NotABorgKeyFile, lambda: self.cmd('key', 'import', self.repository_location, export_file)) + with pytest.raises(NotABorgKeyFile): + self.cmd('key', 'import', self.repository_location, export_file) with open(export_file, 'w') as fd: fd.write('BORG_KEY a0a0a0\n') @@ -2263,7 +2268,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): if self.FORK_DEFAULT: self.cmd('key', 'import', self.repository_location, export_file, exit_code=2) else: - self.assert_raises(RepoIdMismatch, lambda: self.cmd('key', 'import', self.repository_location, export_file)) + with pytest.raises(RepoIdMismatch): + self.cmd('key', 'import', self.repository_location, export_file) def test_key_export_paperkey(self): repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239' @@ -2676,13 +2682,13 @@ class RemoteArchiverTestCase(ArchiverTestCase): self.cmd('init', '--encryption=repokey', self.repository_location) # restricted to repo directory itself, fail for other directories with same prefix: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', self.repository_path]): - self.assert_raises(PathNotAllowed, - lambda: self.cmd('init', '--encryption=repokey', self.repository_location + '_0')) + with pytest.raises(PathNotAllowed): + self.cmd('init', '--encryption=repokey', self.repository_location + '_0') # restricted to a completely different path: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo']): - self.assert_raises(PathNotAllowed, - lambda: self.cmd('init', '--encryption=repokey', self.repository_location + '_1')) + with pytest.raises(PathNotAllowed): + self.cmd('init', '--encryption=repokey', self.repository_location + '_1') path_prefix = os.path.dirname(self.repository_path) # restrict to repo directory's parent directory: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', path_prefix]): From 6cd7d415ca501284a12bbb1462b562f453b3205d Mon Sep 17 00:00:00 2001 From: enkore Date: Tue, 9 May 2017 21:30:14 +0200 Subject: [PATCH 0840/1387] hashindex: Use Python I/O (#2496) - Preparation for #1688 / #1101 - Support hash indices >2 GB - Better error reporting --- src/borg/_hashindex.c | 259 +++++++++++++++++++++++++++-------------- src/borg/hashindex.pyx | 19 ++- src/borg/repository.py | 8 +- 3 files changed, 181 insertions(+), 105 deletions(-) diff --git a/src/borg/_hashindex.c b/src/borg/_hashindex.c index 51290c5a..289457fe 100644 --- a/src/borg/_hashindex.c +++ b/src/borg/_hashindex.c @@ -1,3 +1,5 @@ +#include + #include #include #include @@ -54,6 +56,8 @@ typedef struct { int lower_limit; int upper_limit; int min_empty; + /* buckets may be backed by a Python buffer. If buckets_buffer.buf is NULL then this is not used. */ + Py_buffer buckets_buffer; } HashIndex; /* prime (or w/ big prime factors) hash table sizes @@ -102,8 +106,8 @@ static int hash_sizes[] = { #define EPRINTF(msg, ...) fprintf(stderr, "hashindex: " msg "(%s)\n", ##__VA_ARGS__, strerror(errno)) #define EPRINTF_PATH(path, msg, ...) fprintf(stderr, "hashindex: %s: " msg " (%s)\n", path, ##__VA_ARGS__, strerror(errno)) -static HashIndex *hashindex_read(const char *path); -static int hashindex_write(HashIndex *index, const char *path); +static HashIndex *hashindex_read(PyObject *file_py); +static void hashindex_write(HashIndex *index, PyObject *file_py); static HashIndex *hashindex_init(int capacity, int key_size, int value_size); static const void *hashindex_get(HashIndex *index, const void *key); static int hashindex_set(HashIndex *index, const void *key, const void *value); @@ -113,6 +117,16 @@ static void *hashindex_next_key(HashIndex *index, const void *key); /* Private API */ static void hashindex_free(HashIndex *index); +static void +hashindex_free_buckets(HashIndex *index) +{ + if(index->buckets_buffer.buf) { + PyBuffer_Release(&index->buckets_buffer); + } else { + free(index->buckets); + } +} + static int hashindex_index(HashIndex *index, const void *key) { @@ -171,7 +185,7 @@ hashindex_resize(HashIndex *index, int capacity) return 0; } } - free(index->buckets); + hashindex_free_buckets(index); index->buckets = new->buckets; index->num_buckets = new->num_buckets; index->num_empty = index->num_buckets - index->num_entries; @@ -248,99 +262,146 @@ count_empty(HashIndex *index) } /* Public API */ + static HashIndex * -hashindex_read(const char *path) +hashindex_read(PyObject *file_py) { - FILE *fd; - off_t length, buckets_length, bytes_read; - HashHeader header; + Py_ssize_t length, buckets_length, bytes_read; + Py_buffer header_buffer; + PyObject *header_bytes, *length_object, *bucket_bytes; + HashHeader *header; HashIndex *index = NULL; - if((fd = fopen(path, "rb")) == NULL) { - EPRINTF_PATH(path, "fopen for reading failed"); - return NULL; + header_bytes = PyObject_CallMethod(file_py, "read", "n", (Py_ssize_t)sizeof(HashHeader)); + if(!header_bytes) { + assert(PyErr_Occurred()); + goto fail; + } + + bytes_read = PyBytes_Size(header_bytes); + if(PyErr_Occurred()) { + /* TypeError, not a bytes() object */ + goto fail_decref_header; } - bytes_read = fread(&header, 1, sizeof(HashHeader), fd); if(bytes_read != sizeof(HashHeader)) { - if(ferror(fd)) { - EPRINTF_PATH(path, "fread header failed (expected %ju, got %ju)", - (uintmax_t) sizeof(HashHeader), (uintmax_t) bytes_read); - } - else { - EPRINTF_MSG_PATH(path, "fread header failed (expected %ju, got %ju)", - (uintmax_t) sizeof(HashHeader), (uintmax_t) bytes_read); - } - goto fail; + /* Truncated file */ + /* Note: %zd is the format for Py_ssize_t, %zu is for size_t */ + PyErr_Format(PyExc_ValueError, "Could not read header (expected %zu, but read %zd bytes)", + sizeof(HashHeader), bytes_read); + goto fail_decref_header; } - if(fseek(fd, 0, SEEK_END) < 0) { - EPRINTF_PATH(path, "fseek failed"); - goto fail; + + /* Find length of file */ + length_object = PyObject_CallMethod(file_py, "seek", "ni", (Py_ssize_t)0, SEEK_END); + if(PyErr_Occurred()) { + goto fail_decref_header; } - if((length = ftell(fd)) < 0) { - EPRINTF_PATH(path, "ftell failed"); - goto fail; + length = PyNumber_AsSsize_t(length_object, PyExc_OverflowError); + Py_DECREF(length_object); + if(PyErr_Occurred()) { + /* This shouldn't generally happen; but can if seek() returns something that's not a number */ + goto fail_decref_header; } - if(fseek(fd, sizeof(HashHeader), SEEK_SET) < 0) { - EPRINTF_PATH(path, "fseek failed"); - goto fail; - } - if(memcmp(header.magic, MAGIC, MAGIC_LEN)) { - EPRINTF_MSG_PATH(path, "Unknown MAGIC in header"); - goto fail; - } - buckets_length = (off_t)_le32toh(header.num_buckets) * (header.key_size + header.value_size); - if((size_t) length != sizeof(HashHeader) + buckets_length) { - EPRINTF_MSG_PATH(path, "Incorrect file length (expected %ju, got %ju)", - (uintmax_t) sizeof(HashHeader) + buckets_length, (uintmax_t) length); - goto fail; + + Py_XDECREF(PyObject_CallMethod(file_py, "seek", "ni", (Py_ssize_t)sizeof(HashHeader), SEEK_SET)); + if(PyErr_Occurred()) { + goto fail_decref_header; } + + /* Set up the in-memory header */ if(!(index = malloc(sizeof(HashIndex)))) { - EPRINTF_PATH(path, "malloc header failed"); - goto fail; + PyErr_NoMemory(); + goto fail_decref_header; } - if(!(index->buckets = malloc(buckets_length))) { - EPRINTF_PATH(path, "malloc buckets failed"); - free(index); - index = NULL; - goto fail; + + PyObject_GetBuffer(header_bytes, &header_buffer, PyBUF_SIMPLE); + if(PyErr_Occurred()) { + goto fail_free_index; } - bytes_read = fread(index->buckets, 1, buckets_length, fd); - if(bytes_read != buckets_length) { - if(ferror(fd)) { - EPRINTF_PATH(path, "fread buckets failed (expected %ju, got %ju)", - (uintmax_t) buckets_length, (uintmax_t) bytes_read); - } - else { - EPRINTF_MSG_PATH(path, "fread buckets failed (expected %ju, got %ju)", - (uintmax_t) buckets_length, (uintmax_t) bytes_read); - } - free(index->buckets); - free(index); - index = NULL; - goto fail; + + header = (HashHeader*) header_buffer.buf; + if(memcmp(header->magic, MAGIC, MAGIC_LEN)) { + PyErr_Format(PyExc_ValueError, "Unknown MAGIC in header"); + goto fail_release_header_buffer; } - index->num_entries = _le32toh(header.num_entries); - index->num_buckets = _le32toh(header.num_buckets); - index->key_size = header.key_size; - index->value_size = header.value_size; + + buckets_length = (Py_ssize_t)_le32toh(header->num_buckets) * (header->key_size + header->value_size); + if((Py_ssize_t)length != (Py_ssize_t)sizeof(HashHeader) + buckets_length) { + PyErr_Format(PyExc_ValueError, "Incorrect file length (expected %zd, got %zd)", + sizeof(HashHeader) + buckets_length, length); + goto fail_release_header_buffer; + } + + index->num_entries = _le32toh(header->num_entries); + index->num_buckets = _le32toh(header->num_buckets); + index->key_size = header->key_size; + index->value_size = header->value_size; index->bucket_size = index->key_size + index->value_size; index->lower_limit = get_lower_limit(index->num_buckets); index->upper_limit = get_upper_limit(index->num_buckets); + + /* + * For indices read from disk we don't malloc() the buckets ourselves, + * we have them backed by a Python bytes() object instead, and go through + * Python I/O. + * + * Note: Issuing read(buckets_length) is okay here, because buffered readers + * will issue multiple underlying reads if necessary. This supports indices + * >2 GB on Linux. We also compare lengths later. + */ + bucket_bytes = PyObject_CallMethod(file_py, "read", "n", buckets_length); + if(!bucket_bytes) { + assert(PyErr_Occurred()); + goto fail_release_header_buffer; + } + bytes_read = PyBytes_Size(bucket_bytes); + if(PyErr_Occurred()) { + /* TypeError, not a bytes() object */ + goto fail_decref_buckets; + } + if(bytes_read != buckets_length) { + PyErr_Format(PyExc_ValueError, "Could not read buckets (expected %zd, got %zd)", buckets_length, bytes_read); + goto fail_decref_buckets; + } + + PyObject_GetBuffer(bucket_bytes, &index->buckets_buffer, PyBUF_SIMPLE); + if(PyErr_Occurred()) { + goto fail_decref_buckets; + } + index->buckets = index->buckets_buffer.buf; + index->min_empty = get_min_empty(index->num_buckets); index->num_empty = count_empty(index); + if(index->num_empty < index->min_empty) { /* too many tombstones here / not enough empty buckets, do a same-size rebuild */ if(!hashindex_resize(index, index->num_buckets)) { - free(index->buckets); - free(index); - index = NULL; - goto fail; + PyErr_Format(PyExc_ValueError, "Failed to rebuild table"); + goto fail_free_buckets; } } -fail: - if(fclose(fd) < 0) { - EPRINTF_PATH(path, "fclose failed"); + + /* + * Clean intermediary objects up. Note that index is only freed if an error has occurred. + * Also note that the buffer in index->buckets_buffer holds a reference to buckets_bytes. + */ + +fail_free_buckets: + if(PyErr_Occurred()) { + hashindex_free_buckets(index); } +fail_decref_buckets: + Py_DECREF(bucket_bytes); +fail_release_header_buffer: + PyBuffer_Release(&header_buffer); +fail_free_index: + if(PyErr_Occurred()) { + free(index); + index = NULL; + } +fail_decref_header: + Py_DECREF(header_bytes); +fail: return index; } @@ -369,6 +430,7 @@ hashindex_init(int capacity, int key_size, int value_size) index->lower_limit = get_lower_limit(index->num_buckets); index->upper_limit = get_upper_limit(index->num_buckets); index->min_empty = get_min_empty(index->num_buckets); + index->buckets_buffer.buf = NULL; for(i = 0; i < capacity; i++) { BUCKET_MARK_EMPTY(index, i); } @@ -378,15 +440,17 @@ hashindex_init(int capacity, int key_size, int value_size) static void hashindex_free(HashIndex *index) { - free(index->buckets); + hashindex_free_buckets(index); free(index); } -static int -hashindex_write(HashIndex *index, const char *path) + +static void +hashindex_write(HashIndex *index, PyObject *file_py) { - off_t buckets_length = (off_t)index->num_buckets * index->bucket_size; - FILE *fd; + PyObject *length_object, *buckets_view; + Py_ssize_t length; + Py_ssize_t buckets_length = (Py_ssize_t)index->num_buckets * index->bucket_size; HashHeader header = { .magic = MAGIC, .num_entries = _htole32(index->num_entries), @@ -394,24 +458,41 @@ hashindex_write(HashIndex *index, const char *path) .key_size = index->key_size, .value_size = index->value_size }; - int ret = 1; - if((fd = fopen(path, "wb")) == NULL) { - EPRINTF_PATH(path, "fopen for writing failed"); - return 0; + length_object = PyObject_CallMethod(file_py, "write", "y#", &header, (int)sizeof(HashHeader)); + if(PyErr_Occurred()) { + return; } - if(fwrite(&header, 1, sizeof(header), fd) != sizeof(header)) { - EPRINTF_PATH(path, "fwrite header failed"); - ret = 0; + length = PyNumber_AsSsize_t(length_object, PyExc_OverflowError); + Py_DECREF(length_object); + if(PyErr_Occurred()) { + return; } - if(fwrite(index->buckets, 1, buckets_length, fd) != (size_t) buckets_length) { - EPRINTF_PATH(path, "fwrite buckets failed"); - ret = 0; + if(length != sizeof(HashHeader)) { + PyErr_SetString(PyExc_ValueError, "Failed to write header"); + return; } - if(fclose(fd) < 0) { - EPRINTF_PATH(path, "fclose failed"); + + /* Note: explicitly construct view; BuildValue can convert (pointer, length) to Python objects, but copies them for doing so */ + buckets_view = PyMemoryView_FromMemory((char*)index->buckets, buckets_length, PyBUF_READ); + if(!buckets_view) { + assert(PyErr_Occurred()); + return; + } + length_object = PyObject_CallMethod(file_py, "write", "O", buckets_view); + Py_DECREF(buckets_view); + if(PyErr_Occurred()) { + return; + } + length = PyNumber_AsSsize_t(length_object, PyExc_OverflowError); + Py_DECREF(length_object); + if(PyErr_Occurred()) { + return; + } + if(length != buckets_length) { + PyErr_SetString(PyExc_ValueError, "Failed to write buckets"); + return; } - return ret; } static const void * diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index d696ec3d..fba8c7a3 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -15,12 +15,12 @@ cdef extern from "_hashindex.c": ctypedef struct HashIndex: pass - HashIndex *hashindex_read(char *path) + HashIndex *hashindex_read(object file_py) except * HashIndex *hashindex_init(int capacity, int key_size, int value_size) void hashindex_free(HashIndex *index) int hashindex_len(HashIndex *index) int hashindex_size(HashIndex *index) - int hashindex_write(HashIndex *index, char *path) + void hashindex_write(HashIndex *index, object file_py) except * void *hashindex_get(HashIndex *index, void *key) void *hashindex_next_key(HashIndex *index, void *key) int hashindex_delete(HashIndex *index, void *key) @@ -67,13 +67,9 @@ cdef class IndexBase: def __cinit__(self, capacity=0, path=None, key_size=32): self.key_size = key_size if path: - path = os.fsencode(path) - self.index = hashindex_read(path) - if not self.index: - if errno: - PyErr_SetFromErrnoWithFilename(OSError, path) - return - raise RuntimeError('hashindex_read failed') + with open(path, 'rb') as fd: + self.index = hashindex_read(fd) + assert self.index, 'hashindex_read() returned NULL with no exception set' else: self.index = hashindex_init(capacity, self.key_size, self.value_size) if not self.index: @@ -88,9 +84,8 @@ cdef class IndexBase: return cls(path=path) def write(self, path): - path = os.fsencode(path) - if not hashindex_write(self.index, path): - raise Exception('hashindex_write failed') + with open(path, 'wb') as fd: + hashindex_write(self.index, fd) def clear(self): hashindex_free(self.index) diff --git a/src/borg/repository.py b/src/borg/repository.py index d7a7f449..b253d3f6 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -324,9 +324,8 @@ class Repository: index_path = os.path.join(self.path, 'index.%d' % transaction_id).encode('utf-8') try: return NSIndex.read(index_path) - except RuntimeError as error: - assert str(error) == 'hashindex_read failed' # everything else means we're in *deep* trouble - logger.warning('Repository index missing or corrupted, trying to recover') + except (ValueError, OSError) as exc: + logger.warning('Repository index missing or corrupted, trying to recover from: %s', exc) os.unlink(index_path) if not auto_recover: raise @@ -357,7 +356,8 @@ class Repository: if not self.index or transaction_id is None: try: self.index = self.open_index(transaction_id, False) - except RuntimeError: + except (ValueError, OSError) as exc: + logger.warning('Checking repository transaction due to previous error: %s', exc) self.check_transaction() self.index = self.open_index(transaction_id, False) if transaction_id is None: From 9e6b8f67b90aa0b7a154e22015ffef50d3825d19 Mon Sep 17 00:00:00 2001 From: enkore Date: Fri, 12 May 2017 20:34:45 +0200 Subject: [PATCH 0841/1387] serve: ignore --append-only when creating a repository (#2501) --- docs/changes.rst | 3 +++ docs/usage.rst | 7 +++---- src/borg/remote.py | 10 +++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 113f0909..ebd4e088 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -136,6 +136,9 @@ Compatibility notes: - Repositories in a repokey mode with a blank passphrase are now treated as unencrypted repositories for security checks (e.g. BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK). +- Running "borg init" via a "borg serve --append-only" server will *not* create + an append-only repository anymore. Use "borg init --append-only" to initialize + an append-only repository. Version 1.1.0b5 (2017-04-30) ---------------------------- diff --git a/docs/usage.rst b/docs/usage.rst index 42bda553..6ba7682e 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -677,10 +677,9 @@ in ``.ssh/authorized_keys`` :: command="borg serve --append-only ..." ssh-rsa command="borg serve ..." ssh-rsa -Please note that if you run ``borg init`` via a ``borg serve --append-only`` -server, the repository config will be created with a ``append_only=1`` entry. -This behaviour is subject to change in a later borg version. So, be aware of -it for now, but do not rely on it. +Running ``borg init`` via a ``borg serve --append-only`` server will *not* create +an append-only repository. Running ``borg init --append-only`` creates an append-only +repository regardless of server settings. Example +++++++ diff --git a/src/borg/remote.py b/src/borg/remote.py index b2a9938c..7a54ea70 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -180,6 +180,10 @@ class RepositoryServer: # pragma: no cover def __init__(self, restrict_to_paths, append_only): self.repository = None self.restrict_to_paths = restrict_to_paths + # This flag is parsed from the serve command line via Archiver.do_serve, + # i.e. it reflects local system policy and generally ranks higher than + # whatever the client wants, except when initializing a new repository + # (see RepositoryServer.open below). self.append_only = append_only self.client_version = parse_version('1.0.8') # fallback version if client is too old to send version information @@ -345,8 +349,12 @@ class RepositoryServer: # pragma: no cover break else: raise PathNotAllowed(path) + # "borg init" on "borg serve --append-only" (=self.append_only) does not create an append only repo, + # while "borg init --append-only" (=append_only) does, regardless of the --append-only (self.append_only) + # flag for serve. + append_only = (not create and self.append_only) or append_only self.repository = Repository(path, create, lock_wait=lock_wait, lock=lock, - append_only=self.append_only or append_only, + append_only=append_only, exclusive=exclusive) self.repository.__enter__() # clean exit handled by serve() method return self.repository.id From cad49b844e37a42a0ced0faf375d088568c9e6d9 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 11 May 2017 17:49:02 +0200 Subject: [PATCH 0842/1387] key: authenticated mode = not passphrase protected --- docs/changes.rst | 12 ++++++++++-- src/borg/crypto/key.py | 12 ++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index ebd4e088..a15f7fb0 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -133,13 +133,21 @@ Version 1.1.0b6 (unreleased) Compatibility notes: -- Repositories in a repokey mode with a blank passphrase are now treated - as unencrypted repositories for security checks +- Repositories in a repokey mode (including "authenticated" mode) with a + blank passphrase are now treated as unencrypted repositories for security checks (e.g. BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK). - Running "borg init" via a "borg serve --append-only" server will *not* create an append-only repository anymore. Use "borg init --append-only" to initialize an append-only repository. + Previously there would be no prompts nor messages if an unknown repository + in one of these modes with a blank passphrase was encountered. This would + allow an attacker to swap a repository, if one assumed that the lack of + password prompts was due to a set BORG_PASSPHRASE. + + Since the "trick" does not work if BORG_PASSPHRASE is set, this does generally + not affect scripts. + Version 1.1.0b5 (2017-04-30) ---------------------------- diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 9469be29..772b4ae5 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -749,6 +749,18 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): ARG_NAME = 'authenticated' STORAGE = KeyBlobStorage.REPO + # It's only authenticated, not encrypted. + passphrase_protected = False + + def load(self, target, passphrase): + success = super().load(target, passphrase) + self.passphrase_protected = False + return success + + def save(self, target, passphrase): + super().save(target, passphrase) + self.passphrase_protected = False + def encrypt(self, chunk): data = self.compressor.compress(chunk) return b''.join([self.TYPE_STR, data]) From 848df38d080885b08c2c8f9b6907e3899ee3486e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 11 May 2017 21:03:32 +0200 Subject: [PATCH 0843/1387] Rename Key.passphrase_protected -> logically_encrypted & document --- docs/changes.rst | 9 ++++++--- src/borg/cache.py | 2 +- src/borg/crypto/key.py | 33 +++++++++++++++++++++++++-------- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index a15f7fb0..e511716a 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -133,15 +133,18 @@ Version 1.1.0b6 (unreleased) Compatibility notes: -- Repositories in a repokey mode (including "authenticated" mode) with a - blank passphrase are now treated as unencrypted repositories for security checks +- Repositories in the "repokey" and "repokey-blake2" modes with an empty passphrase + are now treated as unencrypted repositories for security checks (e.g. BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK). - Running "borg init" via a "borg serve --append-only" server will *not* create an append-only repository anymore. Use "borg init --append-only" to initialize an append-only repository. + Repositories in the "authenticated" mode are now treated as the unencrypted repositories + they are. + Previously there would be no prompts nor messages if an unknown repository - in one of these modes with a blank passphrase was encountered. This would + in one of these modes with an empty passphrase was encountered. This would allow an attacker to swap a repository, if one assumed that the lack of password prompts was due to a set BORG_PASSPHRASE. diff --git a/src/borg/cache.py b/src/borg/cache.py index aeb9d3d4..40ed925a 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -130,7 +130,7 @@ class SecurityManager: self.save(manifest, key, cache) def assert_access_unknown(self, warn_if_unencrypted, key): - if warn_if_unencrypted and not key.passphrase_protected and not self.known(): + if warn_if_unencrypted and not key.logically_encrypted and not self.known(): msg = ("Warning: Attempting to access a previously unknown unencrypted repository!\n" + "Do you want to continue? [yN] ") if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 772b4ae5..24bb8103 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -129,15 +129,31 @@ def tam_required(repository): class KeyBase: + # Numeric key type ID, must fit in one byte. TYPE = None # override in subclasses # Human-readable name NAME = 'UNDEFINED' + # Name used in command line / API (e.g. borg init --encryption=...) ARG_NAME = 'UNDEFINED' + # Storage type (no key blob storage / keyfile / repo) STORAGE = KeyBlobStorage.NO_STORAGE + # Seed for the buzhash chunker (borg.algorithms.chunker.Chunker) + # type: int + chunk_seed = None + + # Whether this *particular instance* is encrypted from a practical point of view, + # i.e. when it's using encryption with a empty passphrase, then + # that may be *technically* called encryption, but for all intents and purposes + # that's as good as not encrypting in the first place, and this member should be False. + # + # The empty passphrase is also special because Borg tries it first when no passphrase + # was supplied, and if an empty passphrase works, then Borg won't ask for one. + logically_encrypted = False + def __init__(self, repository): self.TYPE_STR = bytes([self.TYPE]) self.repository = repository @@ -234,7 +250,7 @@ class PlaintextKey(KeyBase): STORAGE = KeyBlobStorage.NO_STORAGE chunk_seed = 0 - passphrase_protected = False + logically_encrypted = False def __init__(self, repository): super().__init__(repository) @@ -314,7 +330,8 @@ class ID_HMAC_SHA_256: class AESKeyBase(KeyBase): - """Common base class shared by KeyfileKey and PassphraseKey + """ + Common base class shared by KeyfileKey and PassphraseKey Chunks are encrypted using 256bit AES in Counter Mode (CTR) @@ -330,7 +347,7 @@ class AESKeyBase(KeyBase): MAC = hmac_sha256 - passphrase_protected = True + logically_encrypted = True def encrypt(self, chunk): data = self.compressor.compress(chunk) @@ -705,7 +722,7 @@ class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase): def load(self, target, passphrase): # While the repository is encrypted, we consider a repokey repository with a blank # passphrase an unencrypted repository. - self.passphrase_protected = passphrase != '' + self.logically_encrypted = passphrase != '' # what we get in target is just a repo location, but we already have the repo obj: target = self.repository @@ -717,7 +734,7 @@ class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase): return success def save(self, target, passphrase): - self.passphrase_protected = passphrase != '' + self.logically_encrypted = passphrase != '' key_data = self._save(passphrase) key_data = key_data.encode('utf-8') # remote repo: msgpack issue #99, giving bytes target.save_key(key_data) @@ -750,16 +767,16 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): STORAGE = KeyBlobStorage.REPO # It's only authenticated, not encrypted. - passphrase_protected = False + logically_encrypted = False def load(self, target, passphrase): success = super().load(target, passphrase) - self.passphrase_protected = False + self.logically_encrypted = False return success def save(self, target, passphrase): super().save(target, passphrase) - self.passphrase_protected = False + self.logically_encrypted = False def encrypt(self, chunk): data = self.compressor.compress(chunk) From a16d81271a3b4915c7fe14a3444df6dd0e88cfad Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 12 May 2017 20:48:47 +0200 Subject: [PATCH 0844/1387] key: add round-trip test --- src/borg/crypto/key.py | 11 +++++++++++ src/borg/testsuite/key.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 24bb8103..37cf3f55 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -778,6 +778,17 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): super().save(target, passphrase) self.logically_encrypted = False + def extract_nonce(self, payload): + # This is called during set-up of the AES ciphers we're not actually using for this + # key. Therefore the return value of this method doesn't matter; it's just around + # to not have it crash should key identification be run against a very small chunk + # by "borg check" when the manifest is lost. (The manifest is always large enough + # to have the original method read some garbage from bytes 33-41). (Also, the return + # value must be larger than the 41 byte bloat of the original format). + if payload[0] != self.TYPE: + raise IntegrityError('Manifest: Invalid encryption envelope') + return 42 + def encrypt(self, chunk): data = self.compressor.compress(chunk) return b''.join([self.TYPE_STR, data]) diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index 5f0ad367..34399f9b 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -11,6 +11,7 @@ from ..crypto.key import Passphrase, PasswordRetriesExceeded, bin_to_hex from ..crypto.key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, \ AuthenticatedKey from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError +from ..crypto.key import identify_key from ..crypto.low_level import bytes_to_long, num_aes_blocks from ..helpers import IntegrityError from ..helpers import Location @@ -224,6 +225,16 @@ class TestKey: id[12] = 0 key.decrypt(id, data) + def test_roundtrip(self, key): + repository = key.repository + plaintext = b'foo' + encrypted = key.encrypt(plaintext) + identified_key_class = identify_key(encrypted) + assert identified_key_class == key.__class__ + loaded_key = identified_key_class.detect(repository, encrypted) + decrypted = loaded_key.decrypt(None, encrypted) + assert decrypted == plaintext + def test_decrypt_decompress(self, key): plaintext = b'123456789' encrypted = key.encrypt(plaintext) From 820066da5d2cf3b6a7f5f005506214f3559e3a41 Mon Sep 17 00:00:00 2001 From: enkore Date: Fri, 12 May 2017 21:38:31 +0200 Subject: [PATCH 0845/1387] Implement IntegrityCheckedFile (#2502) Implement IntegrityCheckedFile This is based on much earlier work from October 2016 by me, but is overall simplified and changed terminology (from "signing" to hashing and integrity checking). See #1688 for the full history. --- src/borg/crypto/file_integrity.py | 182 +++++++++++++++++++++++++++ src/borg/testsuite/file_integrity.py | 152 ++++++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 src/borg/crypto/file_integrity.py create mode 100644 src/borg/testsuite/file_integrity.py diff --git a/src/borg/crypto/file_integrity.py b/src/borg/crypto/file_integrity.py new file mode 100644 index 00000000..5c1fa4e1 --- /dev/null +++ b/src/borg/crypto/file_integrity.py @@ -0,0 +1,182 @@ +import hashlib +import io +import json +import os +from hmac import compare_digest + +from ..helpers import IntegrityError +from ..logger import create_logger + +logger = create_logger() + + +class FileLikeWrapper: + def __enter__(self): + self.fd.__enter__() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.fd.__exit__(exc_type, exc_val, exc_tb) + + def tell(self): + return self.fd.tell() + + def seek(self, offset, whence=io.SEEK_SET): + return self.fd.seek(offset, whence) + + def write(self, data): + return self.fd.write(data) + + def read(self, n=None): + return self.fd.read(n) + + def flush(self): + self.fd.flush() + + def fileno(self): + return self.fd.fileno() + + +class SHA512FileHashingWrapper(FileLikeWrapper): + """ + Wrapper for file-like objects that computes a hash on-the-fly while reading/writing. + + WARNING: Seeks should only be used to query the size of the file, not + to skip data, because skipped data isn't read and not hashed into the digest. + + Similarly skipping while writing to create sparse files is also not supported. + + Data has to be read/written in a symmetric fashion, otherwise different + digests will be generated. + + Note: When used as a context manager read/write operations outside the enclosed scope + are illegal. + """ + + ALGORITHM = 'SHA512' + + def __init__(self, backing_fd, write): + self.fd = backing_fd + self.writing = write + self.hash = hashlib.new(self.ALGORITHM) + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is None: + self.hash_length() + super().__exit__(exc_type, exc_val, exc_tb) + + def write(self, data): + """ + Write *data* to backing file and update internal state. + """ + n = super().write(data) + self.hash.update(data) + return n + + def read(self, n=None): + """ + Read *data* from backing file (*n* has the usual meaning) and update internal state. + """ + data = super().read(n) + self.hash.update(data) + return data + + def hexdigest(self): + """ + Return current digest bytes as hex-string. + + Note: this can be called multiple times. + """ + return self.hash.hexdigest() + + def update(self, data: bytes): + self.hash.update(data) + + def hash_length(self, seek_to_end=False): + if seek_to_end: + # Add length of file to the hash to avoid problems if only a prefix is read. + self.seek(0, io.SEEK_END) + self.hash.update(str(self.tell()).encode()) + + +class FileIntegrityError(IntegrityError): + """File failed integrity check: {}""" + + +class IntegrityCheckedFile(FileLikeWrapper): + def __init__(self, path, write, filename=None, override_fd=None): + self.path = path + self.writing = write + mode = 'wb' if write else 'rb' + self.file_fd = override_fd or open(path, mode) + + self.fd = self.hasher = SHA512FileHashingWrapper(backing_fd=self.file_fd, write=write) + + self.hash_filename(filename) + + if write: + self.digests = {} + else: + self.digests = self.read_integrity_file(path, self.hasher) + # TODO: When we're reading but don't have any digests, i.e. no integrity file existed, + # TODO: then we could just short-circuit. + + def hash_filename(self, filename=None): + # Hash the name of the file, but only the basename, ie. not the path. + # In Borg the name itself encodes the context (eg. index.N, cache, files), + # while the path doesn't matter, and moving e.g. a repository or cache directory is supported. + # Changing the name however imbues a change of context that is not permissible. + filename = os.path.basename(filename or self.path) + self.hasher.update(('%10d' % len(filename)).encode()) + self.hasher.update(filename.encode()) + + @staticmethod + def integrity_file_path(path): + return path + '.integrity' + + @classmethod + def read_integrity_file(cls, path, hasher): + try: + with open(cls.integrity_file_path(path), 'r') as fd: + integrity_file = json.load(fd) + # Provisions for agility now, implementation later, but make sure the on-disk joint is oiled. + algorithm = integrity_file['algorithm'] + if algorithm != hasher.ALGORITHM: + logger.warning('Cannot verify integrity of %s: Unknown algorithm %r', path, algorithm) + return + digests = integrity_file['digests'] + # Require at least presence of the final digest + digests['final'] + return digests + except FileNotFoundError: + logger.info('No integrity file found for %s', path) + except (OSError, ValueError, TypeError, KeyError) as e: + logger.warning('Could not read integrity file for %s: %s', path, e) + raise FileIntegrityError(path) + + def hash_part(self, partname, is_final=False): + if not self.writing and not self.digests: + return + self.hasher.update(partname.encode()) + self.hasher.hash_length(seek_to_end=is_final) + digest = self.hasher.hexdigest() + if self.writing: + self.digests[partname] = digest + elif self.digests and not compare_digest(self.digests.get(partname, ''), digest): + raise FileIntegrityError(self.path) + + def __exit__(self, exc_type, exc_val, exc_tb): + exception = exc_type is not None + if not exception: + self.hash_part('final', is_final=True) + self.hasher.__exit__(exc_type, exc_val, exc_tb) + if exception: + return + if self.writing: + with open(self.integrity_file_path(self.path), 'w') as fd: + json.dump({ + 'algorithm': self.hasher.ALGORITHM, + 'digests': self.digests, + }, fd) + elif self.digests: + logger.debug('Verified integrity of %s', self.path) diff --git a/src/borg/testsuite/file_integrity.py b/src/borg/testsuite/file_integrity.py new file mode 100644 index 00000000..a8ef95f7 --- /dev/null +++ b/src/borg/testsuite/file_integrity.py @@ -0,0 +1,152 @@ + +import pytest + +from ..crypto.file_integrity import IntegrityCheckedFile, FileIntegrityError + + +class TestReadIntegrityFile: + def test_no_integrity(self, tmpdir): + protected_file = tmpdir.join('file') + protected_file.write('1234') + assert IntegrityCheckedFile.read_integrity_file(str(protected_file), None) is None + + def test_truncated_integrity(self, tmpdir): + protected_file = tmpdir.join('file') + protected_file.write('1234') + tmpdir.join('file.integrity').write('') + with pytest.raises(FileIntegrityError): + IntegrityCheckedFile.read_integrity_file(str(protected_file), None) + + def test_unknown_algorithm(self, tmpdir): + class SomeHasher: + ALGORITHM = 'HMAC_FOOHASH9000' + + protected_file = tmpdir.join('file') + protected_file.write('1234') + tmpdir.join('file.integrity').write('{"algorithm": "HMAC_SERIOUSHASH", "digests": "1234"}') + assert IntegrityCheckedFile.read_integrity_file(str(protected_file), SomeHasher()) is None + + @pytest.mark.parametrize('json', ( + '{"ALGORITHM": "HMAC_SERIOUSHASH", "digests": "1234"}', + '[]', + '1234.5', + '"A string"', + 'Invalid JSON', + )) + def test_malformed(self, tmpdir, json): + protected_file = tmpdir.join('file') + protected_file.write('1234') + tmpdir.join('file.integrity').write(json) + with pytest.raises(FileIntegrityError): + IntegrityCheckedFile.read_integrity_file(str(protected_file), None) + + def test_valid(self, tmpdir): + class SomeHasher: + ALGORITHM = 'HMAC_FOO1' + + protected_file = tmpdir.join('file') + protected_file.write('1234') + tmpdir.join('file.integrity').write('{"algorithm": "HMAC_FOO1", "digests": {"final": "1234"}}') + assert IntegrityCheckedFile.read_integrity_file(str(protected_file), SomeHasher()) == {'final': '1234'} + + +class TestIntegrityCheckedFile: + @pytest.fixture + def integrity_protected_file(self, tmpdir): + path = str(tmpdir.join('file')) + with IntegrityCheckedFile(path, write=True) as fd: + fd.write(b'foo and bar') + return path + + def test_simple(self, tmpdir, integrity_protected_file): + assert tmpdir.join('file').check(file=True) + assert tmpdir.join('file.integrity').check(file=True) + with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + assert fd.read() == b'foo and bar' + + def test_corrupted_file(self, integrity_protected_file): + with open(integrity_protected_file, 'ab') as fd: + fd.write(b' extra data') + with pytest.raises(FileIntegrityError): + with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + assert fd.read() == b'foo and bar extra data' + + def test_corrupted_file_partial_read(self, integrity_protected_file): + with open(integrity_protected_file, 'ab') as fd: + fd.write(b' extra data') + with pytest.raises(FileIntegrityError): + with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + data = b'foo and bar' + assert fd.read(len(data)) == data + + @pytest.mark.parametrize('new_name', ( + 'different_file', + 'different_file.different_ext', + )) + def test_renamed_file(self, tmpdir, integrity_protected_file, new_name): + new_path = tmpdir.join(new_name) + tmpdir.join('file').move(new_path) + tmpdir.join('file.integrity').move(new_path + '.integrity') + with pytest.raises(FileIntegrityError): + with IntegrityCheckedFile(str(new_path), write=False) as fd: + assert fd.read() == b'foo and bar' + + def test_moved_file(self, tmpdir, integrity_protected_file): + new_dir = tmpdir.mkdir('another_directory') + tmpdir.join('file').move(new_dir.join('file')) + tmpdir.join('file.integrity').move(new_dir.join('file.integrity')) + new_path = str(new_dir.join('file')) + with IntegrityCheckedFile(new_path, write=False) as fd: + assert fd.read() == b'foo and bar' + + def test_no_integrity(self, tmpdir, integrity_protected_file): + tmpdir.join('file.integrity').remove() + with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + assert fd.read() == b'foo and bar' + + +class TestIntegrityCheckedFileParts: + @pytest.fixture + def integrity_protected_file(self, tmpdir): + path = str(tmpdir.join('file')) + with IntegrityCheckedFile(path, write=True) as fd: + fd.write(b'foo and bar') + fd.hash_part('foopart') + fd.write(b' other data') + return path + + def test_simple(self, integrity_protected_file): + with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + data1 = b'foo and bar' + assert fd.read(len(data1)) == data1 + fd.hash_part('foopart') + assert fd.read() == b' other data' + + def test_wrong_part_name(self, integrity_protected_file): + with pytest.raises(FileIntegrityError): + # Because some hash_part failed, the final digest will fail as well - again - even if we catch + # the failing hash_part. This is intentional: (1) it makes the code simpler (2) it's a good fail-safe + # against overly broad exception handling. + with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + data1 = b'foo and bar' + assert fd.read(len(data1)) == data1 + with pytest.raises(FileIntegrityError): + # This specific bit raises it directly + fd.hash_part('barpart') + # Still explodes in the end. + + @pytest.mark.parametrize('partial_read', (False, True)) + def test_part_independence(self, integrity_protected_file, partial_read): + with open(integrity_protected_file, 'ab') as fd: + fd.write(b'some extra stuff that does not belong') + with pytest.raises(FileIntegrityError): + with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + data1 = b'foo and bar' + try: + assert fd.read(len(data1)) == data1 + fd.hash_part('foopart') + except FileIntegrityError: + assert False, 'This part must not raise, since this part is still valid.' + if not partial_read: + fd.read() + # But overall it explodes with the final digest. Neat, eh? From 3d14fc72ece78e70c65e97a7e83f59dc4104b1cb Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 13 May 2017 12:49:58 +0200 Subject: [PATCH 0846/1387] SVG version of the logo --- docs/_static/logo.svg | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docs/_static/logo.svg diff --git a/docs/_static/logo.svg b/docs/_static/logo.svg new file mode 100644 index 00000000..5f9c5a19 --- /dev/null +++ b/docs/_static/logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + From 8c637da38110a961ec5c6aec9dd7dc6cca9c59f5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 13 May 2017 13:05:18 +0200 Subject: [PATCH 0847/1387] PDF version of the logo --- docs/_static/logo.pdf | Bin 0 -> 1199 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/_static/logo.pdf diff --git a/docs/_static/logo.pdf b/docs/_static/logo.pdf new file mode 100644 index 0000000000000000000000000000000000000000..56a5a1b451c09a02c7fe5fa314a2d912c6733ec8 GIT binary patch literal 1199 zcmY!laBQybzL#B9j3=eKBG;{?^pi^dg9(-qsFF#Ori zRrzLP45usKk-O7wtyQgI?cwOy%BkKdvETmjZT4g5<#(Tukatr(QqiySdu4t9{_WZy zES5%X^p|zIv7!;IIdG+&#OpTN|oKdmq^Je-kG}lsOjO=C;G&>eGX@>1n$@P)3`Id%vSp0Xg z9xZPAeAoR7&y-2OXZ-kbqi4&*InPfy+*B9POY%Jb;Oh^Os$=i!Bsl+9Y_7Yd`z&2tR7&B}YLVzoVa_S=*s`CZ#|-5tBi&Mun}ba#Vk zn~viGrS`d*6D&(RUlt3RZ{DZBDsn%E)T_hR+l=bphOEvq3%Wb zr3FY1f|w5$g}E^pXlqeoUU31)uH;IXu%|*vQ7PCJAdff$Rk@^=WhSQvxjTWBK>VQ} zlv0l#3K~xM47(`H36s4wd87NqA8Nh*pnW?FQMF;iopH_^n*jLa~^EDbR98X6$$Eh$RO%t Date: Sat, 13 May 2017 13:19:23 +0200 Subject: [PATCH 0848/1387] docs: use vector logo for PDF version --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 784c4022..dc40f2a0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -208,7 +208,7 @@ latex_documents = [ # The name of an image file (relative to this directory) to place at the top of # the title page. -latex_logo = '_static/logo.png' +latex_logo = '_static/logo.pdf' # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. From d5edb011f0dc02cce4aad154defb77e43755f8be Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 14 May 2017 17:49:08 +0200 Subject: [PATCH 0849/1387] support common options --- docs/man_intro.rst | 2 +- setup.py | 6 +- src/borg/archiver.py | 199 +++++++++++++++++++++++++-------- src/borg/testsuite/archiver.py | 6 + 4 files changed, 164 insertions(+), 49 deletions(-) diff --git a/docs/man_intro.rst b/docs/man_intro.rst index b726e331..85ba9bd3 100644 --- a/docs/man_intro.rst +++ b/docs/man_intro.rst @@ -14,7 +14,7 @@ deduplicating and encrypting backup tool SYNOPSIS -------- -borg [options] [arguments] +borg [common options] [options] [arguments] DESCRIPTION ----------- diff --git a/setup.py b/setup.py index 274e740d..5e2d309d 100644 --- a/setup.py +++ b/setup.py @@ -259,7 +259,7 @@ class build_usage(Command): "command_": command.replace(' ', '_'), "underline": '-' * len('borg ' + command)} doc.write(".. _borg_{command_}:\n\n".format(**params)) - doc.write("borg {command}\n{underline}\n::\n\n borg {command}".format(**params)) + doc.write("borg {command}\n{underline}\n::\n\n borg [common options] {command}".format(**params)) self.write_usage(parser, doc) epilog = parser.epilog parser.epilog = None @@ -402,10 +402,10 @@ class build_man(Command): if is_intermediary: subparsers = [action for action in parser._actions if 'SubParsersAction' in str(action.__class__)][0] for subcommand in subparsers.choices: - write('| borg', command, subcommand, '...') + write('| borg', '[common options]', command, subcommand, '...') self.see_also.setdefault(command, []).append('%s-%s' % (command, subcommand)) else: - write('borg', command, end='') + write('borg', '[common options]', command, end='') self.write_usage(write, parser) write('\n') diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 1be2b901..71065aa8 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1881,58 +1881,166 @@ class Archiver: epilog = [line for line in epilog if not line.startswith('.. man')] return '\n'.join(epilog) - common_parser = argparse.ArgumentParser(add_help=False, prog=self.prog) + class CommonOptions: + """ + Support class to allow specifying common options directly after the top-level command. - common_group = common_parser.add_argument_group('Common options') - common_group.add_argument('-h', '--help', action='help', help='show this help message and exit') - common_group.add_argument('--critical', dest='log_level', - action='store_const', const='critical', default='warning', - help='work on log level CRITICAL') - common_group.add_argument('--error', dest='log_level', - action='store_const', const='error', default='warning', - help='work on log level ERROR') - common_group.add_argument('--warning', dest='log_level', - action='store_const', const='warning', default='warning', - help='work on log level WARNING (default)') - common_group.add_argument('--info', '-v', '--verbose', dest='log_level', - action='store_const', const='info', default='warning', - help='work on log level INFO') - common_group.add_argument('--debug', dest='log_level', - action='store_const', const='debug', default='warning', - help='enable debug output, work on log level DEBUG') - common_group.add_argument('--debug-topic', dest='debug_topics', - action='append', metavar='TOPIC', default=[], - help='enable TOPIC debugging (can be specified multiple times). ' - 'The logger path is borg.debug. if TOPIC is not fully qualified.') - common_group.add_argument('-p', '--progress', dest='progress', action='store_true', - help='show progress information') - common_group.add_argument('--log-json', dest='log_json', action='store_true', - help='Output one JSON object per log line instead of formatted text.') - common_group.add_argument('--lock-wait', dest='lock_wait', type=int, metavar='N', default=1, - help='wait for the lock, but max. N seconds (default: %(default)d).') - common_group.add_argument('--show-version', dest='show_version', action='store_true', default=False, - help='show/log the borg version') - common_group.add_argument('--show-rc', dest='show_rc', action='store_true', default=False, - help='show/log the return code (rc)') - common_group.add_argument('--no-files-cache', dest='cache_files', action='store_false', - help='do not load/update the file metadata cache used to detect unchanged files') - common_group.add_argument('--umask', dest='umask', type=lambda s: int(s, 8), default=UMASK_DEFAULT, metavar='M', - help='set umask to M (local and remote, default: %(default)04o)') - common_group.add_argument('--remote-path', dest='remote_path', metavar='PATH', - help='use PATH as borg executable on the remote (default: "borg")') - common_group.add_argument('--remote-ratelimit', dest='remote_ratelimit', type=int, metavar='rate', - help='set remote network upload rate limit in kiByte/s (default: 0=unlimited)') - common_group.add_argument('--consider-part-files', dest='consider_part_files', - action='store_true', default=False, - help='treat part files like normal files (e.g. to list/extract them)') + Normally options can only be specified on the parser defining them, which means + that generally speaking *all* options go after all sub-commands. This is annoying + for common options in scripts, e.g. --remote-path or logging options. - parser = argparse.ArgumentParser(prog=self.prog, description='Borg - Deduplicated Backups') + This class allows adding the same set of options to both the top-level parser + and the final sub-command parsers (but not intermediary sub-commands, at least for now). + + It does so by giving every option's target name ("dest") a suffix indicating it's level + -- no two options in the parser hierarchy can have the same target -- + then, after parsing the command line, multiple definitions are resolved. + + Defaults are handled by only setting them on the top-level parser and setting + a sentinel object in all sub-parsers, which then allows to discern which parser + supplied the option. + """ + + # From lowest precedence to highest precedence: + # An option specified on the parser belonging to index 0 is overridden if the + # same option is specified on any parser with a higher index. + SUFFIX_PRECEDENCE = ('_maincommand', '_subcommand') + + def __init__(self): + from collections import defaultdict + + # Maps suffixes to sets of target names. + # E.g. common_options["_subcommand"] = {..., "log_level", ...} + self.common_options = defaultdict(defaultdict) + self.append_options = set() + self.default_sentinel = object() + + def add_common_group(self, parser, suffix='_subcommand', provide_defaults=False): + """ + Add common options to *parser*. + + *provide_defaults* must only be True exactly once in a parser hierarchy, + at the top level, and False on all lower levels. The default is chosen + accordingly. + + *suffix* indicates the suffix to use internally. It also indicates + which precedence the *parser* has for common options. See SUFFIX_PRECEDENCE. + """ + assert suffix in self.SUFFIX_PRECEDENCE + + def add_argument(*args, **kwargs): + if 'dest' in kwargs: + is_append = kwargs.get('action') == 'append' + if is_append: + self.append_options.add(kwargs['dest']) + assert kwargs['default'] == [], 'The default is explicitly constructed as an empty list in resolve()' + else: + self.common_options.setdefault(suffix, set()).add(kwargs['dest']) + kwargs['dest'] += suffix + if not provide_defaults and 'default' in kwargs: + # Interpolate help now, in case the %(default)d (or so) is mentioned, + # to avoid producing incorrect help output. + # Assumption: Interpolated output can safely be interpolated again, + # which should always be the case. + # Note: We control all inputs. + kwargs['help'] = kwargs['help'] % kwargs + if not is_append: + kwargs['default'] = self.default_sentinel + + common_group.add_argument(*args, **kwargs) + + common_group = parser.add_argument_group('Common options') + + add_argument('-h', '--help', action='help', help='show this help message and exit') + add_argument('--critical', dest='log_level', + action='store_const', const='critical', default='warning', + help='work on log level CRITICAL') + add_argument('--error', dest='log_level', + action='store_const', const='error', default='warning', + help='work on log level ERROR') + add_argument('--warning', dest='log_level', + action='store_const', const='warning', default='warning', + help='work on log level WARNING (default)') + add_argument('--info', '-v', '--verbose', dest='log_level', + action='store_const', const='info', default='warning', + help='work on log level INFO') + add_argument('--debug', dest='log_level', + action='store_const', const='debug', default='warning', + help='enable debug output, work on log level DEBUG') + add_argument('--debug-topic', dest='debug_topics', + action='append', metavar='TOPIC', default=[], + help='enable TOPIC debugging (can be specified multiple times). ' + 'The logger path is borg.debug. if TOPIC is not fully qualified.') + add_argument('-p', '--progress', dest='progress', action='store_true', + help='show progress information') + add_argument('--log-json', dest='log_json', action='store_true', + help='Output one JSON object per log line instead of formatted text.') + add_argument('--lock-wait', dest='lock_wait', type=int, metavar='N', default=1, + help='wait for the lock, but max. N seconds (default: %(default)d).') + add_argument('--show-version', dest='show_version', action='store_true', default=False, + help='show/log the borg version') + add_argument('--show-rc', dest='show_rc', action='store_true', default=False, + help='show/log the return code (rc)') + add_argument('--no-files-cache', dest='cache_files', action='store_false', + help='do not load/update the file metadata cache used to detect unchanged files') + add_argument('--umask', dest='umask', type=lambda s: int(s, 8), default=UMASK_DEFAULT, metavar='M', + help='set umask to M (local and remote, default: %(default)04o)') + add_argument('--remote-path', dest='remote_path', metavar='PATH', + help='use PATH as borg executable on the remote (default: "borg")') + add_argument('--remote-ratelimit', dest='remote_ratelimit', type=int, metavar='rate', + help='set remote network upload rate limit in kiByte/s (default: 0=unlimited)') + add_argument('--consider-part-files', dest='consider_part_files', + action='store_true', default=False, + help='treat part files like normal files (e.g. to list/extract them)') + + def resolve(self, args: argparse.Namespace): # Namespace has "in" but otherwise is not like a dict. + """ + Resolve the multiple definitions of each common option to the final value. + """ + for suffix in self.SUFFIX_PRECEDENCE: + # From highest level to lowest level, so the "most-specific" option wins, e.g. + # "borg --debug create --info" shall result in --info being effective. + for dest in self.common_options.get(suffix, []): + # map_from is this suffix' option name, e.g. log_level_subcommand + # map_to is the target name, e.g. log_level + map_from = dest + suffix + map_to = dest + # Retrieve value; depending on the action it may not exist, but usually does + # (store_const/store_true/store_false), either because the action implied a default + # or a default is explicitly supplied. + # Note that defaults on lower levels are replaced with default_sentinel. + # Only the top level has defaults. + value = getattr(args, map_from, self.default_sentinel) + if value is not self.default_sentinel: + # value was indeed specified on this level. Transfer value to target, + # and un-clobber the args (for tidiness - you *cannot* use the suffixed + # names for other purposes, obviously). + setattr(args, map_to, value) + delattr(args, map_from) + + # Options with an "append" action need some special treatment. Instead of + # overriding values, all specified values are merged together. + for dest in self.append_options: + option_value = [] + for suffix in self.SUFFIX_PRECEDENCE: + # Find values of this suffix, if any, and add them to the final list + values = getattr(args, dest + suffix, []) + option_value.extend(values) + setattr(args, dest, option_value) + + parser = argparse.ArgumentParser(prog=self.prog, description='Borg - Deduplicated Backups', + add_help=False) + parser.common_options = CommonOptions() parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__, help='show version number and exit') - subparsers = parser.add_subparsers(title='required arguments', metavar='') + parser.common_options.add_common_group(parser, '_maincommand', provide_defaults=True) + common_parser = argparse.ArgumentParser(add_help=False, prog=self.prog) # some empty defaults for all subparsers common_parser.set_defaults(paths=[], patterns=[]) + parser.common_options.add_common_group(common_parser, '_subcommand') + + subparsers = parser.add_subparsers(title='required arguments', metavar='') serve_epilog = process_epilog(""" This command starts a repository server process. This command is usually not used manually. @@ -3358,6 +3466,7 @@ class Archiver: args = self.preprocess_args(args) parser = self.build_parser() args = parser.parse_args(args or ['-h']) + parser.common_options.resolve(args) # This works around http://bugs.python.org/issue9351 func = getattr(args, 'func', None) or getattr(args, 'fallback_func') if func == self.do_create and not args.paths: diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 14b22ed0..6591fdca 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1680,6 +1680,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert log_message['name'].startswith('borg.') assert isinstance(log_message['message'], str) + def test_common_options(self): + self.create_test_files() + self.cmd('init', '--encryption=repokey', self.repository_location) + log = self.cmd('--debug', 'create', self.repository_location + '::test', 'input') + assert 'security: read previous_location' in log + def _get_sizes(self, compression, compressible, size=10000): if compressible: contents = b'X' * size From 2ab5df0217f3792423a43a3871c5a73543097475 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 14 May 2017 18:50:49 +0200 Subject: [PATCH 0850/1387] support common options on mid-level commands (borg *key* export) --- src/borg/archiver.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 71065aa8..780f68b3 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1904,7 +1904,7 @@ class Archiver: # From lowest precedence to highest precedence: # An option specified on the parser belonging to index 0 is overridden if the # same option is specified on any parser with a higher index. - SUFFIX_PRECEDENCE = ('_maincommand', '_subcommand') + SUFFIX_PRECEDENCE = ('_maincommand', '_midcommand', '_subcommand') def __init__(self): from collections import defaultdict @@ -2040,6 +2040,10 @@ class Archiver: common_parser.set_defaults(paths=[], patterns=[]) parser.common_options.add_common_group(common_parser, '_subcommand') + mid_common_parser = argparse.ArgumentParser(add_help=False, prog=self.prog) + mid_common_parser.set_defaults(paths=[], patterns=[]) + parser.common_options.add_common_group(mid_common_parser, '_midcommand') + subparsers = parser.add_subparsers(title='required arguments', metavar='') serve_epilog = process_epilog(""" @@ -2221,7 +2225,7 @@ class Archiver: help='work slower, but using less space') self.add_archives_filters_args(subparser) - subparser = subparsers.add_parser('key', parents=[common_parser], add_help=False, + subparser = subparsers.add_parser('key', parents=[mid_common_parser], add_help=False, description="Manage a keyfile or repokey of a repository", epilog="", formatter_class=argparse.RawDescriptionHelpFormatter, @@ -3221,7 +3225,7 @@ class Archiver: in case you ever run into some severe malfunction. Use them only if you know what you are doing or if a trusted developer tells you what to do.""") - subparser = subparsers.add_parser('debug', parents=[common_parser], add_help=False, + subparser = subparsers.add_parser('debug', parents=[mid_common_parser], add_help=False, description='debugging command (not intended for normal use)', epilog=debug_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, @@ -3362,7 +3366,7 @@ class Archiver: benchmark_epilog = process_epilog("These commands do various benchmarks.") - subparser = subparsers.add_parser('benchmark', parents=[common_parser], add_help=False, + subparser = subparsers.add_parser('benchmark', parents=[mid_common_parser], add_help=False, description='benchmark command', epilog=benchmark_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, From b9efdb2ce3dfa2e5ac926fae8ba43fadab097259 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 15 May 2017 17:03:30 +0200 Subject: [PATCH 0851/1387] refactor CommonOptions as a reusable class --- src/borg/archiver.py | 311 +++++++++++++++++++++++-------------------- 1 file changed, 164 insertions(+), 147 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 780f68b3..e07aeaee 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1870,6 +1870,126 @@ class Archiver: print(warning, file=sys.stderr) return args + class CommonOptions: + """ + Support class to allow specifying common options directly after the top-level command. + + Normally options can only be specified on the parser defining them, which means + that generally speaking *all* options go after all sub-commands. This is annoying + for common options in scripts, e.g. --remote-path or logging options. + + This class allows adding the same set of options to both the top-level parser + and the final sub-command parsers (but not intermediary sub-commands, at least for now). + + It does so by giving every option's target name ("dest") a suffix indicating its level + -- no two options in the parser hierarchy can have the same target -- + then, after parsing the command line, multiple definitions are resolved. + + Defaults are handled by only setting them on the top-level parser and setting + a sentinel object in all sub-parsers, which then allows to discern which parser + supplied the option. + """ + + def __init__(self, define_common_options, suffix_precedence): + """ + *define_common_options* should be a callable taking one argument, which + will be a argparse.Parser.add_argument-like function. + + *define_common_options* will be called multiple times, and should call + the passed function to define common options exactly the same way each time. + + *suffix_precedence* should be a tuple of the suffixes that will be used. + It is ordered from lowest precedence to highest precedence: + An option specified on the parser belonging to index 0 is overridden if the + same option is specified on any parser with a higher index. + """ + self.define_common_options = define_common_options + self.suffix_precedence = suffix_precedence + + # Maps suffixes to sets of target names. + # E.g. common_options["_subcommand"] = {..., "log_level", ...} + self.common_options = dict() + # Set of options with the 'append' action. + self.append_options = set() + # This is the sentinel object that replaces all default values in parsers + # below the top-level parser. + self.default_sentinel = object() + + def add_common_group(self, parser, suffix, provide_defaults=False): + """ + Add common options to *parser*. + + *provide_defaults* must only be True exactly once in a parser hierarchy, + at the top level, and False on all lower levels. The default is chosen + accordingly. + + *suffix* indicates the suffix to use internally. It also indicates + which precedence the *parser* has for common options. See *suffix_precedence* + of __init__. + """ + assert suffix in self.suffix_precedence + + def add_argument(*args, **kwargs): + if 'dest' in kwargs: + kwargs.setdefault('action', 'store') + assert kwargs['action'] in ('help', 'store_const', 'store_true', 'store_false', 'store', 'append') + is_append = kwargs['action'] == 'append' + if is_append: + self.append_options.add(kwargs['dest']) + assert kwargs['default'] == [], 'The default is explicitly constructed as an empty list in resolve()' + else: + self.common_options.setdefault(suffix, set()).add(kwargs['dest']) + kwargs['dest'] += suffix + if not provide_defaults and 'default' in kwargs: + # Interpolate help now, in case the %(default)d (or so) is mentioned, + # to avoid producing incorrect help output. + # Assumption: Interpolated output can safely be interpolated again, + # which should always be the case. + # Note: We control all inputs. + kwargs['help'] = kwargs['help'] % kwargs + if not is_append: + kwargs['default'] = self.default_sentinel + + common_group.add_argument(*args, **kwargs) + + common_group = parser.add_argument_group('Common options') + self.define_common_options(add_argument) + + def resolve(self, args: argparse.Namespace): # Namespace has "in" but otherwise is not like a dict. + """ + Resolve the multiple definitions of each common option to the final value. + """ + for suffix in self.suffix_precedence: + # From highest level to lowest level, so the "most-specific" option wins, e.g. + # "borg --debug create --info" shall result in --info being effective. + for dest in self.common_options.get(suffix, []): + # map_from is this suffix' option name, e.g. log_level_subcommand + # map_to is the target name, e.g. log_level + map_from = dest + suffix + map_to = dest + # Retrieve value; depending on the action it may not exist, but usually does + # (store_const/store_true/store_false), either because the action implied a default + # or a default is explicitly supplied. + # Note that defaults on lower levels are replaced with default_sentinel. + # Only the top level has defaults. + value = getattr(args, map_from, self.default_sentinel) + if value is not self.default_sentinel: + # value was indeed specified on this level. Transfer value to target, + # and un-clobber the args (for tidiness - you *cannot* use the suffixed + # names for other purposes, obviously). + setattr(args, map_to, value) + delattr(args, map_from) + + # Options with an "append" action need some special treatment. Instead of + # overriding values, all specified values are merged together. + for dest in self.append_options: + option_value = [] + for suffix in self.suffix_precedence: + # Find values of this suffix, if any, and add them to the final list + values = getattr(args, dest + suffix, []) + option_value.extend(values) + setattr(args, dest, option_value) + def build_parser(self): def process_epilog(epilog): epilog = textwrap.dedent(epilog).splitlines() @@ -1881,156 +2001,53 @@ class Archiver: epilog = [line for line in epilog if not line.startswith('.. man')] return '\n'.join(epilog) - class CommonOptions: - """ - Support class to allow specifying common options directly after the top-level command. - - Normally options can only be specified on the parser defining them, which means - that generally speaking *all* options go after all sub-commands. This is annoying - for common options in scripts, e.g. --remote-path or logging options. - - This class allows adding the same set of options to both the top-level parser - and the final sub-command parsers (but not intermediary sub-commands, at least for now). - - It does so by giving every option's target name ("dest") a suffix indicating it's level - -- no two options in the parser hierarchy can have the same target -- - then, after parsing the command line, multiple definitions are resolved. - - Defaults are handled by only setting them on the top-level parser and setting - a sentinel object in all sub-parsers, which then allows to discern which parser - supplied the option. - """ - - # From lowest precedence to highest precedence: - # An option specified on the parser belonging to index 0 is overridden if the - # same option is specified on any parser with a higher index. - SUFFIX_PRECEDENCE = ('_maincommand', '_midcommand', '_subcommand') - - def __init__(self): - from collections import defaultdict - - # Maps suffixes to sets of target names. - # E.g. common_options["_subcommand"] = {..., "log_level", ...} - self.common_options = defaultdict(defaultdict) - self.append_options = set() - self.default_sentinel = object() - - def add_common_group(self, parser, suffix='_subcommand', provide_defaults=False): - """ - Add common options to *parser*. - - *provide_defaults* must only be True exactly once in a parser hierarchy, - at the top level, and False on all lower levels. The default is chosen - accordingly. - - *suffix* indicates the suffix to use internally. It also indicates - which precedence the *parser* has for common options. See SUFFIX_PRECEDENCE. - """ - assert suffix in self.SUFFIX_PRECEDENCE - - def add_argument(*args, **kwargs): - if 'dest' in kwargs: - is_append = kwargs.get('action') == 'append' - if is_append: - self.append_options.add(kwargs['dest']) - assert kwargs['default'] == [], 'The default is explicitly constructed as an empty list in resolve()' - else: - self.common_options.setdefault(suffix, set()).add(kwargs['dest']) - kwargs['dest'] += suffix - if not provide_defaults and 'default' in kwargs: - # Interpolate help now, in case the %(default)d (or so) is mentioned, - # to avoid producing incorrect help output. - # Assumption: Interpolated output can safely be interpolated again, - # which should always be the case. - # Note: We control all inputs. - kwargs['help'] = kwargs['help'] % kwargs - if not is_append: - kwargs['default'] = self.default_sentinel - - common_group.add_argument(*args, **kwargs) - - common_group = parser.add_argument_group('Common options') - - add_argument('-h', '--help', action='help', help='show this help message and exit') - add_argument('--critical', dest='log_level', - action='store_const', const='critical', default='warning', - help='work on log level CRITICAL') - add_argument('--error', dest='log_level', - action='store_const', const='error', default='warning', - help='work on log level ERROR') - add_argument('--warning', dest='log_level', - action='store_const', const='warning', default='warning', - help='work on log level WARNING (default)') - add_argument('--info', '-v', '--verbose', dest='log_level', - action='store_const', const='info', default='warning', - help='work on log level INFO') - add_argument('--debug', dest='log_level', - action='store_const', const='debug', default='warning', - help='enable debug output, work on log level DEBUG') - add_argument('--debug-topic', dest='debug_topics', - action='append', metavar='TOPIC', default=[], - help='enable TOPIC debugging (can be specified multiple times). ' - 'The logger path is borg.debug. if TOPIC is not fully qualified.') - add_argument('-p', '--progress', dest='progress', action='store_true', - help='show progress information') - add_argument('--log-json', dest='log_json', action='store_true', - help='Output one JSON object per log line instead of formatted text.') - add_argument('--lock-wait', dest='lock_wait', type=int, metavar='N', default=1, - help='wait for the lock, but max. N seconds (default: %(default)d).') - add_argument('--show-version', dest='show_version', action='store_true', default=False, - help='show/log the borg version') - add_argument('--show-rc', dest='show_rc', action='store_true', default=False, - help='show/log the return code (rc)') - add_argument('--no-files-cache', dest='cache_files', action='store_false', - help='do not load/update the file metadata cache used to detect unchanged files') - add_argument('--umask', dest='umask', type=lambda s: int(s, 8), default=UMASK_DEFAULT, metavar='M', - help='set umask to M (local and remote, default: %(default)04o)') - add_argument('--remote-path', dest='remote_path', metavar='PATH', - help='use PATH as borg executable on the remote (default: "borg")') - add_argument('--remote-ratelimit', dest='remote_ratelimit', type=int, metavar='rate', - help='set remote network upload rate limit in kiByte/s (default: 0=unlimited)') - add_argument('--consider-part-files', dest='consider_part_files', - action='store_true', default=False, - help='treat part files like normal files (e.g. to list/extract them)') - - def resolve(self, args: argparse.Namespace): # Namespace has "in" but otherwise is not like a dict. - """ - Resolve the multiple definitions of each common option to the final value. - """ - for suffix in self.SUFFIX_PRECEDENCE: - # From highest level to lowest level, so the "most-specific" option wins, e.g. - # "borg --debug create --info" shall result in --info being effective. - for dest in self.common_options.get(suffix, []): - # map_from is this suffix' option name, e.g. log_level_subcommand - # map_to is the target name, e.g. log_level - map_from = dest + suffix - map_to = dest - # Retrieve value; depending on the action it may not exist, but usually does - # (store_const/store_true/store_false), either because the action implied a default - # or a default is explicitly supplied. - # Note that defaults on lower levels are replaced with default_sentinel. - # Only the top level has defaults. - value = getattr(args, map_from, self.default_sentinel) - if value is not self.default_sentinel: - # value was indeed specified on this level. Transfer value to target, - # and un-clobber the args (for tidiness - you *cannot* use the suffixed - # names for other purposes, obviously). - setattr(args, map_to, value) - delattr(args, map_from) - - # Options with an "append" action need some special treatment. Instead of - # overriding values, all specified values are merged together. - for dest in self.append_options: - option_value = [] - for suffix in self.SUFFIX_PRECEDENCE: - # Find values of this suffix, if any, and add them to the final list - values = getattr(args, dest + suffix, []) - option_value.extend(values) - setattr(args, dest, option_value) + def define_common_options(add_common_option): + add_common_option('-h', '--help', action='help', help='show this help message and exit') + add_common_option('--critical', dest='log_level', + action='store_const', const='critical', default='warning', + help='work on log level CRITICAL') + add_common_option('--error', dest='log_level', + action='store_const', const='error', default='warning', + help='work on log level ERROR') + add_common_option('--warning', dest='log_level', + action='store_const', const='warning', default='warning', + help='work on log level WARNING (default)') + add_common_option('--info', '-v', '--verbose', dest='log_level', + action='store_const', const='info', default='warning', + help='work on log level INFO') + add_common_option('--debug', dest='log_level', + action='store_const', const='debug', default='warning', + help='enable debug output, work on log level DEBUG') + add_common_option('--debug-topic', dest='debug_topics', + action='append', metavar='TOPIC', default=[], + help='enable TOPIC debugging (can be specified multiple times). ' + 'The logger path is borg.debug. if TOPIC is not fully qualified.') + add_common_option('-p', '--progress', dest='progress', action='store_true', + help='show progress information') + add_common_option('--log-json', dest='log_json', action='store_true', + help='Output one JSON object per log line instead of formatted text.') + add_common_option('--lock-wait', dest='lock_wait', type=int, metavar='N', default=1, + help='wait for the lock, but max. N seconds (default: %(default)d).') + add_common_option('--show-version', dest='show_version', action='store_true', default=False, + help='show/log the borg version') + add_common_option('--show-rc', dest='show_rc', action='store_true', default=False, + help='show/log the return code (rc)') + add_common_option('--no-files-cache', dest='cache_files', action='store_false', + help='do not load/update the file metadata cache used to detect unchanged files') + add_common_option('--umask', dest='umask', type=lambda s: int(s, 8), default=UMASK_DEFAULT, metavar='M', + help='set umask to M (local and remote, default: %(default)04o)') + add_common_option('--remote-path', dest='remote_path', metavar='PATH', + help='use PATH as borg executable on the remote (default: "borg")') + add_common_option('--remote-ratelimit', dest='remote_ratelimit', type=int, metavar='rate', + help='set remote network upload rate limit in kiByte/s (default: 0=unlimited)') + add_common_option('--consider-part-files', dest='consider_part_files', + action='store_true', default=False, + help='treat part files like normal files (e.g. to list/extract them)') parser = argparse.ArgumentParser(prog=self.prog, description='Borg - Deduplicated Backups', add_help=False) - parser.common_options = CommonOptions() + parser.common_options = self.CommonOptions(define_common_options, + suffix_precedence=('_maincommand', '_midcommand', '_subcommand')) parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__, help='show version number and exit') parser.common_options.add_common_group(parser, '_maincommand', provide_defaults=True) From 0b14ed84ebcb896c4b730e6940ecb042ecdea0b7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 14 May 2017 18:34:23 +0200 Subject: [PATCH 0852/1387] docs: fix wrong heading level of "Common options" --- docs/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.rst b/docs/usage.rst index 6ba7682e..d2b0904a 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -19,7 +19,7 @@ In case you are interested in more details (like formulas), please see :ref:`json_output`. Common options -++++++++++++++ +~~~~~~~~~~~~~~ All |project_name| commands share these options: From 150ace13cd0b78c2bada307731eaa99247bc65d2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 14 May 2017 20:47:47 +0200 Subject: [PATCH 0853/1387] init: document --encryption as required --- src/borg/archiver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e07aeaee..b3bb7fa1 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2082,7 +2082,7 @@ class Archiver: This command initializes an empty repository. A repository is a filesystem directory containing the deduplicated data from zero or more archives. - Encryption can be enabled at repository init time. + Encryption can be enabled at repository init time. It cannot be changed later. It is not recommended to work without encryption. Repository encryption protects you e.g. against the case that an attacker has access to your backup repository. @@ -2137,8 +2137,8 @@ class Archiver: `authenticated` mode uses no encryption, but authenticates repository contents through the same keyed BLAKE2b-256 hash as the other blake2 modes (it uses it - as chunk ID hash). The key is stored like repokey. - This mode is new and not compatible with borg 1.0.x. + as the chunk ID hash). The key is stored like repokey. + This mode is new and *not* compatible with borg 1.0.x. `none` mode uses no encryption and no authentication. It uses sha256 as chunk ID hash. Not recommended, rather consider using an authenticated or @@ -2164,7 +2164,7 @@ class Archiver: help='repository to create') subparser.add_argument('-e', '--encryption', dest='encryption', required=True, choices=('none', 'keyfile', 'repokey', 'keyfile-blake2', 'repokey-blake2', 'authenticated'), - help='select encryption key mode') + help='select encryption key mode **(required)**') subparser.add_argument('-a', '--append-only', dest='append_only', action='store_true', help='create an append-only mode repository') From c831985bba8b6411b2e62d76fef280fc0a88d843 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 16 May 2017 19:35:15 +0200 Subject: [PATCH 0854/1387] CommonOptions: add unit test --- src/borg/archiver.py | 12 +++- src/borg/testsuite/archiver.py | 114 +++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index b3bb7fa1..ecde3f7d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1940,7 +1940,7 @@ class Archiver: else: self.common_options.setdefault(suffix, set()).add(kwargs['dest']) kwargs['dest'] += suffix - if not provide_defaults and 'default' in kwargs: + if not provide_defaults: # Interpolate help now, in case the %(default)d (or so) is mentioned, # to avoid producing incorrect help output. # Assumption: Interpolated output can safely be interpolated again, @@ -1978,7 +1978,10 @@ class Archiver: # and un-clobber the args (for tidiness - you *cannot* use the suffixed # names for other purposes, obviously). setattr(args, map_to, value) + try: delattr(args, map_from) + except AttributeError: + pass # Options with an "append" action need some special treatment. Instead of # overriding values, all specified values are merged together. @@ -1986,8 +1989,11 @@ class Archiver: option_value = [] for suffix in self.suffix_precedence: # Find values of this suffix, if any, and add them to the final list - values = getattr(args, dest + suffix, []) - option_value.extend(values) + extend_from = dest + suffix + if extend_from in args: + values = getattr(args, extend_from) + delattr(args, extend_from) + option_value.extend(values) setattr(args, dest, option_value) def build_parser(self): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 6591fdca..842b8f0a 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1,3 +1,4 @@ +import argparse import errno import json import logging @@ -2995,3 +2996,116 @@ class TestBuildFilter: assert not filter(Item(path='shallow/')) # can this even happen? paths are normalized... assert filter(Item(path='deep enough/file')) assert filter(Item(path='something/dir/file')) + + +class TestCommonOptions: + @staticmethod + def define_common_options(add_common_option): + add_common_option('-h', '--help', action='help', help='show this help message and exit') + add_common_option('--critical', dest='log_level', help='foo', + action='store_const', const='critical', default='warning') + add_common_option('--error', dest='log_level', help='foo', + action='store_const', const='error', default='warning') + add_common_option('--append', dest='append', help='foo', + action='append', metavar='TOPIC', default=[]) + add_common_option('-p', '--progress', dest='progress', action='store_true', help='foo') + add_common_option('--lock-wait', dest='lock_wait', type=int, metavar='N', default=1, + help='(default: %(default)d).') + add_common_option('--no-files-cache', dest='no_files_cache', action='store_false', help='foo') + + @pytest.fixture + def basic_parser(self): + parser = argparse.ArgumentParser(prog='test', description='test parser', add_help=False) + parser.common_options = Archiver.CommonOptions(self.define_common_options, + suffix_precedence=('_level0', '_level1')) + return parser + + @pytest.fixture + def subparsers(self, basic_parser): + return basic_parser.add_subparsers(title='required arguments', metavar='') + + @pytest.fixture + def parser(self, basic_parser): + basic_parser.common_options.add_common_group(basic_parser, '_level0', provide_defaults=True) + return basic_parser + + @pytest.fixture + def common_parser(self, parser): + common_parser = argparse.ArgumentParser(add_help=False, prog='test') + parser.common_options.add_common_group(common_parser, '_level1') + return common_parser + + @pytest.fixture + def parse_vars_from_line(self, parser, subparsers, common_parser): + subparser = subparsers.add_parser('subcommand', parents=[common_parser], add_help=False, + description='foo', epilog='bar', help='baz', + formatter_class=argparse.RawDescriptionHelpFormatter) + subparser.set_defaults(func=1234) + subparser.add_argument('--append-only', dest='append_only', action='store_true') + + def parse_vars_from_line(*line): + print(line) + args = parser.parse_args(line) + parser.common_options.resolve(args) + return vars(args) + + return parse_vars_from_line + + def test_simple(self, parse_vars_from_line): + assert parse_vars_from_line('--error') == { + 'no_files_cache': True, + 'append': [], + 'lock_wait': 1, + 'log_level': 'error', + 'progress': False + } + + assert parse_vars_from_line('--error', 'subcommand', '--critical') == { + 'no_files_cache': True, + 'append': [], + 'lock_wait': 1, + 'log_level': 'critical', + 'progress': False, + 'append_only': False, + 'func': 1234, + } + + with pytest.raises(SystemExit): + parse_vars_from_line('--append-only', 'subcommand') + + assert parse_vars_from_line('--append=foo', '--append', 'bar', 'subcommand', '--append', 'baz') == { + 'no_files_cache': True, + 'append': ['foo', 'bar', 'baz'], + 'lock_wait': 1, + 'log_level': 'warning', + 'progress': False, + 'append_only': False, + 'func': 1234, + } + + @pytest.mark.parametrize('position', ('before', 'after', 'both')) + @pytest.mark.parametrize('flag,args_key,args_value', ( + ('-p', 'progress', True), + ('--lock-wait=3', 'lock_wait', 3), + ('--no-files-cache', 'no_files_cache', False), + )) + def test_flag_position_independence(self, parse_vars_from_line, position, flag, args_key, args_value): + line = [] + if position in ('before', 'both'): + line.append(flag) + line.append('subcommand') + if position in ('after', 'both'): + line.append(flag) + + result = { + 'no_files_cache': True, + 'append': [], + 'lock_wait': 1, + 'log_level': 'warning', + 'progress': False, + 'append_only': False, + 'func': 1234, + } + result[args_key] = args_value + + assert parse_vars_from_line(*line) == result From 4c441c75b1d5d0430d7bd1298552d653cce6cadc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 15 May 2017 20:06:13 +0200 Subject: [PATCH 0855/1387] Implement --debug-profile --- src/borg/archiver.py | 19 ++++++++++++++++++- src/borg/testsuite/archiver.py | 10 ++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index ecde3f7d..e5c606d1 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2049,6 +2049,8 @@ class Archiver: add_common_option('--consider-part-files', dest='consider_part_files', action='store_true', default=False, help='treat part files like normal files (e.g. to list/extract them)') + add_common_option('--debug-profile', dest='debug_profile', default=None, metavar='FILE', + help='Store a Python profile at FILE') parser = argparse.ArgumentParser(prog=self.prog, description='Borg - Deduplicated Backups', add_help=False) @@ -3542,7 +3544,22 @@ class Archiver: self.prerun_checks(logger) if is_slow_msgpack(): logger.warning("Using a pure-python msgpack! This will result in lower performance.") - return set_ec(func(args)) + if args.debug_profile: + # Import these only when needed - avoids a further increase in startup time + import cProfile + import marshal + logger.debug('Writing execution profile to %s', args.debug_profile) + # Open the file early, before running the main program, to avoid + # a very late crash in case the specified path is invalid + with open(args.debug_profile, 'wb') as fd: + profiler = cProfile.Profile() + variables = dict(locals()) + profiler.runctx('rc = set_ec(func(args))', globals(), variables) + profiler.snapshot_stats() + marshal.dump(profiler.stats, fd) + return variables['rc'] + else: + return set_ec(func(args)) def sig_info_handler(sig_no, stack): # pragma: no cover diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 842b8f0a..e2c2be5c 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -3,6 +3,7 @@ import errno import json import logging import os +import pstats import random import shutil import socket @@ -1681,6 +1682,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert log_message['name'].startswith('borg.') assert isinstance(log_message['message'], str) + def test_debug_profile(self): + self.create_test_files() + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input', '--debug-profile=create.prof') + stats = pstats.Stats('create.prof') + stats.strip_dirs() + stats.sort_stats('cumtime') + # Ok, stats can be loaded, good enough. + def test_common_options(self): self.create_test_files() self.cmd('init', '--encryption=repokey', self.repository_location) From a07463c96a642ff923019f0a328e165a4c6da63a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 15 May 2017 23:26:44 +0200 Subject: [PATCH 0856/1387] --debug-profile: use msgpack instead of marshal by default --- scripts/msgpack2marshal.py | 18 ++++++++++++++++++ src/borg/archiver.py | 27 +++++++++++++++++++++------ src/borg/testsuite/archiver.py | 10 ++++++++-- 3 files changed, 47 insertions(+), 8 deletions(-) create mode 100755 scripts/msgpack2marshal.py diff --git a/scripts/msgpack2marshal.py b/scripts/msgpack2marshal.py new file mode 100755 index 00000000..890da1dd --- /dev/null +++ b/scripts/msgpack2marshal.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +import marshal +import sys + +import msgpack + +if len(sys.argv) not in (2, 3): + print('Synopsis:', sys.argv[0], '', '[marshal output]', file=sys.stderr) + sys.exit(1) + +if len(sys.argv) == 2: + outfile = sys.stdout +else: + outfile = open(sys.argv[2], 'wb') + +with outfile: + with open(sys.argv[1], 'rb') as infile: + marshal.dump(msgpack.unpack(infile, use_list=False, encoding='utf-8'), outfile) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e5c606d1..87bc1e25 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2050,7 +2050,8 @@ class Archiver: action='store_true', default=False, help='treat part files like normal files (e.g. to list/extract them)') add_common_option('--debug-profile', dest='debug_profile', default=None, metavar='FILE', - help='Store a Python profile at FILE') + help='Write Python profile in msgpack format into FILE. For local use a cProfile-' + 'compatible file can be generated by suffixing FILE with ".pyprof".') parser = argparse.ArgumentParser(prog=self.prog, description='Borg - Deduplicated Backups', add_help=False) @@ -3545,7 +3546,7 @@ class Archiver: if is_slow_msgpack(): logger.warning("Using a pure-python msgpack! This will result in lower performance.") if args.debug_profile: - # Import these only when needed - avoids a further increase in startup time + # Import only when needed - avoids a further increase in startup time import cProfile import marshal logger.debug('Writing execution profile to %s', args.debug_profile) @@ -3554,10 +3555,24 @@ class Archiver: with open(args.debug_profile, 'wb') as fd: profiler = cProfile.Profile() variables = dict(locals()) - profiler.runctx('rc = set_ec(func(args))', globals(), variables) - profiler.snapshot_stats() - marshal.dump(profiler.stats, fd) - return variables['rc'] + profiler.enable() + try: + return set_ec(func(args)) + finally: + profiler.disable() + profiler.snapshot_stats() + if args.debug_profile.endswith('.pyprof'): + marshal.dump(profiler.stats, fd) + else: + # We use msgpack here instead of the marshal module used by cProfile itself, + # because the latter is insecure. Since these files may be shared over the + # internet we don't want a format that is impossible to interpret outside + # an insecure implementation. + # See scripts/msgpack2marshal.py for a small script that turns a msgpack file + # into a marshal file that can be read by e.g. pyprof2calltree. + # For local use it's unnecessary hassle, though, that's why .pyprof makes + # it compatible (see above). + msgpack.pack(profiler.stats, fd, use_bin_type=True) else: return set_ec(func(args)) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index e2c2be5c..a3e22428 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1686,10 +1686,16 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_test_files() self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input', '--debug-profile=create.prof') - stats = pstats.Stats('create.prof') + stats = pstats.Stats() + with open('create.prof', 'rb') as fd: + stats.stats = msgpack.unpack(fd, use_list=False, encoding='utf-8') + stats.strip_dirs() + stats.sort_stats('cumtime') + + self.cmd('create', self.repository_location + '::test2', 'input', '--debug-profile=create.pyprof') + stats = pstats.Stats('create.pyprof') # Only do this on trusted data! stats.strip_dirs() stats.sort_stats('cumtime') - # Ok, stats can be loaded, good enough. def test_common_options(self): self.create_test_files() From d15c3f2d854192fb2737c128f0561be72a1ca3f5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 16 May 2017 21:14:27 +0200 Subject: [PATCH 0857/1387] do not test logger name, fixes #2504 looks like PyInstaller modifies the module names. --- src/borg/testsuite/archiver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 842b8f0a..f0e86030 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1678,7 +1678,6 @@ class ArchiverTestCase(ArchiverTestCaseBase): log_message = messages['log_message'] assert isinstance(log_message['time'], float) assert log_message['levelname'] == 'DEBUG' # there should only be DEBUG messages - assert log_message['name'].startswith('borg.') assert isinstance(log_message['message'], str) def test_common_options(self): From 6d6ae65be3b88682bd86aaa900d9114859968104 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 16 May 2017 23:13:31 +0200 Subject: [PATCH 0858/1387] replace external script with "borg debug convert-profile" --- scripts/msgpack2marshal.py | 18 ------------------ src/borg/archiver.py | 25 +++++++++++++++++++++++-- src/borg/testsuite/archiver.py | 5 ++--- 3 files changed, 25 insertions(+), 23 deletions(-) delete mode 100755 scripts/msgpack2marshal.py diff --git a/scripts/msgpack2marshal.py b/scripts/msgpack2marshal.py deleted file mode 100755 index 890da1dd..00000000 --- a/scripts/msgpack2marshal.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -import marshal -import sys - -import msgpack - -if len(sys.argv) not in (2, 3): - print('Synopsis:', sys.argv[0], '', '[marshal output]', file=sys.stderr) - sys.exit(1) - -if len(sys.argv) == 2: - outfile = sys.stdout -else: - outfile = open(sys.argv[2], 'wb') - -with outfile: - with open(sys.argv[1], 'rb') as infile: - marshal.dump(msgpack.unpack(infile, use_list=False, encoding='utf-8'), outfile) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 87bc1e25..cfb4ff39 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1576,6 +1576,13 @@ class Archiver: print("object %s not found [info from chunks cache]." % hex_id) return EXIT_SUCCESS + def do_debug_convert_profile(self, args): + """convert Borg profile to Python profile""" + import marshal + with args.output, args.input: + marshal.dump(msgpack.unpack(args.input, use_list=False, encoding='utf-8'), args.output) + return EXIT_SUCCESS + @with_repository(lock=False, manifest=False) def do_break_lock(self, args, repository): """Break the repository lock (e.g. in case it was left by a dead borg.""" @@ -2050,7 +2057,7 @@ class Archiver: action='store_true', default=False, help='treat part files like normal files (e.g. to list/extract them)') add_common_option('--debug-profile', dest='debug_profile', default=None, metavar='FILE', - help='Write Python profile in msgpack format into FILE. For local use a cProfile-' + help='Write execution profile in Borg format into FILE. For local use a Python-' 'compatible file can be generated by suffixing FILE with ".pyprof".') parser = argparse.ArgumentParser(prog=self.prog, description='Borg - Deduplicated Backups', @@ -3390,6 +3397,20 @@ class Archiver: subparser.add_argument('ids', metavar='IDs', nargs='+', type=str, help='hex object ID(s) to show refcounts for') + debug_convert_profile_epilog = process_epilog(""" + Convert a Borg profile to a Python cProfile compatible profile. + """) + subparser = debug_parsers.add_parser('convert-profile', parents=[common_parser], add_help=False, + description=self.do_debug_convert_profile.__doc__, + epilog=debug_convert_profile_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='convert Borg profile to Python profile (debug)') + subparser.set_defaults(func=self.do_debug_convert_profile) + subparser.add_argument('input', metavar='INPUT', type=argparse.FileType('rb'), + help='Borg profile') + subparser.add_argument('output', metavar='OUTPUT', type=argparse.FileType('wb'), + help='Output file') + benchmark_epilog = process_epilog("These commands do various benchmarks.") subparser = subparsers.add_parser('benchmark', parents=[mid_common_parser], add_help=False, @@ -3551,7 +3572,7 @@ class Archiver: import marshal logger.debug('Writing execution profile to %s', args.debug_profile) # Open the file early, before running the main program, to avoid - # a very late crash in case the specified path is invalid + # a very late crash in case the specified path is invalid. with open(args.debug_profile, 'wb') as fd: profiler = cProfile.Profile() variables = dict(locals()) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index a3e22428..d41d3b7b 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1686,9 +1686,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_test_files() self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input', '--debug-profile=create.prof') - stats = pstats.Stats() - with open('create.prof', 'rb') as fd: - stats.stats = msgpack.unpack(fd, use_list=False, encoding='utf-8') + self.cmd('debug', 'convert-profile', 'create.prof', 'create.pyprof') + stats = pstats.Stats('create.pyprof') stats.strip_dirs() stats.sort_stats('cumtime') From 054897c8724e1933d1ed3a1ec537046f30968e18 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 16 May 2017 23:18:48 +0200 Subject: [PATCH 0859/1387] docs: debugging facilities --- docs/usage.rst | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index d2b0904a..bde9a995 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -505,8 +505,8 @@ Miscellaneous Help .. include:: usage/help.rst.inc -Debug Commands --------------- +Debugging Facilities +-------------------- There is a ``borg debug`` command that has some subcommands which are all **not intended for normal use** and **potentially very dangerous** if used incorrectly. @@ -525,6 +525,20 @@ They exist to improve debugging capabilities without direct system access, e.g. in case you ever run into some severe malfunction. Use them only if you know what you are doing or if a trusted |project_name| developer tells you what to do. +Borg has a ``--debug-topic TOPIC`` option to enable specific debugging messages. Topics +are generally not documented. + +A ``--debug-profile FILE`` option exists which writes a profile of the main program's +execution to a file. The format of these files is not directly compatible with the +Python profiling tools, since these use the "marshal" format, which is not intended +to be secure (quoting the Python docs: "Never unmarshal data received from an untrusted +or unauthenticated source."). + +The ``borg debug profile-convert`` command can be used to take a Borg profile and convert +it to a profile file that is compatible with the Python tools. + +Additionally, if the filename specified for ``--debug-profile`` ends with ".pyprof" a +Python compatible profile is generated. This is only intended for local use by developers. Additional Notes ---------------- From 5788219ff43de193f89d4fa48afd5c8638d4efb9 Mon Sep 17 00:00:00 2001 From: enkore Date: Wed, 17 May 2017 00:09:41 +0200 Subject: [PATCH 0860/1387] borg export-tar (#2519) --- docs/man/borg-export-tar.1 | 142 +++++++++++++++++ docs/usage.rst | 18 +++ docs/usage/export-tar.rst.inc | 73 +++++++++ src/borg/archiver.py | 281 +++++++++++++++++++++++++++++++++ src/borg/helpers.py | 13 +- src/borg/testsuite/__init__.py | 17 +- src/borg/testsuite/archiver.py | 37 +++++ 7 files changed, 574 insertions(+), 7 deletions(-) create mode 100644 docs/man/borg-export-tar.1 create mode 100644 docs/usage/export-tar.rst.inc diff --git a/docs/man/borg-export-tar.1 b/docs/man/borg-export-tar.1 new file mode 100644 index 00000000..ecbefc14 --- /dev/null +++ b/docs/man/borg-export-tar.1 @@ -0,0 +1,142 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-EXPORT-TAR 1 "2017-05-16" "" "borg backup tool" +.SH NAME +borg-export-tar \- Export archive contents as a tarball +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg export\-tar ARCHIVE FILE PATH +.SH DESCRIPTION +.sp +This command creates a tarball from an archive. +.sp +When giving \(aq\-\(aq as the output FILE, Borg will write a tar stream to standard output. +.sp +By default (\-\-tar\-filter=auto) Borg will detect whether the FILE should be compressed +based on its file extension and pipe the tarball through an appropriate filter +before writing it to FILE: +.INDENT 0.0 +.IP \(bu 2 +\&.tar.gz: gzip +.IP \(bu 2 +\&.tar.bz2: bzip2 +.IP \(bu 2 +\&.tar.xz: xz +.UNINDENT +.sp +Alternatively a \-\-tar\-filter program may be explicitly specified. It should +read the uncompressed tar stream from stdin and write a compressed/filtered +tar stream to stdout. +.sp +The generated tarball uses the GNU tar format. +.sp +export\-tar is a lossy conversion: +BSD flags, ACLs, extended attributes (xattrs), atime and ctime are not exported. +Timestamp resolution is limited to whole seconds, not the nanosecond resolution +otherwise supported by Borg. +.sp +A \-\-sparse option (as found in borg extract) is not supported. +.sp +By default the entire archive is extracted but a subset of files and directories +can be selected by passing a list of \fBPATHs\fP as arguments. +The file selection can further be restricted by using the \fB\-\-exclude\fP option. +.sp +See the output of the "borg help patterns" command for more help on exclude patterns. +.sp +\fB\-\-progress\fP can be slower than no progress display, since it makes one additional +pass over the archive metadata. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B ARCHIVE +archive to export +.TP +.B FILE +output tar file. "\-" to write to stdout instead. +.TP +.B PATH +paths to extract; patterns are supported +.UNINDENT +.SS optional arguments +.INDENT 0.0 +.TP +.B \-\-tar\-filter +filter program to pipe data through +.TP +.B \-\-list +output verbose list of items (files, dirs, ...) +.TP +.BI \-e \ PATTERN\fP,\fB \ \-\-exclude \ PATTERN +exclude paths matching PATTERN +.TP +.BI \-\-exclude\-from \ EXCLUDEFILE +read exclude patterns from EXCLUDEFILE, one per line +.TP +.BI \-\-pattern \ PATTERN +include/exclude paths matching PATTERN +.TP +.BI \-\-patterns\-from \ PATTERNFILE +read include/exclude patterns from PATTERNFILE, one per line +.TP +.BI \-\-strip\-components \ NUMBER +Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. +.UNINDENT +.SH EXAMPLES +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# export as uncompressed tar +$ borg export\-tar /path/to/repo::Monday Monday.tar + +# exclude some types, compress using gzip +$ borg export\-tar /path/to/repo::Monday Monday.tar.gz \-\-exclude \(aq*.so\(aq + +# use higher compression level with gzip +$ borg export\-tar testrepo::linux \-\-tar\-filter="gzip \-9" Monday.tar.gz + +# export a gzipped tar, but instead of storing it on disk, +# upload it to a remote site using curl. +$ borg export\-tar ... \-\-tar\-filter="gzip" \- | curl \-\-data\-binary @\- https://somewhere/to/POST +.ft P +.fi +.UNINDENT +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/usage.rst b/docs/usage.rst index d2b0904a..33c14535 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -492,6 +492,24 @@ Examples Comment: This is a better comment ... +.. include:: usage/export-tar.rst.inc + +Examples +~~~~~~~~ +:: + + # export as uncompressed tar + $ borg export-tar /path/to/repo::Monday Monday.tar + + # exclude some types, compress using gzip + $ borg export-tar /path/to/repo::Monday Monday.tar.gz --exclude '*.so' + + # use higher compression level with gzip + $ borg export-tar testrepo::linux --tar-filter="gzip -9" Monday.tar.gz + + # export a gzipped tar, but instead of storing it on disk, + # upload it to a remote site using curl. + $ borg export-tar ... --tar-filter="gzip" - | curl --data-binary @- https://somewhere/to/POST .. include:: usage/with-lock.rst.inc diff --git a/docs/usage/export-tar.rst.inc b/docs/usage/export-tar.rst.inc new file mode 100644 index 00000000..af5fb545 --- /dev/null +++ b/docs/usage/export-tar.rst.inc @@ -0,0 +1,73 @@ +.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! + +.. _borg_export-tar: + +borg export-tar +--------------- +:: + + borg export-tar ARCHIVE FILE PATH + +positional arguments + ARCHIVE + archive to export + FILE + output tar file. "-" to write to stdout instead. + PATH + paths to extract; patterns are supported + +optional arguments + ``--tar-filter`` + | filter program to pipe data through + ``--list`` + | output verbose list of items (files, dirs, ...) + ``-e PATTERN``, ``--exclude PATTERN`` + | exclude paths matching PATTERN + ``--exclude-from EXCLUDEFILE`` + | read exclude patterns from EXCLUDEFILE, one per line + ``--pattern PATTERN`` + | include/exclude paths matching PATTERN + ``--patterns-from PATTERNFILE`` + | read include/exclude patterns from PATTERNFILE, one per line + ``--strip-components NUMBER`` + | Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. + +`Common options`_ + | + +Description +~~~~~~~~~~~ + +This command creates a tarball from an archive. + +When giving '-' as the output FILE, Borg will write a tar stream to standard output. + +By default (--tar-filter=auto) Borg will detect whether the FILE should be compressed +based on its file extension and pipe the tarball through an appropriate filter +before writing it to FILE: + +- .tar.gz: gzip +- .tar.bz2: bzip2 +- .tar.xz: xz + +Alternatively a --tar-filter program may be explicitly specified. It should +read the uncompressed tar stream from stdin and write a compressed/filtered +tar stream to stdout. + +The generated tarball uses the GNU tar format. + +export-tar is a lossy conversion: +BSD flags, ACLs, extended attributes (xattrs), atime and ctime are not exported. +Timestamp resolution is limited to whole seconds, not the nanosecond resolution +otherwise supported by Borg. + +A --sparse option (as found in borg extract) is not supported. + +By default the entire archive is extracted but a subset of files and directories +can be selected by passing a list of ``PATHs`` as arguments. +The file selection can further be restricted by using the ``--exclude`` option. + +See the output of the "borg help patterns" command for more help on exclude patterns. + +``--progress`` can be slower than no progress display, since it makes one additional +pass over the archive metadata. \ No newline at end of file diff --git a/src/borg/archiver.py b/src/borg/archiver.py index ecde3f7d..80a0dc4b 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -15,6 +15,7 @@ import signal import stat import subprocess import sys +import tarfile import textwrap import time import traceback @@ -61,6 +62,7 @@ from .helpers import ErrorIgnoringTextIOWrapper from .helpers import ProgressIndicatorPercent from .helpers import basic_json_data, json_print from .helpers import replace_placeholders +from .helpers import ChunkIteratorFileWrapper from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern from .patterns import PatternMatcher from .item import Item @@ -694,6 +696,219 @@ class Archiver: pi.finish() return self.exit_code + @with_repository() + @with_archive + def do_export_tar(self, args, repository, manifest, key, archive): + """Export archive contents as a tarball""" + self.output_list = args.output_list + + # A quick note about the general design of tar_filter and tarfile; + # The tarfile module of Python can provide some compression mechanisms + # by itself, using the builtin gzip, bz2 and lzma modules (and "tarmodes" + # such as "w:xz"). + # + # Doing so would have three major drawbacks: + # For one the compressor runs on the same thread as the program using the + # tarfile, stealing valuable CPU time from Borg and thus reducing throughput. + # Then this limits the available options - what about lz4? Brotli? zstd? + # The third issue is that systems can ship more optimized versions than those + # built into Python, e.g. pigz or pxz, which can use more than one thread for + # compression. + # + # Therefore we externalize compression by using a filter program, which has + # none of these drawbacks. The only issue of using an external filter is + # that it has to be installed -- hardly a problem, considering that + # the decompressor must be installed as well to make use of the exported tarball! + + filter = None + if args.tar_filter == 'auto': + # Note that filter remains None if tarfile is '-'. + if args.tarfile.endswith('.tar.gz'): + filter = 'gzip' + elif args.tarfile.endswith('.tar.bz2'): + filter = 'bzip2' + elif args.tarfile.endswith('.tar.xz'): + filter = 'xz' + logger.debug('Automatically determined tar filter: %s', filter) + else: + filter = args.tar_filter + + if args.tarfile == '-': + tarstream, tarstream_close = sys.stdout.buffer, False + else: + tarstream, tarstream_close = open(args.tarfile, 'wb'), True + + if filter: + # When we put a filter between us and the final destination, + # the selected output (tarstream until now) becomes the output of the filter (=filterout). + # The decision whether to close that or not remains the same. + filterout = tarstream + filterout_close = tarstream_close + # There is no deadlock potential here (the subprocess docs warn about this), because + # communication with the process is a one-way road, i.e. the process can never block + # for us to do something while we block on the process for something different. + filtercmd = shlex.split(filter) + logger.debug('--tar-filter command line: %s', filtercmd) + filterproc = subprocess.Popen(filtercmd, stdin=subprocess.PIPE, stdout=filterout) + # Always close the pipe, otherwise the filter process would not notice when we are done. + tarstream = filterproc.stdin + tarstream_close = True + + # The | (pipe) symbol instructs tarfile to use a streaming mode of operation + # where it never seeks on the passed fileobj. + tar = tarfile.open(fileobj=tarstream, mode='w|') + + self._export_tar(args, archive, tar) + + # This does not close the fileobj (tarstream) we passed to it -- a side effect of the | mode. + tar.close() + + if tarstream_close: + tarstream.close() + + if filter: + logger.debug('Done creating tar, waiting for filter to die...') + rc = filterproc.wait() + if rc: + logger.error('--tar-filter exited with code %d, output file is likely unusable!', rc) + self.exit_code = set_ec(EXIT_ERROR) + else: + logger.debug('filter exited with code %d', rc) + + if filterout_close: + filterout.close() + + return self.exit_code + + def _export_tar(self, args, archive, tar): + matcher = self.build_matcher(args.patterns, args.paths) + + progress = args.progress + output_list = args.output_list + strip_components = args.strip_components + partial_extract = not matcher.empty() or strip_components + hardlink_masters = {} if partial_extract else None + + def peek_and_store_hardlink_masters(item, matched): + if (partial_extract and not matched and hardlinkable(item.mode) and + item.get('hardlink_master', True) and 'source' not in item): + hardlink_masters[item.get('path')] = (item.get('chunks'), None) + + filter = self.build_filter(matcher, peek_and_store_hardlink_masters, strip_components) + + if progress: + pi = ProgressIndicatorPercent(msg='%5.1f%% Processing: %s', step=0.1, msgid='extract') + pi.output('Calculating size') + extracted_size = sum(item.get_size(hardlink_masters) for item in archive.iter_items(filter)) + pi.total = extracted_size + else: + pi = None + + def item_content_stream(item): + """ + Return a file-like object that reads from the chunks of *item*. + """ + chunk_iterator = archive.pipeline.fetch_many([chunk_id for chunk_id, _, _ in item.chunks]) + if pi: + info = [remove_surrogates(item.path)] + return ChunkIteratorFileWrapper(chunk_iterator, + lambda read_bytes: pi.show(increase=len(read_bytes), info=info)) + else: + return ChunkIteratorFileWrapper(chunk_iterator) + + def item_to_tarinfo(item, original_path): + """ + Transform a Borg *item* into a tarfile.TarInfo object. + + Return a tuple (tarinfo, stream), where stream may be a file-like object that represents + the file contents, if any, and is None otherwise. When *tarinfo* is None, the *item* + cannot be represented as a TarInfo object and should be skipped. + """ + + # If we would use the PAX (POSIX) format (which we currently don't), + # we can support most things that aren't possible with classic tar + # formats, including GNU tar, such as: + # atime, ctime, possibly Linux capabilities (security.* xattrs) + # and various additions supported by GNU tar in POSIX mode. + + stream = None + tarinfo = tarfile.TarInfo() + tarinfo.name = item.path + tarinfo.mtime = item.mtime / 1e9 + tarinfo.mode = stat.S_IMODE(item.mode) + tarinfo.uid = item.uid + tarinfo.gid = item.gid + tarinfo.uname = item.user or '' + tarinfo.gname = item.group or '' + # The linkname in tar has the same dual use the 'source' attribute of Borg items, + # i.e. for symlinks it means the destination, while for hardlinks it refers to the + # file. + # Since hardlinks in tar have a different type code (LNKTYPE) the format might + # support hardlinking arbitrary objects (including symlinks and directories), but + # whether implementations actually support that is a whole different question... + tarinfo.linkname = "" + + modebits = stat.S_IFMT(item.mode) + if modebits == stat.S_IFREG: + tarinfo.type = tarfile.REGTYPE + if 'source' in item: + source = os.sep.join(item.source.split(os.sep)[strip_components:]) + if hardlink_masters is None: + linkname = source + else: + chunks, linkname = hardlink_masters.get(item.source, (None, source)) + if linkname: + # Master was already added to the archive, add a hardlink reference to it. + tarinfo.type = tarfile.LNKTYPE + tarinfo.linkname = linkname + elif chunks is not None: + # The item which has the chunks was not put into the tar, therefore + # we do that now and update hardlink_masters to reflect that. + item.chunks = chunks + tarinfo.size = item.get_size() + stream = item_content_stream(item) + hardlink_masters[item.get('source') or original_path] = (None, item.path) + else: + tarinfo.size = item.get_size() + stream = item_content_stream(item) + elif modebits == stat.S_IFDIR: + tarinfo.type = tarfile.DIRTYPE + elif modebits == stat.S_IFLNK: + tarinfo.type = tarfile.SYMTYPE + tarinfo.linkname = item.source + elif modebits == stat.S_IFBLK: + tarinfo.type = tarfile.BLKTYPE + tarinfo.devmajor = os.major(item.rdev) + tarinfo.devminor = os.minor(item.rdev) + elif modebits == stat.S_IFCHR: + tarinfo.type = tarfile.CHRTYPE + tarinfo.devmajor = os.major(item.rdev) + tarinfo.devminor = os.minor(item.rdev) + elif modebits == stat.S_IFIFO: + tarinfo.type = tarfile.FIFOTYPE + else: + self.print_warning('%s: unsupported file type %o for tar export', remove_surrogates(item.path), modebits) + set_ec(EXIT_WARNING) + return None, stream + return tarinfo, stream + + for item in archive.iter_items(filter, preload=True): + orig_path = item.path + if strip_components: + item.path = os.sep.join(orig_path.split(os.sep)[strip_components:]) + tarinfo, stream = item_to_tarinfo(item, orig_path) + if tarinfo: + if output_list: + logging.getLogger('borg.output.list').info(remove_surrogates(orig_path)) + tar.addfile(tarinfo, stream) + + if pi: + pi.finish() + + for pattern in matcher.get_unmatched_include_patterns(): + self.print_warning("Include pattern '%s' never matched.", pattern) + return self.exit_code + @with_repository() @with_archive def do_diff(self, args, repository, manifest, key, archive): @@ -2605,6 +2820,72 @@ class Archiver: subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to extract; patterns are supported') + export_tar_epilog = process_epilog(""" + This command creates a tarball from an archive. + + When giving '-' as the output FILE, Borg will write a tar stream to standard output. + + By default (--tar-filter=auto) Borg will detect whether the FILE should be compressed + based on its file extension and pipe the tarball through an appropriate filter + before writing it to FILE: + + - .tar.gz: gzip + - .tar.bz2: bzip2 + - .tar.xz: xz + + Alternatively a --tar-filter program may be explicitly specified. It should + read the uncompressed tar stream from stdin and write a compressed/filtered + tar stream to stdout. + + The generated tarball uses the GNU tar format. + + export-tar is a lossy conversion: + BSD flags, ACLs, extended attributes (xattrs), atime and ctime are not exported. + Timestamp resolution is limited to whole seconds, not the nanosecond resolution + otherwise supported by Borg. + + A --sparse option (as found in borg extract) is not supported. + + By default the entire archive is extracted but a subset of files and directories + can be selected by passing a list of ``PATHs`` as arguments. + The file selection can further be restricted by using the ``--exclude`` option. + + See the output of the "borg help patterns" command for more help on exclude patterns. + + ``--progress`` can be slower than no progress display, since it makes one additional + pass over the archive metadata. + """) + subparser = subparsers.add_parser('export-tar', parents=[common_parser], add_help=False, + description=self.do_export_tar.__doc__, + epilog=export_tar_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='create tarball from archive') + subparser.set_defaults(func=self.do_export_tar) + subparser.add_argument('--tar-filter', dest='tar_filter', default='auto', + help='filter program to pipe data through') + subparser.add_argument('--list', dest='output_list', + action='store_true', default=False, + help='output verbose list of items (files, dirs, ...)') + subparser.add_argument('-e', '--exclude', dest='patterns', + type=parse_exclude_pattern, action='append', + metavar="PATTERN", help='exclude paths matching PATTERN') + subparser.add_argument('--exclude-from', action=ArgparseExcludeFileAction, + metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') + subparser.add_argument('--pattern', action=ArgparsePatternAction, + metavar="PATTERN", help='include/exclude paths matching PATTERN') + subparser.add_argument('--patterns-from', action=ArgparsePatternFileAction, + metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line') + subparser.add_argument('--strip-components', dest='strip_components', + type=int, default=0, metavar='NUMBER', + help='Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped.') + subparser.add_argument('location', metavar='ARCHIVE', + type=location_validator(archive=True), + help='archive to export') + subparser.add_argument('tarfile', metavar='FILE', + help='output tar file. "-" to write to stdout instead.') + subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, + help='paths to extract; patterns are supported') + diff_epilog = process_epilog(""" This command finds differences (file contents, user/group/mode) between archives. diff --git a/src/borg/helpers.py b/src/borg/helpers.py index c3027143..b67250eb 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1622,11 +1622,20 @@ class ItemFormatter(BaseFormatter): class ChunkIteratorFileWrapper: """File-like wrapper for chunk iterators""" - def __init__(self, chunk_iterator): + def __init__(self, chunk_iterator, read_callback=None): + """ + *chunk_iterator* should be an iterator yielding bytes. These will be buffered + internally as necessary to satisfy .read() calls. + + *read_callback* will be called with one argument, some byte string that has + just been read and will be subsequently returned to a caller of .read(). + It can be used to update a progress display. + """ self.chunk_iterator = chunk_iterator self.chunk_offset = 0 self.chunk = b'' self.exhausted = False + self.read_callback = read_callback def _refill(self): remaining = len(self.chunk) - self.chunk_offset @@ -1655,6 +1664,8 @@ class ChunkIteratorFileWrapper: read_data = self._read(nbytes) nbytes -= len(read_data) parts.append(read_data) + if self.read_callback: + self.read_callback(read_data) return b''.join(parts) diff --git a/src/borg/testsuite/__init__.py b/src/borg/testsuite/__init__.py index fea632cb..38f5d4ab 100644 --- a/src/borg/testsuite/__init__.py +++ b/src/borg/testsuite/__init__.py @@ -150,7 +150,7 @@ class BaseTestCase(unittest.TestCase): diff = filecmp.dircmp(dir1, dir2) self._assert_dirs_equal_cmp(diff, **kwargs) - def _assert_dirs_equal_cmp(self, diff, ignore_bsdflags=False, ignore_xattrs=False): + def _assert_dirs_equal_cmp(self, diff, ignore_bsdflags=False, ignore_xattrs=False, ignore_ns=False): self.assert_equal(diff.left_only, []) self.assert_equal(diff.right_only, []) self.assert_equal(diff.diff_files, []) @@ -162,25 +162,30 @@ class BaseTestCase(unittest.TestCase): s2 = os.lstat(path2) # Assume path2 is on FUSE if st_dev is different fuse = s1.st_dev != s2.st_dev - attrs = ['st_mode', 'st_uid', 'st_gid', 'st_rdev'] + attrs = ['st_uid', 'st_gid', 'st_rdev'] if not fuse or not os.path.isdir(path1): # dir nlink is always 1 on our fuse filesystem attrs.append('st_nlink') d1 = [filename] + [getattr(s1, a) for a in attrs] d2 = [filename] + [getattr(s2, a) for a in attrs] + d1.insert(1, oct(s1.st_mode)) + d2.insert(1, oct(s2.st_mode)) if not ignore_bsdflags: d1.append(get_flags(path1, s1)) d2.append(get_flags(path2, s2)) # ignore st_rdev if file is not a block/char device, fixes #203 - if not stat.S_ISCHR(d1[1]) and not stat.S_ISBLK(d1[1]): + if not stat.S_ISCHR(s1.st_mode) and not stat.S_ISBLK(s1.st_mode): d1[4] = None - if not stat.S_ISCHR(d2[1]) and not stat.S_ISBLK(d2[1]): + if not stat.S_ISCHR(s2.st_mode) and not stat.S_ISBLK(s2.st_mode): d2[4] = None # If utime isn't fully supported, borg can't set mtime. # Therefore, we shouldn't test it in that case. if is_utime_fully_supported(): # Older versions of llfuse do not support ns precision properly - if fuse and not have_fuse_mtime_ns: + if ignore_ns: + d1.append(int(s1.st_mtime_ns / 1e9)) + d2.append(int(s2.st_mtime_ns / 1e9)) + elif fuse and not have_fuse_mtime_ns: d1.append(round(s1.st_mtime_ns, -4)) d2.append(round(s2.st_mtime_ns, -4)) else: @@ -191,7 +196,7 @@ class BaseTestCase(unittest.TestCase): d2.append(no_selinux(get_all(path2, follow_symlinks=False))) self.assert_equal(d1, d2) for sub_diff in diff.subdirs.values(): - self._assert_dirs_equal_cmp(sub_diff, ignore_bsdflags=ignore_bsdflags, ignore_xattrs=ignore_xattrs) + self._assert_dirs_equal_cmp(sub_diff, ignore_bsdflags=ignore_bsdflags, ignore_xattrs=ignore_xattrs, ignore_ns=ignore_ns) @contextmanager def fuse_mount(self, location, mountpoint, *options): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index f0e86030..dd3285fd 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -96,6 +96,14 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw): sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr +def have_gnutar(): + if not shutil.which('tar'): + return False + popen = subprocess.Popen(['tar', '--version'], stdout=subprocess.PIPE) + stdout, stderr = popen.communicate() + return b'GNU tar' in stdout + + # check if the binary "borg.exe" is available (for local testing a symlink to virtualenv/bin/borg should do) try: exec_cmd('help', exe='borg.exe', fork=True) @@ -2354,6 +2362,35 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 assert '_meta' in result assert '_items' in result + requires_gnutar = pytest.mark.skipif(not have_gnutar(), reason='GNU tar must be installed for this test.') + requires_gzip = pytest.mark.skipif(not shutil.which('gzip'), reason='gzip must be installed for this test.') + + @requires_gnutar + def test_export_tar(self): + self.create_test_files() + os.unlink('input/flagfile') + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + self.cmd('export-tar', self.repository_location + '::test', 'simple.tar') + with changedir('output'): + # This probably assumes GNU tar. Note -p switch to extract permissions regardless of umask. + subprocess.check_output(['tar', 'xpf', '../simple.tar']) + self.assert_dirs_equal('input', 'output/input', ignore_bsdflags=True, ignore_xattrs=True, ignore_ns=True) + + @requires_gnutar + @requires_gzip + def test_export_tar_gz(self): + if not shutil.which('gzip'): + pytest.skip('gzip is not installed') + self.create_test_files() + os.unlink('input/flagfile') + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + self.cmd('export-tar', self.repository_location + '::test', 'simple.tar.gz') + with changedir('output'): + subprocess.check_output(['tar', 'xpf', '../simple.tar.gz']) + self.assert_dirs_equal('input', 'output/input', ignore_bsdflags=True, ignore_xattrs=True, ignore_ns=True) + @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available') class ArchiverTestCaseBinary(ArchiverTestCase): From 293324810b165d64e6f3ae3ed88b7caedd759ff5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 17 May 2017 10:54:39 +0200 Subject: [PATCH 0861/1387] introduce popen_with_error_handling to handle common user errors (without tracebacks) --- src/borg/archiver.py | 7 ++++--- src/borg/helpers.py | 33 +++++++++++++++++++++++++++++++++ src/borg/testsuite/helpers.py | 27 +++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 7e316d07..8d8302dd 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -63,6 +63,7 @@ from .helpers import ProgressIndicatorPercent from .helpers import basic_json_data, json_print from .helpers import replace_placeholders from .helpers import ChunkIteratorFileWrapper +from .helpers import popen_with_error_handling from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern from .patterns import PatternMatcher from .item import Item @@ -747,9 +748,9 @@ class Archiver: # There is no deadlock potential here (the subprocess docs warn about this), because # communication with the process is a one-way road, i.e. the process can never block # for us to do something while we block on the process for something different. - filtercmd = shlex.split(filter) - logger.debug('--tar-filter command line: %s', filtercmd) - filterproc = subprocess.Popen(filtercmd, stdin=subprocess.PIPE, stdout=filterout) + filterproc = popen_with_error_handling(filter, stdin=subprocess.PIPE, stdout=filterout, log_prefix='--tar-filter: ') + if not filterproc: + return EXIT_ERROR # Always close the pipe, otherwise the filter process would not notice when we are done. tarstream = filterproc.stdin tarstream_close = True diff --git a/src/borg/helpers.py b/src/borg/helpers.py index b67250eb..ec06946e 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -11,9 +11,11 @@ import os.path import platform import pwd import re +import shlex import signal import socket import stat +import subprocess import sys import textwrap import threading @@ -1962,3 +1964,34 @@ def secure_erase(path): fd.flush() os.fsync(fd.fileno()) os.unlink(path) + + +def popen_with_error_handling(cmd_line: str, log_prefix='', **kwargs): + """ + Handle typical errors raised by subprocess.Popen. Return None if an error occurred, + otherwise return the Popen object. + + *cmd_line* is split using shlex (e.g. 'gzip -9' => ['gzip', '-9']). + + Log messages will be prefixed with *log_prefix*; if set, it should end with a space + (e.g. log_prefix='--some-option: '). + + Does not change the exit code. + """ + assert not kwargs.get('shell'), 'Sorry pal, shell mode is a no-no' + try: + command = shlex.split(cmd_line) + if not command: + raise ValueError('an empty command line is not permitted') + except ValueError as ve: + logger.error('%s%s', log_prefix, ve) + return + logger.debug('%scommand line: %s', log_prefix, command) + try: + return subprocess.Popen(command, **kwargs) + except FileNotFoundError: + logger.error('%sexecutable not found: %s', log_prefix, command[0]) + return + except PermissionError: + logger.error('%spermission denied: %s', log_prefix, command[0]) + return diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 7eb42116..ff6b5efe 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -2,6 +2,7 @@ import argparse import hashlib import io import os +import shutil import sys from datetime import datetime, timezone, timedelta from time import mktime, strptime, sleep @@ -26,6 +27,7 @@ from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless from ..helpers import swidth_slice from ..helpers import chunkit from ..helpers import safe_ns, safe_s, SUPPORT_32BIT_PLATFORMS +from ..helpers import popen_with_error_handling from . import BaseTestCase, FakeInputs @@ -816,3 +818,28 @@ def test_safe_timestamps(): datetime.utcfromtimestamp(beyond_y10k) assert datetime.utcfromtimestamp(safe_s(beyond_y10k)) > datetime(2262, 1, 1) assert datetime.utcfromtimestamp(safe_ns(beyond_y10k) / 1000000000) > datetime(2262, 1, 1) + + +class TestPopenWithErrorHandling: + @pytest.mark.skipif(not shutil.which('test'), reason='"test" binary is needed') + def test_simple(self): + proc = popen_with_error_handling('test 1') + assert proc.wait() == 0 + + @pytest.mark.skipif(shutil.which('borg-foobar-test-notexist'), reason='"borg-foobar-test-notexist" binary exists (somehow?)') + def test_not_found(self): + proc = popen_with_error_handling('borg-foobar-test-notexist 1234') + assert proc is None + + @pytest.mark.parametrize('cmd', ( + 'mismatched "quote', + 'foo --bar="baz', + '' + )) + def test_bad_syntax(self, cmd): + proc = popen_with_error_handling(cmd) + assert proc is None + + def test_shell(self): + with pytest.raises(AssertionError): + popen_with_error_handling('', shell=True) From 042a4b960bd2dc5dae8e631b6974d5f49bdf8189 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 17 May 2017 11:04:20 +0200 Subject: [PATCH 0862/1387] export-tar: test strip-components and hardlinks for partial export --- src/borg/archiver.py | 2 +- src/borg/testsuite/archiver.py | 56 ++++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 8d8302dd..0ddfe1fe 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -772,7 +772,7 @@ class Archiver: rc = filterproc.wait() if rc: logger.error('--tar-filter exited with code %d, output file is likely unusable!', rc) - self.exit_code = set_ec(EXIT_ERROR) + self.exit_code = EXIT_ERROR else: logger.debug('filter exited with code %d', rc) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index dfe6e75c..29460189 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -728,7 +728,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') - @pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported') + requires_hardlinks = pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported') + + @requires_hardlinks def test_strip_components_links(self): self._extract_hardlinks_setup() with changedir('output'): @@ -741,7 +743,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('extract', self.repository_location + '::test') assert os.stat('input/dir1/hardlink').st_nlink == 4 - @pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported') + @requires_hardlinks def test_extract_hardlinks(self): self._extract_hardlinks_setup() with changedir('output'): @@ -2386,10 +2388,10 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 os.unlink('input/flagfile') self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') - self.cmd('export-tar', self.repository_location + '::test', 'simple.tar') + self.cmd('export-tar', self.repository_location + '::test', 'simple.tar', '--progress') with changedir('output'): # This probably assumes GNU tar. Note -p switch to extract permissions regardless of umask. - subprocess.check_output(['tar', 'xpf', '../simple.tar']) + subprocess.check_call(['tar', 'xpf', '../simple.tar']) self.assert_dirs_equal('input', 'output/input', ignore_bsdflags=True, ignore_xattrs=True, ignore_ns=True) @requires_gnutar @@ -2401,11 +2403,53 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 os.unlink('input/flagfile') self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') - self.cmd('export-tar', self.repository_location + '::test', 'simple.tar.gz') + list = self.cmd('export-tar', self.repository_location + '::test', 'simple.tar.gz', '--list') + assert 'input/file1\n' in list + assert 'input/dir2\n' in list with changedir('output'): - subprocess.check_output(['tar', 'xpf', '../simple.tar.gz']) + subprocess.check_call(['tar', 'xpf', '../simple.tar.gz']) self.assert_dirs_equal('input', 'output/input', ignore_bsdflags=True, ignore_xattrs=True, ignore_ns=True) + @requires_gnutar + def test_export_tar_strip_components(self): + if not shutil.which('gzip'): + pytest.skip('gzip is not installed') + self.create_test_files() + os.unlink('input/flagfile') + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + list = self.cmd('export-tar', self.repository_location + '::test', 'simple.tar', '--strip-components=1', '--list') + # --list's path are those before processing with --strip-components + assert 'input/file1\n' in list + assert 'input/dir2\n' in list + with changedir('output'): + subprocess.check_call(['tar', 'xpf', '../simple.tar']) + self.assert_dirs_equal('input', 'output/', ignore_bsdflags=True, ignore_xattrs=True, ignore_ns=True) + + @requires_hardlinks + @requires_gnutar + def test_export_tar_strip_components_links(self): + self._extract_hardlinks_setup() + self.cmd('export-tar', self.repository_location + '::test', 'output.tar', '--strip-components=2') + with changedir('output'): + subprocess.check_call(['tar', 'xpf', '../output.tar']) + assert os.stat('hardlink').st_nlink == 2 + assert os.stat('subdir/hardlink').st_nlink == 2 + assert os.stat('aaaa').st_nlink == 2 + assert os.stat('source2').st_nlink == 2 + + @requires_hardlinks + @requires_gnutar + def test_extract_hardlinks(self): + self._extract_hardlinks_setup() + self.cmd('export-tar', self.repository_location + '::test', 'output.tar', 'input/dir1') + with changedir('output'): + subprocess.check_call(['tar', 'xpf', '../output.tar']) + assert os.stat('input/dir1/hardlink').st_nlink == 2 + assert os.stat('input/dir1/subdir/hardlink').st_nlink == 2 + assert os.stat('input/dir1/aaaa').st_nlink == 2 + assert os.stat('input/dir1/source2').st_nlink == 2 + @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available') class ArchiverTestCaseBinary(ArchiverTestCase): From b7a6ac94c3caf90dad6a4d295b2750d600ab6b1f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 17 May 2017 11:52:48 +0200 Subject: [PATCH 0863/1387] docs: ran build_man, build_usage --- docs/man/borg-benchmark-crud.1 | 101 ++++++++++++++++++++++ docs/man/borg-benchmark.1 | 47 ++++++++++ docs/man/borg-break-lock.1 | 4 +- docs/man/borg-change-passphrase.1 | 4 +- docs/man/borg-check.1 | 7 +- docs/man/borg-common.1 | 8 +- docs/man/borg-compression.1 | 2 +- docs/man/borg-create.1 | 11 +-- docs/man/borg-delete.1 | 7 +- docs/man/borg-diff.1 | 4 +- docs/man/borg-export-tar.1 | 4 +- docs/man/borg-extract.1 | 10 +-- docs/man/borg-info.1 | 4 +- docs/man/borg-init.1 | 12 +-- docs/man/borg-key-change-passphrase.1 | 4 +- docs/man/borg-key-export.1 | 4 +- docs/man/borg-key-import.1 | 4 +- docs/man/borg-key-migrate-to-repokey.1 | 4 +- docs/man/borg-key.1 | 10 +-- docs/man/borg-list.1 | 9 +- docs/man/borg-mount.1 | 4 +- docs/man/borg-patterns.1 | 2 +- docs/man/borg-placeholders.1 | 2 +- docs/man/borg-prune.1 | 7 +- docs/man/borg-recreate.1 | 7 +- docs/man/borg-rename.1 | 4 +- docs/man/borg-serve.1 | 4 +- docs/man/borg-umount.1 | 4 +- docs/man/borg-upgrade.1 | 7 +- docs/man/borg-with-lock.1 | 4 +- docs/man/borg.1 | 2 +- docs/usage/benchmark_crud.rst.inc | 59 +++++++++++++ docs/usage/break-lock.rst.inc | 2 +- docs/usage/change-passphrase.rst.inc | 2 +- docs/usage/check.rst.inc | 4 +- docs/usage/common-options.rst.inc | 6 +- docs/usage/create.rst.inc | 8 +- docs/usage/delete.rst.inc | 4 +- docs/usage/diff.rst.inc | 2 +- docs/usage/export-tar.rst.inc | 2 +- docs/usage/extract.rst.inc | 9 +- docs/usage/info.rst.inc | 2 +- docs/usage/init.rst.inc | 10 +-- docs/usage/key_change-passphrase.rst.inc | 2 +- docs/usage/key_export.rst.inc | 2 +- docs/usage/key_import.rst.inc | 2 +- docs/usage/key_migrate-to-repokey.rst.inc | 2 +- docs/usage/list.rst.inc | 6 +- docs/usage/mount.rst.inc | 2 +- docs/usage/prune.rst.inc | 4 +- docs/usage/recreate.rst.inc | 4 +- docs/usage/rename.rst.inc | 2 +- docs/usage/serve.rst.inc | 2 +- docs/usage/umount.rst.inc | 2 +- docs/usage/upgrade.rst.inc | 4 +- docs/usage/with-lock.rst.inc | 2 +- 56 files changed, 327 insertions(+), 126 deletions(-) create mode 100644 docs/man/borg-benchmark-crud.1 create mode 100644 docs/man/borg-benchmark.1 create mode 100644 docs/usage/benchmark_crud.rst.inc diff --git a/docs/man/borg-benchmark-crud.1 b/docs/man/borg-benchmark-crud.1 new file mode 100644 index 00000000..ed1a5e1e --- /dev/null +++ b/docs/man/borg-benchmark-crud.1 @@ -0,0 +1,101 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-BENCHMARK-CRUD 1 "2017-05-17" "" "borg backup tool" +.SH NAME +borg-benchmark-crud \- Benchmark Create, Read, Update, Delete for archives. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +borg [common options] benchmark crud REPO PATH +.SH DESCRIPTION +.sp +This command benchmarks borg CRUD (create, read, update, delete) operations. +.sp +It creates input data below the given PATH and backups this data into the given REPO. +The REPO must already exist (it could be a fresh empty repo or an existing repo, the +command will create / read / update / delete some archives named borg\-test\-data* there. +.sp +Make sure you have free space there, you\(aqll need about 1GB each (+ overhead). +.sp +If your repository is encrypted and borg needs a passphrase to unlock the key, use: +.sp +BORG_PASSPHRASE=mysecret borg benchmark crud REPO PATH +.sp +Measurements are done with different input file sizes and counts. +The file contents are very artificial (either all zero or all random), +thus the measurement results do not necessarily reflect performance with real data. +Also, due to the kind of content used, no compression is used in these benchmarks. +.INDENT 0.0 +.TP +.B C\- == borg create (1st archive creation, no compression, do not use files cache) +C\-Z\- == all\-zero files. full dedup, this is primarily measuring reader/chunker/hasher. +C\-R\- == random files. no dedup, measuring throughput through all processing stages. +.TP +.B R\- == borg extract (extract archive, dry\-run, do everything, but do not write files to disk) +R\-Z\- == all zero files. Measuring heavily duplicated files. +R\-R\- == random files. No duplication here, measuring throughput through all processing +.IP "System Message: ERROR/3 (docs/virtmanpage.rst:, line 56)" +Unexpected indentation. +.INDENT 7.0 +.INDENT 3.5 +stages, except writing to disk. +.UNINDENT +.UNINDENT +.TP +.B U\- == borg create (2nd archive creation of unchanged input files, measure files cache speed) +The throughput value is kind of virtual here, it does not actually read the file. +U\-Z\- == needs to check the 2 all\-zero chunks\(aq existence in the repo. +U\-R\- == needs to check existence of a lot of different chunks in the repo. +.TP +.B D\- == borg delete archive (delete last remaining archive, measure deletion + compaction) +D\-Z\- == few chunks to delete / few segments to compact/remove. +D\-R\- == many chunks to delete / many segments to compact/remove. +.UNINDENT +.sp +Please note that there might be quite some variance in these measurements. +Try multiple measurements and having a otherwise idle machine (and network, if you use it). +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS arguments +.INDENT 0.0 +.TP +.B REPO +repo to use for benchmark (must exist) +.TP +.B PATH +path were to create benchmark input data +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-benchmark.1 b/docs/man/borg-benchmark.1 new file mode 100644 index 00000000..0f46eb8a --- /dev/null +++ b/docs/man/borg-benchmark.1 @@ -0,0 +1,47 @@ +.\" Man page generated from reStructuredText. +. +.TH BORG-BENCHMARK 1 "2017-05-17" "" "borg backup tool" +.SH NAME +borg-benchmark \- benchmark command +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.nf +borg [common options] benchmark crud ... +.fi +.sp +.SH DESCRIPTION +.sp +These commands do various benchmarks. +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP, \fIborg\-benchmark\-crud(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-break-lock.1 b/docs/man/borg-break-lock.1 index c83b2d6d..a7275b0c 100644 --- a/docs/man/borg-break-lock.1 +++ b/docs/man/borg-break-lock.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-BREAK-LOCK 1 "2017-04-29" "" "borg backup tool" +.TH BORG-BREAK-LOCK 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-break-lock \- Break the repository lock (e.g. in case it was left by a dead borg. . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg break\-lock REPOSITORY +borg [common options] break\-lock REPOSITORY .SH DESCRIPTION .sp This command breaks the repository and cache locks. diff --git a/docs/man/borg-change-passphrase.1 b/docs/man/borg-change-passphrase.1 index 1a73f339..a4649a71 100644 --- a/docs/man/borg-change-passphrase.1 +++ b/docs/man/borg-change-passphrase.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CHANGE-PASSPHRASE 1 "2017-04-29" "" "borg backup tool" +.TH BORG-CHANGE-PASSPHRASE 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-change-passphrase \- Change repository key file passphrase . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg change\-passphrase REPOSITORY +borg [common options] change\-passphrase REPOSITORY .SH DESCRIPTION .sp The key files used for repository encryption are optionally passphrase diff --git a/docs/man/borg-check.1 b/docs/man/borg-check.1 index ea4ef558..cb694cac 100644 --- a/docs/man/borg-check.1 +++ b/docs/man/borg-check.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CHECK 1 "2017-04-29" "" "borg backup tool" +.TH BORG-CHECK 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-check \- Check repository consistency . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg check REPOSITORY_OR_ARCHIVE +borg [common options] check REPOSITORY_OR_ARCHIVE .SH DESCRIPTION .sp The check command verifies the consistency of a repository and the corresponding archives. @@ -120,9 +120,6 @@ attempt to repair any inconsistencies found .TP .B \-\-save\-space work slower, but using less space -.TP -.B \-p\fP,\fB \-\-progress -show progress display while checking .UNINDENT .SS filters .INDENT 0.0 diff --git a/docs/man/borg-common.1 b/docs/man/borg-common.1 index 3ca2d1f9..223fd33a 100644 --- a/docs/man/borg-common.1 +++ b/docs/man/borg-common.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-COMMON 1 "2017-04-29" "" "borg backup tool" +.TH BORG-COMMON 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-common \- Common options of Borg commands . @@ -54,6 +54,9 @@ enable debug output, work on log level DEBUG .BI \-\-debug\-topic \ TOPIC enable TOPIC debugging (can be specified multiple times). The logger path is borg.debug. if TOPIC is not fully qualified. .TP +.B \-p\fP,\fB \-\-progress +show progress information +.TP .B \-\-log\-json Output one JSON object per log line instead of formatted text. .TP @@ -80,6 +83,9 @@ set remote network upload rate limit in kiByte/s (default: 0=unlimited) .TP .B \-\-consider\-part\-files treat part files like normal files (e.g. to list/extract them) +.TP +.BI \-\-debug\-profile \ FILE +Write execution profile in Borg format into FILE. For local use a Python\-compatible file can be generated by suffixing FILE with ".pyprof". .UNINDENT .SH SEE ALSO .sp diff --git a/docs/man/borg-compression.1 b/docs/man/borg-compression.1 index da3c8487..9e176f22 100644 --- a/docs/man/borg-compression.1 +++ b/docs/man/borg-compression.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-COMPRESSION 1 "2017-04-29" "" "borg backup tool" +.TH BORG-COMPRESSION 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-compression \- Details regarding compression . diff --git a/docs/man/borg-create.1 b/docs/man/borg-create.1 index 8196befa..0cae7ca0 100644 --- a/docs/man/borg-create.1 +++ b/docs/man/borg-create.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CREATE 1 "2017-04-29" "" "borg backup tool" +.TH BORG-CREATE 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-create \- Create new archive . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg create ARCHIVE PATH +borg [common options] create ARCHIVE PATH .SH DESCRIPTION .sp This command creates a backup archive containing all files found while recursively @@ -63,6 +63,10 @@ creation of a new archive to ensure fast operation. This is because the file cac is used to determine changed files quickly uses absolute filenames. If this is not possible, consider creating a bind mount to a stable location. .sp +The \-\-progress option shows (from left to right) Original, Compressed and Deduplicated +(O, C and D, respectively), then the Number of files (N) processed so far, followed by +the currently processed path. +.sp See the output of the "borg help patterns" command for more help on exclude patterns. See the output of the "borg help placeholders" command for more help on placeholders. .SH OPTIONS @@ -86,9 +90,6 @@ do not create a backup archive .B \-s\fP,\fB \-\-stats print statistics for the created archive .TP -.B \-p\fP,\fB \-\-progress -show progress display while creating the archive, showing Original, Compressed and Deduplicated sizes, followed by the Number of files seen and the path being processed, default: False -.TP .B \-\-list output verbose list of items (files, dirs, ...) .TP diff --git a/docs/man/borg-delete.1 b/docs/man/borg-delete.1 index c911ec82..c7c96aa1 100644 --- a/docs/man/borg-delete.1 +++ b/docs/man/borg-delete.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-DELETE 1 "2017-04-29" "" "borg backup tool" +.TH BORG-DELETE 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-delete \- Delete an existing repository or archives . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg delete TARGET +borg [common options] delete TARGET .SH DESCRIPTION .sp This command deletes an archive from the repository or the complete repository. @@ -50,9 +50,6 @@ archive or repository to delete .SS optional arguments .INDENT 0.0 .TP -.B \-p\fP,\fB \-\-progress -show progress display while deleting a single archive -.TP .B \-s\fP,\fB \-\-stats print statistics for the deleted archive .TP diff --git a/docs/man/borg-diff.1 b/docs/man/borg-diff.1 index c01f70f4..b69a792b 100644 --- a/docs/man/borg-diff.1 +++ b/docs/man/borg-diff.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-DIFF 1 "2017-04-29" "" "borg backup tool" +.TH BORG-DIFF 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-diff \- Diff contents of two archives . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg diff REPO_ARCHIVE1 ARCHIVE2 PATH +borg [common options] diff REPO_ARCHIVE1 ARCHIVE2 PATH .SH DESCRIPTION .sp This command finds differences (file contents, user/group/mode) between archives. diff --git a/docs/man/borg-export-tar.1 b/docs/man/borg-export-tar.1 index ecbefc14..73cd9815 100644 --- a/docs/man/borg-export-tar.1 +++ b/docs/man/borg-export-tar.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-EXPORT-TAR 1 "2017-05-16" "" "borg backup tool" +.TH BORG-EXPORT-TAR 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-export-tar \- Export archive contents as a tarball . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg export\-tar ARCHIVE FILE PATH +borg [common options] export\-tar ARCHIVE FILE PATH .SH DESCRIPTION .sp This command creates a tarball from an archive. diff --git a/docs/man/borg-extract.1 b/docs/man/borg-extract.1 index 9e8d113d..dbe9c353 100644 --- a/docs/man/borg-extract.1 +++ b/docs/man/borg-extract.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-EXTRACT 1 "2017-04-29" "" "borg backup tool" +.TH BORG-EXTRACT 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-extract \- Extract archive contents . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg extract ARCHIVE PATH +borg [common options] extract ARCHIVE PATH .SH DESCRIPTION .sp This command extracts the contents of an archive. By default the entire @@ -45,6 +45,9 @@ See the output of the "borg help patterns" command for more help on exclude patt By using \fB\-\-dry\-run\fP, you can do all extraction steps except actually writing the output data: reading metadata and data chunks from the repo, checking the hash/hmac, decrypting, decompressing. +.sp +\fB\-\-progress\fP can be slower than no progress display, since it makes one additional +pass over the archive metadata. .SH OPTIONS .sp See \fIborg\-common(1)\fP for common options of Borg commands. @@ -60,9 +63,6 @@ paths to extract; patterns are supported .SS optional arguments .INDENT 0.0 .TP -.B \-p\fP,\fB \-\-progress -show progress while extracting (may be slower) -.TP .B \-\-list output verbose list of items (files, dirs, ...) .TP diff --git a/docs/man/borg-info.1 b/docs/man/borg-info.1 index cc165d6d..f235a866 100644 --- a/docs/man/borg-info.1 +++ b/docs/man/borg-info.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-INFO 1 "2017-04-29" "" "borg backup tool" +.TH BORG-INFO 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-info \- Show archive details such as disk space used . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg info REPOSITORY_OR_ARCHIVE +borg [common options] info REPOSITORY_OR_ARCHIVE .SH DESCRIPTION .sp This command displays detailed information about the specified archive or repository. diff --git a/docs/man/borg-init.1 b/docs/man/borg-init.1 index c5eb14dc..d0e1f5ed 100644 --- a/docs/man/borg-init.1 +++ b/docs/man/borg-init.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-INIT 1 "2017-04-29" "" "borg backup tool" +.TH BORG-INIT 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-init \- Initialize an empty repository . @@ -32,13 +32,13 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg init REPOSITORY +borg [common options] init REPOSITORY .SH DESCRIPTION .sp This command initializes an empty repository. A repository is a filesystem directory containing the deduplicated data from zero or more archives. .sp -Encryption can be enabled at repository init time. +Encryption can be enabled at repository init time. It cannot be changed later. .sp It is not recommended to work without encryption. Repository encryption protects you e.g. against the case that an attacker has access to your backup repository. @@ -94,8 +94,8 @@ These modes are new and \fInot\fP compatible with borg 1.0.x. .sp \fIauthenticated\fP mode uses no encryption, but authenticates repository contents through the same keyed BLAKE2b\-256 hash as the other blake2 modes (it uses it -as chunk ID hash). The key is stored like repokey. -This mode is new and not compatible with borg 1.0.x. +as the chunk ID hash). The key is stored like repokey. +This mode is new and \fInot\fP compatible with borg 1.0.x. .sp \fInone\fP mode uses no encryption and no authentication. It uses sha256 as chunk ID hash. Not recommended, rather consider using an authenticated or @@ -123,7 +123,7 @@ repository to create .INDENT 0.0 .TP .B \-e\fP,\fB \-\-encryption -select encryption key mode +select encryption key mode \fB(required)\fP .TP .B \-a\fP,\fB \-\-append\-only create an append\-only mode repository diff --git a/docs/man/borg-key-change-passphrase.1 b/docs/man/borg-key-change-passphrase.1 index 6903cf4a..6457d3a7 100644 --- a/docs/man/borg-key-change-passphrase.1 +++ b/docs/man/borg-key-change-passphrase.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-CHANGE-PASSPHRASE 1 "2017-04-29" "" "borg backup tool" +.TH BORG-KEY-CHANGE-PASSPHRASE 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-key-change-passphrase \- Change repository key file passphrase . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg key change\-passphrase REPOSITORY +borg [common options] key change\-passphrase REPOSITORY .SH DESCRIPTION .sp The key files used for repository encryption are optionally passphrase diff --git a/docs/man/borg-key-export.1 b/docs/man/borg-key-export.1 index 6210505f..9736bba3 100644 --- a/docs/man/borg-key-export.1 +++ b/docs/man/borg-key-export.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-EXPORT 1 "2017-04-29" "" "borg backup tool" +.TH BORG-KEY-EXPORT 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-key-export \- Export the repository key for backup . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg key export REPOSITORY PATH +borg [common options] key export REPOSITORY PATH .SH DESCRIPTION .sp If repository encryption is used, the repository is inaccessible diff --git a/docs/man/borg-key-import.1 b/docs/man/borg-key-import.1 index a6c0abef..2ab5af4c 100644 --- a/docs/man/borg-key-import.1 +++ b/docs/man/borg-key-import.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-IMPORT 1 "2017-04-29" "" "borg backup tool" +.TH BORG-KEY-IMPORT 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-key-import \- Import the repository key from backup . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg key import REPOSITORY PATH +borg [common options] key import REPOSITORY PATH .SH DESCRIPTION .sp This command allows to restore a key previously backed up with the diff --git a/docs/man/borg-key-migrate-to-repokey.1 b/docs/man/borg-key-migrate-to-repokey.1 index ea3eed03..17842979 100644 --- a/docs/man/borg-key-migrate-to-repokey.1 +++ b/docs/man/borg-key-migrate-to-repokey.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-MIGRATE-TO-REPOKEY 1 "2017-04-29" "" "borg backup tool" +.TH BORG-KEY-MIGRATE-TO-REPOKEY 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-key-migrate-to-repokey \- Migrate passphrase -> repokey . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg key migrate\-to\-repokey REPOSITORY +borg [common options] key migrate\-to\-repokey REPOSITORY .SH DESCRIPTION .sp This command migrates a repository from passphrase mode (removed in Borg 1.0) diff --git a/docs/man/borg-key.1 b/docs/man/borg-key.1 index b05452e2..e61f8e30 100644 --- a/docs/man/borg-key.1 +++ b/docs/man/borg-key.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY 1 "2017-04-29" "" "borg backup tool" +.TH BORG-KEY 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-key \- Manage a keyfile or repokey of a repository . @@ -32,10 +32,10 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .nf -borg key export ... -borg key import ... -borg key change\-passphrase ... -borg key migrate\-to\-repokey ... +borg [common options] key export ... +borg [common options] key import ... +borg [common options] key change\-passphrase ... +borg [common options] key migrate\-to\-repokey ... .fi .sp .SH SEE ALSO diff --git a/docs/man/borg-list.1 b/docs/man/borg-list.1 index de919db3..66bcf1c1 100644 --- a/docs/man/borg-list.1 +++ b/docs/man/borg-list.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-LIST 1 "2017-04-29" "" "borg backup tool" +.TH BORG-LIST 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-list \- List archive or repository contents . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg list REPOSITORY_OR_ARCHIVE PATH +borg [common options] list REPOSITORY_OR_ARCHIVE PATH .SH DESCRIPTION .sp This command lists the contents of a repository or an archive. @@ -61,7 +61,10 @@ specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") .TP .B \-\-json -format output as JSON. The form of \-\-format is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. +Only valid for listing repository contents. Format output as JSON. The form of \-\-format is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "barchive" key is therefore not available. +.TP +.B \-\-json\-lines +Only valid for listing archive contents. Format output as JSON Lines. The form of \-\-format is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. .UNINDENT .SS filters .INDENT 0.0 diff --git a/docs/man/borg-mount.1 b/docs/man/borg-mount.1 index 89a3ee24..298967af 100644 --- a/docs/man/borg-mount.1 +++ b/docs/man/borg-mount.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-MOUNT 1 "2017-04-29" "" "borg backup tool" +.TH BORG-MOUNT 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-mount \- Mount archive or an entire repository as a FUSE filesystem . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg mount REPOSITORY_OR_ARCHIVE MOUNTPOINT +borg [common options] mount REPOSITORY_OR_ARCHIVE MOUNTPOINT .SH DESCRIPTION .sp This command mounts an archive as a FUSE filesystem. This can be useful for diff --git a/docs/man/borg-patterns.1 b/docs/man/borg-patterns.1 index e2d12055..0c329d0d 100644 --- a/docs/man/borg-patterns.1 +++ b/docs/man/borg-patterns.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PATTERNS 1 "2017-04-29" "" "borg backup tool" +.TH BORG-PATTERNS 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-patterns \- Details regarding patterns . diff --git a/docs/man/borg-placeholders.1 b/docs/man/borg-placeholders.1 index c906f8dc..72ae1046 100644 --- a/docs/man/borg-placeholders.1 +++ b/docs/man/borg-placeholders.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PLACEHOLDERS 1 "2017-04-29" "" "borg backup tool" +.TH BORG-PLACEHOLDERS 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-placeholders \- Details regarding placeholders . diff --git a/docs/man/borg-prune.1 b/docs/man/borg-prune.1 index 55031228..dcb817a6 100644 --- a/docs/man/borg-prune.1 +++ b/docs/man/borg-prune.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PRUNE 1 "2017-04-29" "" "borg backup tool" +.TH BORG-PRUNE 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-prune \- Prune repository archives according to specified rules . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg prune REPOSITORY +borg [common options] prune REPOSITORY .SH DESCRIPTION .sp The prune command prunes a repository by deleting all archives not matching @@ -91,9 +91,6 @@ do not change repository .B \-\-force force pruning of corrupted archives .TP -.B \-p\fP,\fB \-\-progress -show progress display while deleting archives -.TP .B \-s\fP,\fB \-\-stats print statistics for the deleted archive .TP diff --git a/docs/man/borg-recreate.1 b/docs/man/borg-recreate.1 index 8f2bf554..0e22a320 100644 --- a/docs/man/borg-recreate.1 +++ b/docs/man/borg-recreate.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-RECREATE 1 "2017-04-29" "" "borg backup tool" +.TH BORG-RECREATE 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-recreate \- Re-create archives . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg recreate REPOSITORY_OR_ARCHIVE PATH +borg [common options] recreate REPOSITORY_OR_ARCHIVE PATH .SH DESCRIPTION .sp Recreate the contents of existing archives. @@ -92,9 +92,6 @@ output verbose list of items (files, dirs, ...) .BI \-\-filter \ STATUSCHARS only display items with the given status characters .TP -.B \-p\fP,\fB \-\-progress -show progress display while recreating archives -.TP .B \-n\fP,\fB \-\-dry\-run do not change anything .TP diff --git a/docs/man/borg-rename.1 b/docs/man/borg-rename.1 index 6a85b8d1..9c075314 100644 --- a/docs/man/borg-rename.1 +++ b/docs/man/borg-rename.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-RENAME 1 "2017-04-29" "" "borg backup tool" +.TH BORG-RENAME 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-rename \- Rename an existing archive . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg rename ARCHIVE NEWNAME +borg [common options] rename ARCHIVE NEWNAME .SH DESCRIPTION .sp This command renames an archive in the repository. diff --git a/docs/man/borg-serve.1 b/docs/man/borg-serve.1 index 90957e8b..798d6486 100644 --- a/docs/man/borg-serve.1 +++ b/docs/man/borg-serve.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-SERVE 1 "2017-04-29" "" "borg backup tool" +.TH BORG-SERVE 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-serve \- Start in server mode. This command is usually not used manually. . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg serve +borg [common options] serve .SH DESCRIPTION .sp This command starts a repository server process. This command is usually not used manually. diff --git a/docs/man/borg-umount.1 b/docs/man/borg-umount.1 index 98be9f53..26a3c1f6 100644 --- a/docs/man/borg-umount.1 +++ b/docs/man/borg-umount.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-UMOUNT 1 "2017-04-29" "" "borg backup tool" +.TH BORG-UMOUNT 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-umount \- un-mount the FUSE filesystem . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg umount MOUNTPOINT +borg [common options] umount MOUNTPOINT .SH DESCRIPTION .sp This command un\-mounts a FUSE filesystem that was mounted with \fBborg mount\fP\&. diff --git a/docs/man/borg-upgrade.1 b/docs/man/borg-upgrade.1 index b91f693d..ed9e419d 100644 --- a/docs/man/borg-upgrade.1 +++ b/docs/man/borg-upgrade.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-UPGRADE 1 "2017-04-29" "" "borg backup tool" +.TH BORG-UPGRADE 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-upgrade \- upgrade a repository from a previous version . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg upgrade REPOSITORY +borg [common options] upgrade REPOSITORY .SH DESCRIPTION .sp Upgrade an existing, local Borg repository. @@ -129,9 +129,6 @@ path to the repository to be upgraded .SS optional arguments .INDENT 0.0 .TP -.B \-p\fP,\fB \-\-progress -show progress display while upgrading the repository -.TP .B \-n\fP,\fB \-\-dry\-run do not change repository .TP diff --git a/docs/man/borg-with-lock.1 b/docs/man/borg-with-lock.1 index 6fe30706..4099a911 100644 --- a/docs/man/borg-with-lock.1 +++ b/docs/man/borg-with-lock.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-WITH-LOCK 1 "2017-04-29" "" "borg backup tool" +.TH BORG-WITH-LOCK 1 "2017-05-17" "" "borg backup tool" .SH NAME borg-with-lock \- run a user specified command with the repository lock held . @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg with\-lock REPOSITORY COMMAND ARGS +borg [common options] with\-lock REPOSITORY COMMAND ARGS .SH DESCRIPTION .sp This command runs a user\-specified command while the repository lock is held. diff --git a/docs/man/borg.1 b/docs/man/borg.1 index 8e924745..7e663cb3 100644 --- a/docs/man/borg.1 +++ b/docs/man/borg.1 @@ -32,7 +32,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH SYNOPSIS .sp -borg [options] [arguments] +borg [common options] [options] [arguments] .SH DESCRIPTION .\" we don't include the README.rst here since we want to keep this terse. . diff --git a/docs/usage/benchmark_crud.rst.inc b/docs/usage/benchmark_crud.rst.inc new file mode 100644 index 00000000..d47e8d62 --- /dev/null +++ b/docs/usage/benchmark_crud.rst.inc @@ -0,0 +1,59 @@ +.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! + +.. _borg_benchmark_crud: + +borg benchmark crud +------------------- +:: + + borg [common options] benchmark crud REPO PATH + +positional arguments + REPO + repo to use for benchmark (must exist) + PATH + path were to create benchmark input data + +`Common options`_ + | + +Description +~~~~~~~~~~~ + +This command benchmarks borg CRUD (create, read, update, delete) operations. + +It creates input data below the given PATH and backups this data into the given REPO. +The REPO must already exist (it could be a fresh empty repo or an existing repo, the +command will create / read / update / delete some archives named borg-test-data* there. + +Make sure you have free space there, you'll need about 1GB each (+ overhead). + +If your repository is encrypted and borg needs a passphrase to unlock the key, use: + +BORG_PASSPHRASE=mysecret borg benchmark crud REPO PATH + +Measurements are done with different input file sizes and counts. +The file contents are very artificial (either all zero or all random), +thus the measurement results do not necessarily reflect performance with real data. +Also, due to the kind of content used, no compression is used in these benchmarks. + +C- == borg create (1st archive creation, no compression, do not use files cache) + C-Z- == all-zero files. full dedup, this is primarily measuring reader/chunker/hasher. + C-R- == random files. no dedup, measuring throughput through all processing stages. + +R- == borg extract (extract archive, dry-run, do everything, but do not write files to disk) + R-Z- == all zero files. Measuring heavily duplicated files. + R-R- == random files. No duplication here, measuring throughput through all processing + stages, except writing to disk. + +U- == borg create (2nd archive creation of unchanged input files, measure files cache speed) + The throughput value is kind of virtual here, it does not actually read the file. + U-Z- == needs to check the 2 all-zero chunks' existence in the repo. + U-R- == needs to check existence of a lot of different chunks in the repo. + +D- == borg delete archive (delete last remaining archive, measure deletion + compaction) + D-Z- == few chunks to delete / few segments to compact/remove. + D-R- == many chunks to delete / many segments to compact/remove. + +Please note that there might be quite some variance in these measurements. +Try multiple measurements and having a otherwise idle machine (and network, if you use it). \ No newline at end of file diff --git a/docs/usage/break-lock.rst.inc b/docs/usage/break-lock.rst.inc index 756b0d39..1b8e5915 100644 --- a/docs/usage/break-lock.rst.inc +++ b/docs/usage/break-lock.rst.inc @@ -6,7 +6,7 @@ borg break-lock --------------- :: - borg break-lock REPOSITORY + borg [common options] break-lock REPOSITORY positional arguments REPOSITORY diff --git a/docs/usage/change-passphrase.rst.inc b/docs/usage/change-passphrase.rst.inc index 92a82c0d..b0a6c2bb 100644 --- a/docs/usage/change-passphrase.rst.inc +++ b/docs/usage/change-passphrase.rst.inc @@ -6,7 +6,7 @@ borg change-passphrase ---------------------- :: - borg change-passphrase REPOSITORY + borg [common options] change-passphrase REPOSITORY positional arguments REPOSITORY diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index 9958603e..56bc42c8 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -6,7 +6,7 @@ borg check ---------- :: - borg check REPOSITORY_OR_ARCHIVE + borg [common options] check REPOSITORY_OR_ARCHIVE positional arguments REPOSITORY_OR_ARCHIVE @@ -23,8 +23,6 @@ optional arguments | attempt to repair any inconsistencies found ``--save-space`` | work slower, but using less space - ``-p``, ``--progress`` - | show progress display while checking `Common options`_ | diff --git a/docs/usage/common-options.rst.inc b/docs/usage/common-options.rst.inc index 5dadfd52..7299aa72 100644 --- a/docs/usage/common-options.rst.inc +++ b/docs/usage/common-options.rst.inc @@ -12,6 +12,8 @@ | enable debug output, work on log level DEBUG ``--debug-topic TOPIC`` | enable TOPIC debugging (can be specified multiple times). The logger path is borg.debug. if TOPIC is not fully qualified. + ``-p``, ``--progress`` + | show progress information ``--log-json`` | Output one JSON object per log line instead of formatted text. ``--lock-wait N`` @@ -29,4 +31,6 @@ ``--remote-ratelimit rate`` | set remote network upload rate limit in kiByte/s (default: 0=unlimited) ``--consider-part-files`` - | treat part files like normal files (e.g. to list/extract them) \ No newline at end of file + | treat part files like normal files (e.g. to list/extract them) + ``--debug-profile FILE`` + | Write execution profile in Borg format into FILE. For local use a Python-compatible file can be generated by suffixing FILE with ".pyprof". \ No newline at end of file diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index d3aa2be1..6b7006cb 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -6,7 +6,7 @@ borg create ----------- :: - borg create ARCHIVE PATH + borg [common options] create ARCHIVE PATH positional arguments ARCHIVE @@ -19,8 +19,6 @@ optional arguments | do not create a backup archive ``-s``, ``--stats`` | print statistics for the created archive - ``-p``, ``--progress`` - | show progress display while creating the archive, showing Original, Compressed and Deduplicated sizes, followed by the Number of files seen and the path being processed, default: False ``--list`` | output verbose list of items (files, dirs, ...) ``--filter STATUSCHARS`` @@ -104,6 +102,10 @@ creation of a new archive to ensure fast operation. This is because the file cac is used to determine changed files quickly uses absolute filenames. If this is not possible, consider creating a bind mount to a stable location. +The --progress option shows (from left to right) Original, Compressed and Deduplicated +(O, C and D, respectively), then the Number of files (N) processed so far, followed by +the currently processed path. + See the output of the "borg help patterns" command for more help on exclude patterns. See the output of the "borg help placeholders" command for more help on placeholders. diff --git a/docs/usage/delete.rst.inc b/docs/usage/delete.rst.inc index 5c1361a2..0977f022 100644 --- a/docs/usage/delete.rst.inc +++ b/docs/usage/delete.rst.inc @@ -6,15 +6,13 @@ borg delete ----------- :: - borg delete TARGET + borg [common options] delete TARGET positional arguments TARGET archive or repository to delete optional arguments - ``-p``, ``--progress`` - | show progress display while deleting a single archive ``-s``, ``--stats`` | print statistics for the deleted archive ``-c``, ``--cache-only`` diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index 65d5afe6..0163c5dc 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -6,7 +6,7 @@ borg diff --------- :: - borg diff REPO_ARCHIVE1 ARCHIVE2 PATH + borg [common options] diff REPO_ARCHIVE1 ARCHIVE2 PATH positional arguments REPO_ARCHIVE1 diff --git a/docs/usage/export-tar.rst.inc b/docs/usage/export-tar.rst.inc index af5fb545..f2c4e03a 100644 --- a/docs/usage/export-tar.rst.inc +++ b/docs/usage/export-tar.rst.inc @@ -6,7 +6,7 @@ borg export-tar --------------- :: - borg export-tar ARCHIVE FILE PATH + borg [common options] export-tar ARCHIVE FILE PATH positional arguments ARCHIVE diff --git a/docs/usage/extract.rst.inc b/docs/usage/extract.rst.inc index 704c7c64..f5b2d494 100644 --- a/docs/usage/extract.rst.inc +++ b/docs/usage/extract.rst.inc @@ -6,7 +6,7 @@ borg extract ------------ :: - borg extract ARCHIVE PATH + borg [common options] extract ARCHIVE PATH positional arguments ARCHIVE @@ -15,8 +15,6 @@ positional arguments paths to extract; patterns are supported optional arguments - ``-p``, ``--progress`` - | show progress while extracting (may be slower) ``--list`` | output verbose list of items (files, dirs, ...) ``-n``, ``--dry-run`` @@ -53,4 +51,7 @@ See the output of the "borg help patterns" command for more help on exclude patt By using ``--dry-run``, you can do all extraction steps except actually writing the output data: reading metadata and data chunks from the repo, checking the hash/hmac, -decrypting, decompressing. \ No newline at end of file +decrypting, decompressing. + +``--progress`` can be slower than no progress display, since it makes one additional +pass over the archive metadata. \ No newline at end of file diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index 4926b640..0376329a 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -6,7 +6,7 @@ borg info --------- :: - borg info REPOSITORY_OR_ARCHIVE + borg [common options] info REPOSITORY_OR_ARCHIVE positional arguments REPOSITORY_OR_ARCHIVE diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index 9e858d22..a5a6dbfd 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -6,7 +6,7 @@ borg init --------- :: - borg init REPOSITORY + borg [common options] init REPOSITORY positional arguments REPOSITORY @@ -14,7 +14,7 @@ positional arguments optional arguments ``-e``, ``--encryption`` - | select encryption key mode + | select encryption key mode **(required)** ``-a``, ``--append-only`` | create an append-only mode repository @@ -27,7 +27,7 @@ Description This command initializes an empty repository. A repository is a filesystem directory containing the deduplicated data from zero or more archives. -Encryption can be enabled at repository init time. +Encryption can be enabled at repository init time. It cannot be changed later. It is not recommended to work without encryption. Repository encryption protects you e.g. against the case that an attacker has access to your backup repository. @@ -82,8 +82,8 @@ These modes are new and *not* compatible with borg 1.0.x. `authenticated` mode uses no encryption, but authenticates repository contents through the same keyed BLAKE2b-256 hash as the other blake2 modes (it uses it -as chunk ID hash). The key is stored like repokey. -This mode is new and not compatible with borg 1.0.x. +as the chunk ID hash). The key is stored like repokey. +This mode is new and *not* compatible with borg 1.0.x. `none` mode uses no encryption and no authentication. It uses sha256 as chunk ID hash. Not recommended, rather consider using an authenticated or diff --git a/docs/usage/key_change-passphrase.rst.inc b/docs/usage/key_change-passphrase.rst.inc index c34fd002..7666afc2 100644 --- a/docs/usage/key_change-passphrase.rst.inc +++ b/docs/usage/key_change-passphrase.rst.inc @@ -6,7 +6,7 @@ borg key change-passphrase -------------------------- :: - borg key change-passphrase REPOSITORY + borg [common options] key change-passphrase REPOSITORY positional arguments REPOSITORY diff --git a/docs/usage/key_export.rst.inc b/docs/usage/key_export.rst.inc index 3013b8d2..e976ae2d 100644 --- a/docs/usage/key_export.rst.inc +++ b/docs/usage/key_export.rst.inc @@ -6,7 +6,7 @@ borg key export --------------- :: - borg key export REPOSITORY PATH + borg [common options] key export REPOSITORY PATH positional arguments REPOSITORY diff --git a/docs/usage/key_import.rst.inc b/docs/usage/key_import.rst.inc index c92bc9cd..ceb89e3f 100644 --- a/docs/usage/key_import.rst.inc +++ b/docs/usage/key_import.rst.inc @@ -6,7 +6,7 @@ borg key import --------------- :: - borg key import REPOSITORY PATH + borg [common options] key import REPOSITORY PATH positional arguments REPOSITORY diff --git a/docs/usage/key_migrate-to-repokey.rst.inc b/docs/usage/key_migrate-to-repokey.rst.inc index 66629bdb..df242566 100644 --- a/docs/usage/key_migrate-to-repokey.rst.inc +++ b/docs/usage/key_migrate-to-repokey.rst.inc @@ -6,7 +6,7 @@ borg key migrate-to-repokey --------------------------- :: - borg key migrate-to-repokey REPOSITORY + borg [common options] key migrate-to-repokey REPOSITORY positional arguments REPOSITORY diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index ee14a108..cd8db74c 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -6,7 +6,7 @@ borg list --------- :: - borg list REPOSITORY_OR_ARCHIVE PATH + borg [common options] list REPOSITORY_OR_ARCHIVE PATH positional arguments REPOSITORY_OR_ARCHIVE @@ -21,7 +21,9 @@ optional arguments | specify format for file listing | (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") ``--json`` - | format output as JSON. The form of --format is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. + | Only valid for listing repository contents. Format output as JSON. The form of --format is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "barchive" key is therefore not available. + ``--json-lines`` + | Only valid for listing archive contents. Format output as JSON Lines. The form of --format is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. `Common options`_ | diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index e7e60ce2..026cc680 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -6,7 +6,7 @@ borg mount ---------- :: - borg mount REPOSITORY_OR_ARCHIVE MOUNTPOINT + borg [common options] mount REPOSITORY_OR_ARCHIVE MOUNTPOINT positional arguments REPOSITORY_OR_ARCHIVE diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index c09b9885..40e0c26c 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -6,7 +6,7 @@ borg prune ---------- :: - borg prune REPOSITORY + borg [common options] prune REPOSITORY positional arguments REPOSITORY @@ -17,8 +17,6 @@ optional arguments | do not change repository ``--force`` | force pruning of corrupted archives - ``-p``, ``--progress`` - | show progress display while deleting archives ``-s``, ``--stats`` | print statistics for the deleted archive ``--list`` diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index a84c9faf..fd3ef446 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -6,7 +6,7 @@ borg recreate ------------- :: - borg recreate REPOSITORY_OR_ARCHIVE PATH + borg [common options] recreate REPOSITORY_OR_ARCHIVE PATH positional arguments REPOSITORY_OR_ARCHIVE @@ -19,8 +19,6 @@ optional arguments | output verbose list of items (files, dirs, ...) ``--filter STATUSCHARS`` | only display items with the given status characters - ``-p``, ``--progress`` - | show progress display while recreating archives ``-n``, ``--dry-run`` | do not change anything ``-s``, ``--stats`` diff --git a/docs/usage/rename.rst.inc b/docs/usage/rename.rst.inc index 6e53d245..13baa7e4 100644 --- a/docs/usage/rename.rst.inc +++ b/docs/usage/rename.rst.inc @@ -6,7 +6,7 @@ borg rename ----------- :: - borg rename ARCHIVE NEWNAME + borg [common options] rename ARCHIVE NEWNAME positional arguments ARCHIVE diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index 628ff399..f3f1aa65 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -6,7 +6,7 @@ borg serve ---------- :: - borg serve + borg [common options] serve optional arguments ``--restrict-to-path PATH`` diff --git a/docs/usage/umount.rst.inc b/docs/usage/umount.rst.inc index f99c1d46..ab02038b 100644 --- a/docs/usage/umount.rst.inc +++ b/docs/usage/umount.rst.inc @@ -6,7 +6,7 @@ borg umount ----------- :: - borg umount MOUNTPOINT + borg [common options] umount MOUNTPOINT positional arguments MOUNTPOINT diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index 4048ae5a..bdf76ccd 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -6,15 +6,13 @@ borg upgrade ------------ :: - borg upgrade REPOSITORY + borg [common options] upgrade REPOSITORY positional arguments REPOSITORY path to the repository to be upgraded optional arguments - ``-p``, ``--progress`` - | show progress display while upgrading the repository ``-n``, ``--dry-run`` | do not change repository ``-i``, ``--inplace`` diff --git a/docs/usage/with-lock.rst.inc b/docs/usage/with-lock.rst.inc index 407bda72..47b5abcc 100644 --- a/docs/usage/with-lock.rst.inc +++ b/docs/usage/with-lock.rst.inc @@ -6,7 +6,7 @@ borg with-lock -------------- :: - borg with-lock REPOSITORY COMMAND ARGS + borg [common options] with-lock REPOSITORY COMMAND ARGS positional arguments REPOSITORY From 9778c103ef5328d2e9b8f032f600222185871f07 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 17 May 2017 16:27:52 +0200 Subject: [PATCH 0864/1387] serve: fix incorrect type of exception_short for Errors --- src/borg/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 7a54ea70..c32ba9e6 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -252,7 +252,7 @@ class RepositoryServer: # pragma: no cover ex_short = traceback.format_exception_only(e.__class__, e) ex_full = traceback.format_exception(*sys.exc_info()) if isinstance(e, Error): - ex_short = e.get_message() + ex_short = [e.get_message()] if isinstance(e, (Repository.DoesNotExist, Repository.AlreadyExists, PathNotAllowed)): # These exceptions are reconstructed on the client end in RemoteRepository.call_many(), # and will be handled just like locally raised exceptions. Suppress the remote traceback From be40e2fcfab5518f7ed4b18e1ab0eae2f59f9e54 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 2 Mar 2017 13:54:38 +0100 Subject: [PATCH 0865/1387] faq: I get an IntegrityError or similar - what now? --- docs/faq.rst | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index a56c87c3..ae93039a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -108,6 +108,9 @@ Are there other known limitations? An easy workaround is to create multiple archives with less items each. See also the :ref:`archive_limitation` and :issue:`1452`. + :ref:`borg_info` shows how large (relative to the maximum size) existing + archives are. + Why is my backup bigger than with attic? ---------------------------------------- @@ -186,6 +189,46 @@ Yes, if you want to detect accidental data damage (like bit rot), use the If you want to be able to detect malicious tampering also, use an encrypted repo. It will then be able to check using CRCs and HMACs. +.. _faq-integrityerror: + +I get an IntegrityError or similar - what now? +---------------------------------------------- + +The first step should be to check whether it's a problem with the disk drive, +IntegrityErrors can be a sign of drive failure or other hardware issues. + +Using the smartmontools one can retrieve self-diagnostics of the drive in question +(where the repository is located, use *findmnt*, *mount* or *lsblk* to find the +*/dev/...* path of the drive):: + + # smartctl -a /dev/sdSomething + +Attributes that are a typical cause of data corruption are *Offline_Uncorrectable*, +*Current_Pending_Sector*, *Reported_Uncorrect*. A high *UDMA_CRC_Error_Count* usually +indicates a bad cable. If the *entire drive* is failing, then all data should be copied +off it as soon as possible. + +Some drives log IO errors, which are also logged by the system (refer to the journal/dmesg). +IO errors that impact only the filesystem can go unnoticed, since they are not reported +to applications (e.g. Borg), but can still corrupt data. + +If any of these are suspicious, a self-test is recommended:: + + # smartctl -t long /dev/sdSomething + +Running ``fsck`` if not done already might yield further insights. + +:ref:`borg_check` provides diagnostics and ``--repair`` options for repositories with +issues. We recommend to first run without ``--repair`` to assess the situation and +if the found issues / proposed repairs sound right re-run it with ``--repair`` enabled. + +When errors are intermittent the cause might be bad memory, running memtest86+ or a similar +test is recommended. + +A single error does not indicate bad hardware or a Borg bug -- all hardware has a certain +bit error rate (BER), for hard drives this is typically specified as less than one error +every 12 to 120 TB (one bit error in 10e14 to 10e15 bits) and often called +*unrecoverable read error rate* (URE rate). Security ######## From fc105b49b1805586b3d7880266a5e310078f818b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 17 May 2017 17:17:15 +0200 Subject: [PATCH 0866/1387] fix --progress and logging in general for remote --- src/borg/archiver.py | 20 ++++---- src/borg/helpers.py | 8 +++ src/borg/logger.py | 12 ++++- src/borg/remote.py | 83 +++++++++++++++++++++++++++----- src/borg/testsuite/repository.py | 1 + 5 files changed, 100 insertions(+), 24 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 0ddfe1fe..9946dd47 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -3815,15 +3815,15 @@ class Archiver: """ turn on INFO level logging for args that imply that they will produce output """ # map of option name to name of logger for that option option_logger = { - 'output_list': 'borg.output.list', - 'show_version': 'borg.output.show-version', - 'show_rc': 'borg.output.show-rc', - 'stats': 'borg.output.stats', - 'progress': 'borg.output.progress', - } + 'output_list': 'borg.output.list', + 'show_version': 'borg.output.show-version', + 'show_rc': 'borg.output.show-rc', + 'stats': 'borg.output.stats', + 'progress': 'borg.output.progress', + } for option, logger_name in option_logger.items(): - if args.get(option, False): - logging.getLogger(logger_name).setLevel('INFO') + option_set = args.get(option, False) + logging.getLogger(logger_name).setLevel('INFO' if option_set else 'WARN') def _setup_topic_debugging(self, args): """Turn on DEBUG level logging for specified --debug-topics.""" @@ -3839,8 +3839,10 @@ class Archiver: # This works around http://bugs.python.org/issue9351 func = getattr(args, 'func', None) or getattr(args, 'fallback_func') # do not use loggers before this! - setup_logging(level=args.log_level, is_serve=func == self.do_serve, json=args.log_json) + is_serve = func == self.do_serve + setup_logging(level=args.log_level, is_serve=is_serve, json=args.log_json) self.log_json = args.log_json + args.progress |= is_serve self._setup_implied_logging(vars(args)) self._setup_topic_debugging(args) if args.show_version: diff --git a/src/borg/helpers.py b/src/borg/helpers.py index ec06946e..a93ba710 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1226,6 +1226,14 @@ class ProgressIndicatorBase: if self.logger.level == logging.NOTSET: self.logger.setLevel(logging.WARN) self.logger.propagate = False + + # If --progress is not set then the progress logger level will be WARN + # due to setup_implied_logging (it may be NOTSET with a logging config file, + # but the interactions there are generally unclear), so self.emit becomes + # False, which is correct. + # If --progress is set then the level will be INFO as per setup_implied_logging; + # note that this is always the case for serve processes due to a "args.progress |= is_serve". + # In this case self.emit is True. self.emit = self.logger.getEffectiveLevel() == logging.INFO def __del__(self): diff --git a/src/borg/logger.py b/src/borg/logger.py index 6300776d..69cb86f1 100644 --- a/src/borg/logger.py +++ b/src/borg/logger.py @@ -88,15 +88,21 @@ def setup_logging(stream=None, conf_fname=None, env_var='BORG_LOGGING_CONF', lev # if we did not / not successfully load a logging configuration, fallback to this: logger = logging.getLogger('') handler = logging.StreamHandler(stream) - if is_serve: + if is_serve and not json: fmt = '$LOG %(levelname)s %(name)s Remote: %(message)s' else: fmt = '%(message)s' - formatter = JsonFormatter(fmt) if json and not is_serve else logging.Formatter(fmt) + formatter = JsonFormatter(fmt) if json else logging.Formatter(fmt) handler.setFormatter(formatter) borg_logger = logging.getLogger('borg') borg_logger.formatter = formatter borg_logger.json = json + if configured and logger.handlers: + # The RepositoryServer can call setup_logging a second time to adjust the output + # mode from text-ish is_serve to json is_serve. + # Thus, remove the previously installed handler, if any. + logger.handlers[0].close() + logger.handlers.clear() logger.addHandler(handler) logger.setLevel(level.upper()) configured = True @@ -224,6 +230,8 @@ class JsonFormatter(logging.Formatter): data = { 'type': 'log_message', 'time': record.created, + 'message': '', + 'levelname': 'CRITICAL', } for attr in self.RECORD_ATTRIBUTES: value = getattr(record, attr, None) diff --git a/src/borg/remote.py b/src/borg/remote.py index c32ba9e6..3cd4cb93 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -2,29 +2,30 @@ import errno import fcntl import functools import inspect +import json import logging import os import select import shlex import sys import tempfile -import traceback import textwrap import time +import traceback from subprocess import Popen, PIPE import msgpack from . import __version__ from .helpers import Error, IntegrityError -from .helpers import get_home_dir -from .helpers import sysinfo from .helpers import bin_to_hex -from .helpers import replace_placeholders +from .helpers import get_home_dir from .helpers import hostname_is_unique +from .helpers import replace_placeholders +from .helpers import sysinfo +from .logger import create_logger, setup_logging from .repository import Repository, MAX_OBJECT_SIZE, LIST_SCAN_LIMIT from .version import parse_version, format_version -from .logger import create_logger logger = create_logger(__name__) @@ -312,6 +313,9 @@ class RepositoryServer: # pragma: no cover # clients since 1.1.0b3 use a dict as client_data if isinstance(client_data, dict): self.client_version = client_data[b'client_version'] + level = logging.getLevelName(logging.getLogger('').level) + setup_logging(is_serve=True, json=True, level=level) + logger.debug('Initialized loggin system for new protocol') else: self.client_version = BORG_VERSION # seems to be newer than current version (no known old format) @@ -646,12 +650,6 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. opts.append('--critical') else: raise ValueError('log level missing, fix this code') - try: - borg_logger = logging.getLogger('borg') - if borg_logger.json: - opts.append('--log-json') - except AttributeError: - pass env_vars = [] if not hostname_is_unique(): env_vars.append('BORG_HOSTNAME_IS_UNIQUE=no') @@ -930,7 +928,58 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. def handle_remote_line(line): - if line.startswith('$LOG '): + """ + Handle a remote log line. + + This function is remarkably complex because it handles three different wire formats. + """ + if line.startswith('{'): + # This format is used by Borg since 1.1.0b6 for new-protocol clients. + # It is the same format that is exposed by --log-json. + msg = json.loads(line) + + if msg['type'] not in ('progress_message', 'progress_percent', 'log_message'): + logger.warning('Dropped remote log message with unknown type %r: %s', msg['type'], line) + return + + if msg['type'] == 'log_message': + # Re-emit log messages on the same level as the remote to get correct log suppression and verbosity. + level = getattr(logging, msg['levelname'], logging.CRITICAL) + assert isinstance(level, int) + target_logger = logging.getLogger(msg['name']) + # We manually check whether the log message should be propagated + if level >= target_logger.getEffectiveLevel() and logging.getLogger('borg').json: + sys.stderr.write(line) + else: + target_logger.log(level, '%s', msg['message']) + elif msg['type'].startswith('progress_') and not msg.get('finished'): + # Progress messages are a bit more complex. + # First of all, we check whether progress output is enabled. This is signalled + # through the effective level of the borg.output.progress logger + # (also see ProgressIndicatorBase in borg.helpers). + progress_logger = logging.getLogger('borg.output.progress') + if progress_logger.getEffectiveLevel() == logging.INFO: + # When progress output is enabled, then we check whether the client is in + # --log-json mode, as signalled by the "json" attribute on the "borg" logger. + if logging.getLogger('borg').json: + # In --log-json mode we directly re-emit the progress line as sent by the server. + sys.stderr.write(line) + else: + # In text log mode we write only the message to stderr and terminate with \r + # (carriage return, i.e. move the write cursor back to the beginning of the line) + # so that the next message, progress or not, overwrites it. This mirrors the behaviour + # of local progress displays. + sys.stderr.write(msg['message'] + '\r') + elif line.startswith('$LOG '): + # This format is used by Borg since 1.1.0b1. + # It prefixed log lines with $LOG as a marker, followed by the log level + # and optionally a logger name, then "Remote:" as a separator followed by the original + # message. + # + # It is the oldest format supported by these servers, so it was important to make + # it readable with older (1.0.x) clients. + # + # TODO: Remove this block (so it'll be handled by the "else:" below) with a Borg 1.1 RC. _, level, msg = line.split(' ', 2) level = getattr(logging, level, logging.CRITICAL) # str -> int if msg.startswith('Remote:'): @@ -941,7 +990,15 @@ def handle_remote_line(line): logname, msg = msg.split(' ', 1) logging.getLogger(logname).log(level, msg.rstrip()) else: - sys.stderr.write('Remote: ' + line) + # Plain 1.0.x and older format - re-emit to stderr (mirroring what the 1.0.x + # client did) or as a generic log message. + # We don't know what priority the line had. + if logging.getLogger('borg').json: + logging.getLogger('').warning('Remote: ' + line.strip()) + else: + # In non-JSON mode we circumvent logging to preserve carriage returns (\r) + # which are generated by remote progress displays. + sys.stderr.write('Remote: ' + line) class RepositoryNoCache: diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 2819c64c..192f8d3e 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -714,6 +714,7 @@ class RemoteRepositoryTestCase(RepositoryTestCase): class MockArgs: remote_path = 'borg' umask = 0o077 + debug_topics = [] assert self.repository.borg_cmd(None, testing=True) == [sys.executable, '-m', 'borg.archiver', 'serve'] args = MockArgs() From 18a2902c9c0f4ebeeb458f72ed02ba29d1053169 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 17 May 2017 20:49:52 +0200 Subject: [PATCH 0867/1387] rpc negotiate: enable v3 log protocol only for supported clients avoid seeing JSON log output when a 1.1.0b<5 client talks to a 1.1.0b>6 server. --- src/borg/remote.py | 12 ++++++++---- src/borg/testsuite/repository.py | 3 +++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 3cd4cb93..59d4bf5d 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -313,9 +313,10 @@ class RepositoryServer: # pragma: no cover # clients since 1.1.0b3 use a dict as client_data if isinstance(client_data, dict): self.client_version = client_data[b'client_version'] - level = logging.getLevelName(logging.getLogger('').level) - setup_logging(is_serve=True, json=True, level=level) - logger.debug('Initialized loggin system for new protocol') + if client_data.get(b'client_supports_log_v3', False): + level = logging.getLevelName(logging.getLogger('').level) + setup_logging(is_serve=True, json=True, level=level) + logger.debug('Initialized logging system for new (v3) protocol') else: self.client_version = BORG_VERSION # seems to be newer than current version (no known old format) @@ -559,7 +560,10 @@ class RemoteRepository: try: try: - version = self.call('negotiate', {'client_data': {b'client_version': BORG_VERSION}}) + version = self.call('negotiate', {'client_data': { + b'client_version': BORG_VERSION, + b'client_supports_log_v3': True, + }}) except ConnectionClosed: raise ConnectionClosedWithHint('Is borg working on the server?') from None if version == RPC_PROTOCOL_VERSION: diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 192f8d3e..16f47b91 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -724,6 +724,9 @@ class RemoteRepositoryTestCase(RepositoryTestCase): assert self.repository.borg_cmd(args, testing=False) == ['borg', 'serve', '--umask=077', '--info'] args.remote_path = 'borg-0.28.2' assert self.repository.borg_cmd(args, testing=False) == ['borg-0.28.2', 'serve', '--umask=077', '--info'] + args.debug_topics = ['something_client_side', 'repository_compaction'] + assert self.repository.borg_cmd(args, testing=False) == ['borg-0.28.2', 'serve', '--umask=077', '--info', + '--debug-topic=borg.debug.repository_compaction'] class RemoteLegacyFree(RepositoryTestCaseBase): From 5f4d97ff2ba734a44f46e410a8c60189493afd47 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 18 May 2017 16:54:44 +0200 Subject: [PATCH 0868/1387] remote: restore "Remote:" prefix (as used in 1.0.x) --- src/borg/remote.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 59d4bf5d..3a637349 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -952,28 +952,32 @@ def handle_remote_line(line): assert isinstance(level, int) target_logger = logging.getLogger(msg['name']) # We manually check whether the log message should be propagated + msg['message'] = 'Remote: ' + msg['message'] if level >= target_logger.getEffectiveLevel() and logging.getLogger('borg').json: - sys.stderr.write(line) + sys.stderr.write(json.dumps(msg) + '\n') else: target_logger.log(level, '%s', msg['message']) - elif msg['type'].startswith('progress_') and not msg.get('finished'): + elif msg['type'].startswith('progress_'): # Progress messages are a bit more complex. # First of all, we check whether progress output is enabled. This is signalled # through the effective level of the borg.output.progress logger # (also see ProgressIndicatorBase in borg.helpers). progress_logger = logging.getLogger('borg.output.progress') if progress_logger.getEffectiveLevel() == logging.INFO: - # When progress output is enabled, then we check whether the client is in + # When progress output is enabled, we check whether the client is in # --log-json mode, as signalled by the "json" attribute on the "borg" logger. if logging.getLogger('borg').json: - # In --log-json mode we directly re-emit the progress line as sent by the server. - sys.stderr.write(line) - else: + # In --log-json mode we re-emit the progress JSON line as sent by the server, prefixed + # with "Remote: " when it contains a message. + if 'message' in msg: + msg['message'] = 'Remote: ' + msg['message'] + sys.stderr.write(json.dumps(msg) + '\n') + elif 'message' in msg: # In text log mode we write only the message to stderr and terminate with \r # (carriage return, i.e. move the write cursor back to the beginning of the line) # so that the next message, progress or not, overwrites it. This mirrors the behaviour # of local progress displays. - sys.stderr.write(msg['message'] + '\r') + sys.stderr.write('Remote: ' + msg['message'] + '\r') elif line.startswith('$LOG '): # This format is used by Borg since 1.1.0b1. # It prefixed log lines with $LOG as a marker, followed by the log level From b3b555395cb799f0696c518cb21fb8180e897b52 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 20 May 2017 12:52:32 +0200 Subject: [PATCH 0869/1387] remote: clarify remote log handling comments --- src/borg/remote.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 3a637349..5cd95b1c 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -935,7 +935,7 @@ def handle_remote_line(line): """ Handle a remote log line. - This function is remarkably complex because it handles three different wire formats. + This function is remarkably complex because it handles multiple wire formats. """ if line.startswith('{'): # This format is used by Borg since 1.1.0b6 for new-protocol clients. @@ -951,9 +951,9 @@ def handle_remote_line(line): level = getattr(logging, msg['levelname'], logging.CRITICAL) assert isinstance(level, int) target_logger = logging.getLogger(msg['name']) - # We manually check whether the log message should be propagated msg['message'] = 'Remote: ' + msg['message'] - if level >= target_logger.getEffectiveLevel() and logging.getLogger('borg').json: + # In JSON mode, we manually check whether the log message should be propagated. + if logging.getLogger('borg').json and level >= target_logger.getEffectiveLevel(): sys.stderr.write(json.dumps(msg) + '\n') else: target_logger.log(level, '%s', msg['message']) @@ -967,8 +967,8 @@ def handle_remote_line(line): # When progress output is enabled, we check whether the client is in # --log-json mode, as signalled by the "json" attribute on the "borg" logger. if logging.getLogger('borg').json: - # In --log-json mode we re-emit the progress JSON line as sent by the server, prefixed - # with "Remote: " when it contains a message. + # In --log-json mode we re-emit the progress JSON line as sent by the server, + # with the message, if any, prefixed with "Remote: ". if 'message' in msg: msg['message'] = 'Remote: ' + msg['message'] sys.stderr.write(json.dumps(msg) + '\n') @@ -988,6 +988,7 @@ def handle_remote_line(line): # it readable with older (1.0.x) clients. # # TODO: Remove this block (so it'll be handled by the "else:" below) with a Borg 1.1 RC. + # Also check whether client_supports_log_v3 should be removed. _, level, msg = line.split(' ', 2) level = getattr(logging, level, logging.CRITICAL) # str -> int if msg.startswith('Remote:'): From 97e96035318e90f8e6c69ed4fedb76f99de5cb5d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 17 May 2017 17:43:48 +0200 Subject: [PATCH 0870/1387] implement --debug-topic for remote servers --- src/borg/remote.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/borg/remote.py b/src/borg/remote.py index 5cd95b1c..1a67930e 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -654,6 +654,23 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. opts.append('--critical') else: raise ValueError('log level missing, fix this code') + + # Tell the remote server about debug topics it may need to consider. + # Note that debug topics are usable for "spew" or "trace" logs which would + # be too plentiful to transfer for normal use, so the server doesn't send + # them unless explicitly enabled. + # + # Needless to say, if you do --debug-topic=repository.compaction, for example, + # with a 1.0.x server it won't work, because the server does not recognize the + # option. + # + # This is not considered a problem, since this is a debugging feature that + # should not be used for regular use. + for topic in args.debug_topics: + if '.' not in topic: + topic = 'borg.debug.' + topic + if 'repository' in topic: + opts.append('--debug-topic=%s' % topic) env_vars = [] if not hostname_is_unique(): env_vars.append('BORG_HOSTNAME_IS_UNIQUE=no') From 482b65eaea3d85efa45a9c78643a1c46eb4e5631 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 10 May 2017 15:30:51 +0200 Subject: [PATCH 0871/1387] cache: extract CacheConfig class --- src/borg/cache.py | 246 ++++++++++++++++++++++++++++++---------------- 1 file changed, 160 insertions(+), 86 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 40ed925a..ecca1ab7 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -35,6 +35,7 @@ class SecurityManager: def __init__(self, repository): self.repository = repository self.dir = get_security_dir(repository.id_str) + self.cache_dir = cache_dir(repository) self.key_type_file = os.path.join(self.dir, 'key-type') self.location_file = os.path.join(self.dir, 'location') self.manifest_ts_file = os.path.join(self.dir, 'manifest-timestamp') @@ -52,9 +53,9 @@ class SecurityManager: except OSError as exc: logger.warning('Could not read/parse key type file: %s', exc) - def save(self, manifest, key, cache): + def save(self, manifest, key): logger.debug('security: saving state for %s to %s', self.repository.id_str, self.dir) - current_location = cache.repository._location.canonical_path() + current_location = self.repository._location.canonical_path() logger.debug('security: current location %s', current_location) logger.debug('security: key type %s', str(key.TYPE)) logger.debug('security: manifest timestamp %s', manifest.timestamp) @@ -65,7 +66,7 @@ class SecurityManager: with open(self.manifest_ts_file, 'w') as fd: fd.write(manifest.timestamp) - def assert_location_matches(self, cache): + def assert_location_matches(self, cache_config): # Warn user before sending data to a relocated repository try: with open(self.location_file) as fd: @@ -77,13 +78,15 @@ class SecurityManager: except OSError as exc: logger.warning('Could not read previous location file: %s', exc) previous_location = None - if cache.previous_location and previous_location != cache.previous_location: + if cache_config.previous_location and previous_location != cache_config.previous_location: # Reconcile cache and security dir; we take the cache location. - previous_location = cache.previous_location + previous_location = cache_config.previous_location logger.debug('security: using previous_location of cache: %r', previous_location) - if previous_location and previous_location != self.repository._location.canonical_path(): + + repository_location = self.repository._location.canonical_path() + if previous_location and previous_location != repository_location: msg = ("Warning: The repository at location {} was previously located at {}\n".format( - self.repository._location.canonical_path(), previous_location) + + repository_location, previous_location) + "Do you want to continue? [yN] ") if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", retry=False, env_var_override='BORG_RELOCATED_REPO_ACCESS_IS_OK'): @@ -91,11 +94,10 @@ class SecurityManager: # adapt on-disk config immediately if the new location was accepted logger.debug('security: updating location stored in cache and security dir') with open(self.location_file, 'w') as fd: - fd.write(cache.repository._location.canonical_path()) - cache.begin_txn() - cache.commit() + fd.write(repository_location) + cache_config.save() - def assert_no_manifest_replay(self, manifest, key, cache): + def assert_no_manifest_replay(self, manifest, key, cache_config): try: with open(self.manifest_ts_file) as fd: timestamp = fd.read() @@ -106,7 +108,7 @@ class SecurityManager: except OSError as exc: logger.warning('Could not read previous location file: %s', exc) timestamp = '' - timestamp = max(timestamp, cache.timestamp or '') + timestamp = max(timestamp, cache_config.timestamp or '') logger.debug('security: determined newest manifest timestamp as %s', timestamp) # If repository is older than the cache or security dir something fishy is going on if timestamp and timestamp > manifest.timestamp: @@ -122,22 +124,136 @@ class SecurityManager: if self.known() and not self.key_matches(key): raise Cache.EncryptionMethodMismatch() - def assert_secure(self, manifest, key, cache): - self.assert_location_matches(cache) - self.assert_key_type(key, cache) - self.assert_no_manifest_replay(manifest, key, cache) - if not self.known(): - self.save(manifest, key, cache) + def assert_secure(self, manifest, key, *, cache_config=None, warn_if_unencrypted=True): + self.assert_access_unknown(warn_if_unencrypted, manifest, key) + if cache_config: + self._assert_secure(manifest, key, cache_config) + else: + with CacheConfig(self.repository): + self._assert_secure(manifest, key, cache_config) - def assert_access_unknown(self, warn_if_unencrypted, key): - if warn_if_unencrypted and not key.logically_encrypted and not self.known(): + def _assert_secure(self, manifest, key, cache_config): + self.assert_location_matches(cache_config) + self.assert_key_type(key, cache_config) + self.assert_no_manifest_replay(manifest, key, cache_config) + if not self.known(): + logger.debug('security: saving state for previously unknown repository') + self.save(manifest, key) + + def assert_access_unknown(self, warn_if_unencrypted, manifest, key): + if not key.logically_encrypted and not self.known(): msg = ("Warning: Attempting to access a previously unknown unencrypted repository!\n" + "Do you want to continue? [yN] ") - if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.", - retry=False, env_var_override='BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK'): + allow_access = not warn_if_unencrypted or yes(msg, false_msg="Aborting.", + invalid_msg="Invalid answer, aborting.", + retry=False, env_var_override='BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK') + if allow_access: + if warn_if_unencrypted: + logger.debug('security: saving state for unknown unencrypted repository (explicitly granted)') + else: + logger.debug('security: saving state for unknown unencrypted repository') + self.save(manifest, key) + else: raise Cache.CacheInitAbortedError() +def assert_secure(repository, manifest): + sm = SecurityManager(repository) + sm.assert_secure(manifest, manifest.key) + + +def recanonicalize_relative_location(cache_location, repository): + # borg < 1.0.8rc1 had different canonicalization for the repo location (see #1655 and #1741). + repo_location = repository._location.canonical_path() + rl = Location(repo_location) + cl = Location(cache_location) + if cl.proto == rl.proto and cl.user == rl.user and cl.host == rl.host and cl.port == rl.port \ + and \ + cl.path and rl.path and \ + cl.path.startswith('/~/') and rl.path.startswith('/./') and cl.path[3:] == rl.path[3:]: + # everything is same except the expected change in relative path canonicalization, + # update previous_location to avoid warning / user query about changed location: + return repo_location + else: + return cache_location + + +def cache_dir(repository, path=None): + return path or os.path.join(get_cache_dir(), repository.id_str) + + +class CacheConfig: + def __init__(self, repository, path=None, lock_wait=None): + self.repository = repository + self.path = cache_dir(repository, path) + self.config_path = os.path.join(self.path, 'config') + self.lock = None + self.lock_wait = lock_wait + + def __enter__(self): + self.open() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def create(self): + assert not self.exists() + config = configparser.ConfigParser(interpolation=None) + config.add_section('cache') + config.set('cache', 'version', '1') + config.set('cache', 'repository', self.repository.id_str) + config.set('cache', 'manifest', '') + with SaveFile(self.config_path) as fd: + config.write(fd) + + def open(self): + self.lock = Lock(os.path.join(self.path, 'lock'), exclusive=True, timeout=self.lock_wait, + kill_stale_locks=hostname_is_unique()).acquire() + self.load() + + def load(self): + self._config = configparser.ConfigParser(interpolation=None) + self._config.read(self.config_path) + self._check_upgrade(self.config_path) + self.id = self._config.get('cache', 'repository') + self.manifest_id = unhexlify(self._config.get('cache', 'manifest')) + self.timestamp = self._config.get('cache', 'timestamp', fallback=None) + self.key_type = self._config.get('cache', 'key_type', fallback=None) + previous_location = self._config.get('cache', 'previous_location', fallback=None) + if previous_location: + self.previous_location = recanonicalize_relative_location(previous_location, self.repository) + else: + self.previous_location = None + + def save(self, manifest=None, key=None): + if manifest: + self._config.set('cache', 'manifest', manifest.id_str) + self._config.set('cache', 'timestamp', manifest.timestamp) + if key: + self._config.set('cache', 'key_type', str(key.TYPE)) + self._config.set('cache', 'previous_location', self.repository._location.canonical_path()) + with SaveFile(self.config_path) as fd: + self._config.write(fd) + + def close(self): + if self.lock is not None: + self.lock.release() + self.lock = None + + def _check_upgrade(self, config_path): + try: + cache_version = self._config.getint('cache', 'version') + wanted_version = 1 + if cache_version != wanted_version: + self.close() + raise Exception('%s has unexpected cache version %d (wanted: %d).' % + (config_path, cache_version, wanted_version)) + except configparser.NoSectionError: + self.close() + raise Exception('%s does not look like a Borg cache.' % config_path) from None + + class Cache: """Client Side cache """ @@ -158,7 +274,7 @@ class Cache: @staticmethod def break_lock(repository, path=None): - path = path or os.path.join(get_cache_dir(), repository.id_str) + path = cache_dir(repository, path) Lock(os.path.join(path, 'lock'), exclusive=True).break_lock() @staticmethod @@ -178,25 +294,27 @@ class Cache: :param lock_wait: timeout for lock acquisition (None: return immediately if lock unavailable) :param sync: do :meth:`.sync` """ - self.lock = None - self.timestamp = None - self.lock = None - self.txn_active = False self.repository = repository self.key = key self.manifest = manifest self.progress = progress - self.path = path or os.path.join(get_cache_dir(), repository.id_str) - self.security_manager = SecurityManager(repository) self.do_files = do_files + self.timestamp = None + self.txn_active = False + + self.path = cache_dir(repository, path) + self.security_manager = SecurityManager(repository) + self.cache_config = CacheConfig(self.repository, self.path, lock_wait) + # Warn user before sending data to a never seen before unencrypted repository if not os.path.exists(self.path): - self.security_manager.assert_access_unknown(warn_if_unencrypted, key) + self.security_manager.assert_access_unknown(warn_if_unencrypted, manifest, key) self.create() - self.open(lock_wait=lock_wait) + + self.open() try: - self.security_manager.assert_secure(manifest, key, self) - if sync and self.manifest.id != self.manifest_id: + self.security_manager.assert_secure(manifest, key, cache_config=self.cache_config) + if sync and self.manifest.id != self.cache_config.manifest_id: self.sync() self.commit() except: @@ -240,66 +358,27 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" os.makedirs(self.path) with open(os.path.join(self.path, 'README'), 'w') as fd: fd.write(CACHE_README) - config = configparser.ConfigParser(interpolation=None) - config.add_section('cache') - config.set('cache', 'version', '1') - config.set('cache', 'repository', self.repository.id_str) - config.set('cache', 'manifest', '') - with SaveFile(os.path.join(self.path, 'config')) as fd: - config.write(fd) + self.cache_config.create() ChunkIndex().write(os.path.join(self.path, 'chunks').encode('utf-8')) os.makedirs(os.path.join(self.path, 'chunks.archive.d')) with SaveFile(os.path.join(self.path, 'files'), binary=True) as fd: pass # empty file - def _check_upgrade(self, config_path): - try: - cache_version = self.config.getint('cache', 'version') - wanted_version = 1 - if cache_version != wanted_version: - self.close() - raise Exception('%s has unexpected cache version %d (wanted: %d).' % ( - config_path, cache_version, wanted_version)) - except configparser.NoSectionError: - self.close() - raise Exception('%s does not look like a Borg cache.' % config_path) from None - # borg < 1.0.8rc1 had different canonicalization for the repo location (see #1655 and #1741). - cache_loc = self.config.get('cache', 'previous_location', fallback=None) - if cache_loc: - repo_loc = self.repository._location.canonical_path() - rl = Location(repo_loc) - cl = Location(cache_loc) - if cl.proto == rl.proto and cl.user == rl.user and cl.host == rl.host and cl.port == rl.port \ - and \ - cl.path and rl.path and \ - cl.path.startswith('/~/') and rl.path.startswith('/./') and cl.path[3:] == rl.path[3:]: - # everything is same except the expected change in relative path canonicalization, - # update previous_location to avoid warning / user query about changed location: - self.config.set('cache', 'previous_location', repo_loc) - def _do_open(self): - self.config = configparser.ConfigParser(interpolation=None) - config_path = os.path.join(self.path, 'config') - self.config.read(config_path) - self._check_upgrade(config_path) - self.id = self.config.get('cache', 'repository') - self.manifest_id = unhexlify(self.config.get('cache', 'manifest')) - self.timestamp = self.config.get('cache', 'timestamp', fallback=None) - self.key_type = self.config.get('cache', 'key_type', fallback=None) - self.previous_location = self.config.get('cache', 'previous_location', fallback=None) + self.cache_config.load() self.chunks = ChunkIndex.read(os.path.join(self.path, 'chunks').encode('utf-8')) self.files = None - def open(self, lock_wait=None): + def open(self): if not os.path.isdir(self.path): raise Exception('%s Does not look like a Borg cache' % self.path) - self.lock = Lock(os.path.join(self.path, 'lock'), exclusive=True, timeout=lock_wait, kill_stale_locks=hostname_is_unique()).acquire() + self.cache_config.open() self.rollback() def close(self): - if self.lock is not None: - self.lock.release() - self.lock = None + if self.cache_config is not None: + self.cache_config.close() + self.cache_config = None def _read_files(self): self.files = {} @@ -338,7 +417,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" """ if not self.txn_active: return - self.security_manager.save(self.manifest, self.key, self) + self.security_manager.save(self.manifest, self.key) pi = ProgressIndicatorMessage(msgid='cache.commit') if self.files is not None: if self._newest_mtime is None: @@ -356,12 +435,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" entry.age > 0 and entry.age < ttl: msgpack.pack((path_hash, entry), fd) pi.output('Saving cache config') - self.config.set('cache', 'manifest', self.manifest.id_str) - self.config.set('cache', 'timestamp', self.manifest.timestamp) - self.config.set('cache', 'key_type', str(self.key.TYPE)) - self.config.set('cache', 'previous_location', self.repository._location.canonical_path()) - with SaveFile(os.path.join(self.path, 'config')) as fd: - self.config.write(fd) + self.cache_config.save(self.manifest, self.key) pi.output('Saving chunks cache') self.chunks.write(os.path.join(self.path, 'chunks').encode('utf-8')) os.rename(os.path.join(self.path, 'txn.active'), From c23e1e28c63be1efcd7b21ebf08c56a2aff27f2b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 10 May 2017 18:34:22 +0200 Subject: [PATCH 0872/1387] use assert_secure for all commands that use the manifest This already excludes the debug commands that we wouldn't want this on. --- src/borg/archiver.py | 8 +++-- src/borg/cache.py | 63 ++++++++++++++++++++++++++-------- src/borg/testsuite/archiver.py | 2 +- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 0ddfe1fe..601c9848 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -36,7 +36,7 @@ from . import helpers from .algorithms.crc32 import crc32 from .archive import Archive, ArchiveChecker, ArchiveRecreater, Statistics, is_special from .archive import BackupOSError, backup_io -from .cache import Cache +from .cache import Cache, assert_secure from .constants import * # NOQA from .compress import CompressionSpec from .crypto.key import key_creator, tam_required_file, tam_required, RepoKey, PassphraseKey @@ -86,7 +86,8 @@ def argument(args, str_or_bool): return str_or_bool -def with_repository(fake=False, invert_fake=False, create=False, lock=True, exclusive=False, manifest=True, cache=False): +def with_repository(fake=False, invert_fake=False, create=False, lock=True, exclusive=False, manifest=True, cache=False, + secure=True): """ Method decorator for subcommand-handling methods: do_XYZ(self, args, repository, …) @@ -97,6 +98,7 @@ def with_repository(fake=False, invert_fake=False, create=False, lock=True, excl :param exclusive: (str or bool) lock repository exclusively (for writing) :param manifest: load manifest and key, pass them as keyword arguments :param cache: open cache, pass it as keyword argument (implies manifest) + :param secure: do assert_secure after loading manifest """ def decorator(method): @functools.wraps(method) @@ -117,6 +119,8 @@ def with_repository(fake=False, invert_fake=False, create=False, lock=True, excl kwargs['manifest'], kwargs['key'] = Manifest.load(repository) if 'compression' in args: kwargs['key'].compressor = args.compression.compressor + if secure: + assert_secure(repository, kwargs['manifest']) if cache: with Cache(repository, kwargs['key'], kwargs['manifest'], do_files=getattr(args, 'cache_files', False), diff --git a/src/borg/cache.py b/src/borg/cache.py index ecca1ab7..882d9863 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -32,6 +32,25 @@ FileCacheEntry = namedtuple('FileCacheEntry', 'age inode size mtime chunk_ids') class SecurityManager: + """ + Tracks repositories. Ensures that nothing bad happens (repository swaps, + replay attacks, unknown repositories etc.). + + This is complicated by the Cache being initially used for this, while + only some commands actually use the Cache, which meant that other commands + did not perform these checks. + + Further complications were created by the Cache being a cache, so it + could be legitimately deleted, which is annoying because Borg didn't + recognize repositories after that. + + Therefore a second location, the security database (see get_security_dir), + was introduced which stores this information. However, this means that + the code has to deal with a cache existing but no security DB entry, + or inconsistencies between the security DB and the cache which have to + be reconciled, and also with no cache existing but a security DB entry. + """ + def __init__(self, repository): self.repository = repository self.dir = get_security_dir(repository.id_str) @@ -66,19 +85,19 @@ class SecurityManager: with open(self.manifest_ts_file, 'w') as fd: fd.write(manifest.timestamp) - def assert_location_matches(self, cache_config): + def assert_location_matches(self, cache_config=None): # Warn user before sending data to a relocated repository try: with open(self.location_file) as fd: previous_location = fd.read() - logger.debug('security: read previous_location %r', previous_location) + logger.debug('security: read previous location %r', previous_location) except FileNotFoundError: - logger.debug('security: previous_location file %s not found', self.location_file) + logger.debug('security: previous location file %s not found', self.location_file) previous_location = None except OSError as exc: logger.warning('Could not read previous location file: %s', exc) previous_location = None - if cache_config.previous_location and previous_location != cache_config.previous_location: + if cache_config and cache_config.previous_location and previous_location != cache_config.previous_location: # Reconcile cache and security dir; we take the cache location. previous_location = cache_config.previous_location logger.debug('security: using previous_location of cache: %r', previous_location) @@ -95,9 +114,10 @@ class SecurityManager: logger.debug('security: updating location stored in cache and security dir') with open(self.location_file, 'w') as fd: fd.write(repository_location) - cache_config.save() + if cache_config: + cache_config.save() - def assert_no_manifest_replay(self, manifest, key, cache_config): + def assert_no_manifest_replay(self, manifest, key, cache_config=None): try: with open(self.manifest_ts_file) as fd: timestamp = fd.read() @@ -108,7 +128,8 @@ class SecurityManager: except OSError as exc: logger.warning('Could not read previous location file: %s', exc) timestamp = '' - timestamp = max(timestamp, cache_config.timestamp or '') + if cache_config: + timestamp = max(timestamp, cache_config.timestamp or '') logger.debug('security: determined newest manifest timestamp as %s', timestamp) # If repository is older than the cache or security dir something fishy is going on if timestamp and timestamp > manifest.timestamp: @@ -117,30 +138,39 @@ class SecurityManager: else: raise Cache.RepositoryReplay() - def assert_key_type(self, key, cache): + def assert_key_type(self, key, cache_config=None): # Make sure an encrypted repository has not been swapped for an unencrypted repository - if cache.key_type is not None and cache.key_type != str(key.TYPE): + if cache_config and cache_config.key_type is not None and cache_config.key_type != str(key.TYPE): raise Cache.EncryptionMethodMismatch() if self.known() and not self.key_matches(key): raise Cache.EncryptionMethodMismatch() def assert_secure(self, manifest, key, *, cache_config=None, warn_if_unencrypted=True): + # warn_if_unencrypted=False is only used for initializing a new repository. + # Thus, avoiding asking about a repository that's currently initializing. self.assert_access_unknown(warn_if_unencrypted, manifest, key) if cache_config: self._assert_secure(manifest, key, cache_config) else: - with CacheConfig(self.repository): - self._assert_secure(manifest, key, cache_config) + cache_config = CacheConfig(self.repository) + if cache_config.exists(): + with cache_config: + self._assert_secure(manifest, key, cache_config) + else: + self._assert_secure(manifest, key) + logger.debug('security: repository checks ok, allowing access') - def _assert_secure(self, manifest, key, cache_config): + def _assert_secure(self, manifest, key, cache_config=None): self.assert_location_matches(cache_config) self.assert_key_type(key, cache_config) self.assert_no_manifest_replay(manifest, key, cache_config) if not self.known(): - logger.debug('security: saving state for previously unknown repository') + logger.debug('security: remembering previously unknown repository') self.save(manifest, key) def assert_access_unknown(self, warn_if_unencrypted, manifest, key): + # warn_if_unencrypted=False is only used for initializing a new repository. + # Thus, avoiding asking about a repository that's currently initializing. if not key.logically_encrypted and not self.known(): msg = ("Warning: Attempting to access a previously unknown unencrypted repository!\n" + "Do you want to continue? [yN] ") @@ -149,9 +179,9 @@ class SecurityManager: retry=False, env_var_override='BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK') if allow_access: if warn_if_unencrypted: - logger.debug('security: saving state for unknown unencrypted repository (explicitly granted)') + logger.debug('security: remembering unknown unencrypted repository (explicitly allowed)') else: - logger.debug('security: saving state for unknown unencrypted repository') + logger.debug('security: initializing unencrypted repository') self.save(manifest, key) else: raise Cache.CacheInitAbortedError() @@ -197,6 +227,9 @@ class CacheConfig: def __exit__(self, exc_type, exc_val, exc_tb): self.close() + def exists(self): + return os.path.exists(self.config_path) + def create(self): assert not self.exists() config = configparser.ConfigParser(interpolation=None) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 29460189..8f47b0b9 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1709,7 +1709,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_test_files() self.cmd('init', '--encryption=repokey', self.repository_location) log = self.cmd('--debug', 'create', self.repository_location + '::test', 'input') - assert 'security: read previous_location' in log + assert 'security: read previous location' in log def _get_sizes(self, compression, compressible, size=10000): if compressible: From c5159582d4ba71118d6f4221a55c75359f1bbbf4 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 20 May 2017 15:59:20 +0200 Subject: [PATCH 0873/1387] add docs/misc/borg-data-flow data flow chart --- docs/misc/borg-data-flow.png | Bin 0 -> 100199 bytes docs/misc/borg-data-flow.vsd | Bin 0 -> 111104 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/misc/borg-data-flow.png create mode 100644 docs/misc/borg-data-flow.vsd diff --git a/docs/misc/borg-data-flow.png b/docs/misc/borg-data-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..ed7f52b9baab124b0ba53d8aae6bfed2b08fe3f8 GIT binary patch literal 100199 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

!0>a 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 Date: Sat, 20 May 2017 16:28:56 +0200 Subject: [PATCH 0874/1387] fuse: fix crash if empty (None) xattr is read --- src/borg/fuse.py | 2 +- src/borg/testsuite/archiver.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 20782544..28e09689 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -288,7 +288,7 @@ class FuseOperations(llfuse.Operations): def getxattr(self, inode, name, ctx=None): item = self.get_item(inode) try: - return item.get('xattrs', {})[name] + return item.get('xattrs', {})[name] or b'' except KeyError: raise llfuse.FUSEError(llfuse.ENOATTR) from None diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 8f47b0b9..6d8c23f9 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -326,6 +326,7 @@ class ArchiverTestCaseBase(BaseTestCase): # into "fakeroot space". Because the xattrs exposed by borgfs are these of an underlying file # (from fakeroots point of view) they are invisible to the test process inside the fakeroot. xattr.setxattr(os.path.join(self.input_path, 'fusexattr'), 'user.foo', b'bar') + xattr.setxattr(os.path.join(self.input_path, 'fusexattr'), 'user.empty', b'') # XXX this always fails for me # ubuntu 14.04, on a TMP dir filesystem with user_xattr, using fakeroot # same for newer ubuntu and centos. @@ -1874,8 +1875,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): in_fn = 'input/fusexattr' out_fn = os.path.join(mountpoint, 'input', 'fusexattr') if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path): - assert no_selinux(xattr.listxattr(out_fn)) == ['user.foo', ] + assert sorted(no_selinux(xattr.listxattr(out_fn))) == ['user.empty', 'user.foo', ] assert xattr.getxattr(out_fn, 'user.foo') == b'bar' + # Special case: getxattr returns None (not b'') when reading an empty xattr. + assert xattr.getxattr(out_fn, 'user.empty') is None else: assert xattr.listxattr(out_fn) == [] try: From 58edbe15f4970d40eb8c377b7c06c23226e2b557 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 20 May 2017 16:40:36 +0200 Subject: [PATCH 0875/1387] xattr: document API --- src/borg/xattr.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/borg/xattr.py b/src/borg/xattr.py index c37d1a81..03c5eae2 100644 --- a/src/borg/xattr.py +++ b/src/borg/xattr.py @@ -35,6 +35,16 @@ def is_enabled(path=None): def get_all(path, follow_symlinks=True): + """ + Return all extended attributes on *path* as a mapping. + + *path* can either be a path (str or bytes) or an open file descriptor (int). + *follow_symlinks* indicates whether symlinks should be followed + and only applies when *path* is not an open file descriptor. + + The returned mapping maps xattr names (str) to values (bytes or None). + None indicates, as a xattr value, an empty value, i.e. a value of length zero. + """ try: result = {} names = listxattr(path, follow_symlinks=follow_symlinks) @@ -111,7 +121,7 @@ def split_lstring(buf): class BufferTooSmallError(Exception): - """the buffer given to an xattr function was too small for the result.""" + """the buffer given to a xattr function was too small for the result.""" def _check(rv, path=None, detect_buffer_too_small=False): @@ -346,10 +356,33 @@ elif sys.platform.startswith('freebsd'): # pragma: freebsd only else: # pragma: unknown platform only def listxattr(path, *, follow_symlinks=True): + """ + Return list of xattr names on a file. + + *path* can either be a path (str or bytes) or an open file descriptor (int). + *follow_symlinks* indicates whether symlinks should be followed + and only applies when *path* is not an open file descriptor. + """ return [] def getxattr(path, name, *, follow_symlinks=True): - pass + """ + Read xattr and return its value (as bytes) or None if its empty. + + *path* can either be a path (str or bytes) or an open file descriptor (int). + *name* is the name of the xattr to read (str). + *follow_symlinks* indicates whether symlinks should be followed + and only applies when *path* is not an open file descriptor. + """ def setxattr(path, name, value, *, follow_symlinks=True): - pass + """ + Write xattr on *path*. + + *path* can either be a path (str or bytes) or an open file descriptor (int). + *name* is the name of the xattr to read (str). + *value* is the value to write. It is either bytes or None. The latter + signals that the value shall be empty (size equals zero). + *follow_symlinks* indicates whether symlinks should be followed + and only applies when *path* is not an open file descriptor. + """ From 85aabeb9c7caa954cd8facc7debf0cdf4bdc1fbf Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 20 May 2017 22:55:17 +0200 Subject: [PATCH 0876/1387] testsuite: call setup_logging after destroying logging config --- src/borg/testsuite/archiver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 6d8c23f9..bf25eb7d 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -44,6 +44,7 @@ from ..helpers import bin_to_hex from ..helpers import MAX_S from ..patterns import IECommand, PatternMatcher, parse_pattern from ..item import Item +from ..logger import setup_logging from ..remote import RemoteRepository, PathNotAllowed from ..repository import Repository from . import has_lchflags, has_llfuse @@ -265,6 +266,7 @@ class ArchiverTestCaseBase(BaseTestCase): shutil.rmtree(self.tmpdir, ignore_errors=True) # destroy logging configuration logging.Logger.manager.loggerDict.clear() + setup_logging() def cmd(self, *args, **kw): exit_code = kw.pop('exit_code', 0) From e689d8f2f66799664149f24ae9f3a20b88acb36b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 21 May 2017 11:24:09 +0200 Subject: [PATCH 0877/1387] mount: check llfuse is installed before asking for passphrase --- src/borg/archiver.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 35cf7144..79f89e97 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1229,15 +1229,20 @@ class Archiver: logger.info("Cache deleted.") return self.exit_code - @with_repository() - def do_mount(self, args, repository, manifest, key): + def do_mount(self, args): """Mount archive or an entire repository as a FUSE filesystem""" try: - from .fuse import FuseOperations + import borg.fuse except ImportError as e: - self.print_error('Loading fuse support failed [ImportError: %s]' % str(e)) + self.print_error('borg mount not available: loading fuse support failed [ImportError: %s]' % str(e)) return self.exit_code + return self._do_mount(args) + + @with_repository() + def _do_mount(self, args, repository, manifest, key): + from .fuse import FuseOperations + if not os.path.isdir(args.mountpoint) or not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK): self.print_error('%s: Mountpoint must be a writable directory' % args.mountpoint) return self.exit_code From 73123578ed81fdef6628e3ead2a591b1e3a2ba76 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 21 May 2017 16:36:50 +0200 Subject: [PATCH 0878/1387] docs/data structures: add chunk layout diagram --- docs/internals/data-structures.rst | 7 ++++--- docs/internals/encryption.png | Bin 0 -> 67927 bytes docs/internals/encryption.vsd | Bin 0 -> 97792 bytes 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 docs/internals/encryption.png create mode 100644 docs/internals/encryption.vsd diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 3a483310..d15858de 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -451,9 +451,10 @@ Encryption AES_-256 is used in CTR mode (so no need for padding). A 64 bit initialization vector is used, a MAC is computed on the encrypted chunk -and both are stored in the chunk. -The header of each chunk is: ``TYPE(1)`` + ``MAC(32)`` + ``NONCE(8)`` + ``CIPHERTEXT``. -Encryption and MAC use two different keys. +and both are stored in the chunk. Encryption and MAC use two different keys. +Each chunk consists of ``TYPE(1)`` + ``MAC(32)`` + ``NONCE(8)`` + ``CIPHERTEXT``: + +.. figure:: encryption.png In AES-CTR mode you can think of the IV as the start value for the counter. The counter itself is incremented by one after each 16 byte block. diff --git a/docs/internals/encryption.png b/docs/internals/encryption.png new file mode 100644 index 0000000000000000000000000000000000000000..e35120748a677f54beed68b35cae0ac8d45a5ac2 GIT binary patch literal 67927 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

!b?ykO!$GdjKYR^80CHTEeDzSjm+# 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 z zVGEYl@I3Fb-HP;F$|;iDeFM}JVRHxh4wLAvYC{9jN4!u4vxFIG3B*qv5jGiU>2u6P z9^xZ1x+xWer5Y4{S2cPi=0t=_ucjvJyrT2nJpl2Ndw5!>gaM?G!YwhwGos358{w2e zTLh83rx0TE2rm~OJp4|e7j3OMC~8m@Uv^8FSnsZxh+I~Q5mu^i)V41jQ2%nQVX zaYG|}oXD=n0-8la(38Ll!9{9^oo_PR9{)Mo>1*;jJJ%|@5lFv$E8^lj2B077Y6D%V z-pI)Vjkwuv5n4;;)=g*mh~0O@yVB`=ak43SqZ9NmLcmUi-pOU{E#s1M1m%%MWBZCE z^qaCH!m!xHnwU9R_LnTX`C_5E8PkvR&2e)|pd`)CeYQ_=$ zxOQcj%;$sC2}?h2X8i?W{b5yERLYwv>LS>o;=@^fuo+d}haiNyzE$)>Ug^JKp9)$f|~?DmHt8{+6HFb$@SFZ0zU?^4+)RdXMMC zjMgaS7jV^(#p+_iHu_8uysn9nXL+#}+1b+IOTJAOF+w{GPTR@-NVzMt-l__ozm?*9 ztc@ziSPvt1h={GNF`~Em<^-ReDRywAOt?-pHK}To#V(EkHDci~teiWVbW?9j`bu2F zet`3s^~|v#{UGsxua9RnVnGM~>@cVWL{mvP^F(YZGtAqJG+|*_U~ZFA-r*FzJd*pQ z_wvjL(ada)e31B^U%Le+FCUU1Z))Xg*?6>^MjJY@>N<`qk*mYxez3*iEEF|6HS7t_ z&-Mk*z?Qe0rW|W7%dAT1PgnCf3}^I!c-;_((ld=pb%NuqW87MC2}j=gl>TllK&ntx z?9*UTR|%spY1BpAo3P@?lan&rb6({b6D)DVga=jO=}z`CNL%%hXSMeY`U&Cmh8eiy%@JZRgvhCbPetkE*f5J#i76AL4YDa)%l)9#l^Y=pR;1v%s&M8>* z7~kILFakT@G^VG?9}z#2fVS@8UYxgoIT_{1>CdU^YEXN#l5KG?0ys=U9tiChyEMP0 z$rsRLT@STV6_{-hftjd12!!j&8+gHY-pG+bAVGb!R4%zC-k$WU zUY%3G6Y8Ql;GFX9x%#_A_r)fi8NW<&O0S)~IXmhX2KSDFPSXux-Ertv1s@>kbfM)n z3m@}28?fAg_ZKV^fswO~Pq(ZSY=WRozlaZ8V)Hs{ioHT4Ij_7mxg^EvCbt^AFe#c@ z{V8-af4a9$xMYB9&krk6!$VM!908pt9)H@I*|FgtbC67gJZ+triM0}RcO2C&p7d$f ze%6@Dd4<$;)T@x`KvR5tld7O5)0n3Au*tG21{Ydr}(32j2zE{-&4!ON(q>TW@ zH|S8Xx?=ARlazsq$E)^(KsU`<5$8w~lck7D5|g3#BIF2y0a?l{VF#O}d$wlTv9RY` zSpl)%9E6uIr5X4cbXkoo8(zOR7-9jBSiKMvAdjq6&e|1-@~ipoon`IRCpDQ8V;r8c z9UWNT8M9|1zgHfSezito6;UNBA`0QRmRnV*4SOG1#vsWxApEwpd5JYy5=iMZwl@`L z`%xg}Xzc`ctGm7mPW)^M12VY7L*IM3wdXxU!z15EE?uiBeI;s?xY3FCaoSGTb+bAJ zuZ+)Kwr{?E;JB`FROPlf=qfj4$D$tL9J(j4HaZmM&DJdeR)3SG00~{XO$iV5#t~0< z^2-g_v0&H<%KnUAq^QocJaow&cow5JudslycxB*F`I`c-WxTW4QxOJyv?P}TB>a7( zBU4TONBdN8q1eMVN+|YlIFv`bBE~&OpHY-TM8(%RbZ1X=BbX%{6grf?Db+@d818_wZDDtF zBY&KSeW9&GZqFQm_bvuW9r@So%arA(e!Sw`cxa!|UkWSVC@C-A+i{VV0&Z7#g!0$- zRIIKQZY<>Bq$nZHjY#q0$nN4MI!fYF?X)dQA6AmM_@ZQ+aJytyR560GZ`Pn8Evk&6 zB(w}HyEJDS3=Tb<8PcQ?6~rGp4_?OMh5hpdS2sfI>~ZkRRQ<9YPNT%O{KJdDHT_R5 zdUw(|d)8Il=w=|h5BN+k&wo{)kG3=WB7~#kcCq#!EC1K<))#up?hjcHtX8on_?eRe z@w|Z0G47An|6BF_|GJ)j5`_L|J^G}G|9@3aKiSCtm(}!}t+iE|S|L_Jgggu-^HTssnfkCB#sHTfLbGo+G~f?1vuy0h`{$M%a=W(bmS&=T<3QQk_xiWuk-4 z&k_AIod!+f&3o)8poj+HqcqM8{TkIk<+bp3MJN4&HY+cU#q}Ud!RR^ZcmN);M=8Np zP1feLemjh1QJybVbZIx$3~}&44GCVDl`!y5V7?Ix;tOqn-Z5qQGE;u7;=9BA`dchO`#$L-=0A%7X2sbk=vPA3j&%4A&Z{GUV)NI{gQm4~@CRc0MzxVPb)zw)Flc!jDO?E)u&Cg3twNYbiR@U7?2!j6Fw(wJg%a;&egY))NO zq=OE!BmQlMS?MOrZ-Iu%GZlYh8sXS`@e6NB%oR}d-2d)f;~~Po{nY&KLOUy;hu9|c zHE)<;Yf_-^NCq#!@Y?6&RrjekUD!1N(7geeB1f@kkWa^f|*Pu zLD<2|MsatC4py{1zw{9JNtk!%%TyjAPgtR5w_6I1hRUn2h3Jt&w$A8-8L3%j672my zI>q)#1@>~}YX?b(j1~1mcnXa)XrLGhso}LMDul5{ezG5Xr#mn8LI6?9Q9QajE?Lo9 zz|>7;kYZfYE`yw{(V7tMwdOkE@-sj2@^;>%-s&Bh?ULcu2qd?s z&}kZs6?=-ydwi9#)*UKT zn*2!K`iF0<*P0DiH*48tv@`YTtG;|2sIB=DQo_N#PqXXc;V#%)eGqoa=h5UIWHfPm zAb0(6;HhQez}rrBDB8-&ye#=ah3f3lDZDXGvVkTqI?+r?s=>Wxx0B2lSX4)ryiyo>dojgXGZx zQHtG9#xFH;F_LM>U}%xIQ2@pXs3w>o`axu`ut*xJ9Xp-&^z+zlkwXi;10TB`WAM|| z@p4>dbS0iv+@WsEMxd#2&X<0CQHX#05>}9dQl7OMaysq2jZ@+`7XMJtCG5+jyALKi zxG&^>uLl5j-PfD-8_2yG}~oJc*#rOinqB?ZS; zt8!;jrH;j;%vJxgxXbpls9W#hi>YVS3h5uau5C|@meeC4{Uz?bgc8VYcdfBfHcbNY5snc{RW|xeY^;7wWoKDg=>BzN~6)CAm?qzI{OGQCC+LEl{*cT z^J`?T$X?zFpR3_hU;p$p!n)BZmc}RxLrBOXo~T zwrF<;oP){Xwp28u#Jxfo0w@IDS-D;^nl*R=6buWUGv8i0`HiNL;k%8VRe8`*>relK zIAxYeesSYS4(CJ@yqU!vysn9s6sJ_i+IR!n!yvnXvf2Y64&y>YFTV1e6_N}w`rh&E zFR5GZOvW~*c#3HuZN-gX&ar!_jrKxWQ22}0vNEgL)c7@OAdsN=q)}O_T9(KtA z@J8(Mo4-Z74_5D2&zwVtc-`i;u6wZm&xlvQM}Tv_7C0uJU?S z({gI#pO*gL;7Yoy{(7yC*Ur?!e_1V+=}c>0y(!cp;PJxY*GtDIx66M(UpF3K`SO?J z^bva2FhnePr=^uf0&%)`PHm1!`=$BHSAjfMMz)*0FD^D-Jm8;*Hm?7g)L;4)#lIn?FE-x4X&J^!Gj+&WWt!|@zd9@PUAcmJw4M_{_qrSL(;7B_l^bGUxuN3$IL z1DyvOax$5afNzKoS=zj~S%}xOFZ<{(a&r(A!9`B6K4e3rq^azv6D@Xg$tc(1FjURNbZp68P1Pjv}xi)XaTXVM1A`cyR!9>@(OM=sVd z<)u(V89%qb?<1&Y+!TG~%iWuM`+cIj^xagYvFpKpo64~ogTI*00RO4Lz=Gzm*y&1y z#r|pEi2WZET>L8ymp!Kdx>KrHNnRA^+%zhj^^nHZQj>P~oisP=`$SL4yYKcn~E>S01(>cMhFN0my7mTMm=JV0IAbDt%m-DmC#*C}eu@ThME3s)t7P zS`OaJIWtN?r;t!s7`olsavet;NE1^^vSd%Te2ZaME4JGG{rPCFAykjpNLNs8SEy3gmX#>-$1sp<$|E zFbG5+CkN)ETY9|et(5YFO6E-wFTV?10W2u;pChoGp(F0zX>FE17aj^P+_8qmEvoZ@ z{u>dj;*iqZ&^IC@^!+KSw-!g%(fgQE9I#(NsL6mxKp_93XW0Ep(&3G|6slK$`>iY= zy7hlXQE$@7p9r7SS|Hf+SSIS{wVoq+^QtpF&sZ-{o&EVYSsPi*{>`pzTJnU~Fr$y0 zQ&a)%76-4)UoPL3|K|MTTGUUjgV534dXuv`&COXoEcdAksj!`NX|H@w1vy=uV+=@6 zRsdu9qJI9>@|cRrN>206dQcMlD&wcyh`zt$KJ2R-v+XT9BAex9zAUH+1YUg;dm}vV z0nL|!D-E2TFR#%EUGD!LG%1{gWqxtNL2cEUsqoMYb(p&%#8s{DR9Yj*p8cy|rn!B* zO)_1HE*97;_H<10%lh#A8zIljzN`jLOJ+ZljC3%JstEnYGuCBNdh74>;#!Ff!ysuN zqWl8oI5bP+irDbv=9_8FykFC}YT(X!k3)Md`UxSmtq&_n?g&6ut3L_2`H&iAul zPGL{8rCv=HV(OljLU+;LAo@F^buyU)N8GFaZPMvdxxAk~jYk1N`nBC}qe5n3? zor+1tFljS(x*kMH)<;&=o7VPn8eGsEpL8QCH#0PUat&?OixHZPKx^&Rk77 zJ8{;dv8BCU7(&qV`{fB!=+LH$WlKJ9Fx1I*=>Q`JjZC@`Hr*iW9~4sk;QPI7?Z=b# zQHO5(EYV8jQV@t(-#=V(lj`HmmihU0N5w_z`mT14 zoHr@S{#V0#wq7qTXU@@_J*^!c_B=4+l2ZiaW*GNE%i?=QtxbhEn;WW!cw|y+s8Mnx zLi`K?CEL#!s-~B7E&qG#jgcOX+4f6=j5P}7xMdIbM~!`Sr-gi`mZmQ~WP4C@d1!O6 z3{rj+K{0ys<+NZ$<#dlFoztJAADHDV*vt0QoAxu9_J=BgJB*roskgJBbSk@_qp#c-nuJ(I zT9BHd0K4*|vg2SLafFFVmY5uekhCRJgQe0lO#6l)MKNYK{poJ6vvGl>N5|A-revDF zGa`$%k_($ZDNycqLmt`1CjSx#1CF2Jy3^olhmdm8^ANmALs+hfO{b-OgcYd3x=Ua?PMm&As*|?}TfU@vg-Ur(ee(HG4j_ zGEvb2H=k0zmUGwEp2Y2_Lfm`HckKFFj(+GgS-0#cbA~ST`~msV8I~- z7&Iifhru0!yA6;83ju-i{BU@`LwZPj{~r%2GB~>jAsjC|zEi zGRZUG+H0_Buvu$I7ZkA6eX<+lvBO#D$7kM@qIU|P8k)XzsN<&!-EO~rB+8szsi}EP zhX=$uKa+p;rO&xRI<}vCild*e&@(zm8oS?P-mpVI+)Ye;fcgV{v&%I_D-cXDuRBIm zo%EH>4>kA7!FPeDr`3d~39`dF5^Z*i9UbN$TbC$?ig_5&#M})gg0gF+ z{{~+N^lqL=yPMXe{oxCq<9=MFNlI&SA8|6AuPwj%zymf$vlOS^MPp*^-?kRXecMts zq6?T16PAczExfDzZE)NlgUu6^0cxZQPlGUqd>Wwj1i0FAe zKl_~QI-DNGA`}NyFgPi%O^LG{V!WwK5rMPo%j})CbeQm${*F3|LEcKFMODLOGnnvE`D)fmHRosKJ4M2a1hSHgR}_^VvIx#Pkaw*JA+^MGZUew)1iYwwytr7k z=m!Zt5BQd`4A6ek;eHK(RNxPm5!lL0jRzSQn(W!W5(>r#RMW zTvwRM!==Rkuu~G|a78@S@AK@^GP z*@ImuM9F3w6iVzoj0&MJVA+V06DAFfBwGvBf$|RqG`B#wm zkqLDbmm|dQb?W)u5k9_ZJMK1+B_W9Fo)?{K%6J4jjulliIZ*&S5VuYYN!$IC`(+ol zQ`zz*=r+nrC?q~C`WYPTZ2CsNQFs|4fM$xbH|Z9F&paMo-B zl#x&;R5#reD3Go(SY$n|4>F9>cL*LaY`C6Jv)tRH6Al9 zf4#W0+P=^!`y-svj7O9&yi)<|%wg{c6T+^A722UT9GoOnpF_e18bYZYpj)rEPpfh& zI-aVZG=dfyJ&4p1$LTf*?DL~1vmAxNnuJKX1_WMHv?j<^!h*T$Qv6Blwd zWUz(T4a{%eT}>Y8HI|kkHHYehD`tuhCGOI1pq# zg%3liHsWfi%U`4{6N`C~JMP-ImbSj(ZsXJ6L9xk60cs6TpHFfACI-OF_0Q{|>G)!l zp$ wMk8cI+o!&tw9-R8KGqlhqF>v;E}3}dqp{RX#nF&qjg6j6<*YiChEcQk0%+q zxgx0eTKSEAC{f_%dew*yWVBbFB2zWttwB-h*nH+#HDTS(?qZ=VnnAOnMn(Taj$o&% zJg+ewc?jNIiP;vB#Dd*pqY5}}ZQbLiVBQ%GXsTRWYNmgl+Ee`G`l^Z017Fb50nR4R z#B~{Uob9x=*egJZvXYRVMtFa7t81};_D%7xg-9;V-9B?U(+mzRzVx6s6JZ73SSD-q znd&2o)<~MxjoWPBDRUG;oV)7vll6b}*C0}`?IJIyq%n%=5H^Dvf6cpUnz`42c*~1j z<(sojZ=^(0u7x?E1iyj+yUn04^GT^fn{#H3A$WwR`7E1;@@=g&HNv{qBSd>5>8JK> zpDYjBy4Mo??_P8f8*2BsS{(NgZp9RtlssNu_*C}G{IJ-qV;*qfMl9B^k5Is0H`lhB zUZbs4Yq*9J&voT}MQ2Hhl}6~xVY92*f&ugJ61Z){w$ry1m)lwz7I);AM~cCKj`FG7 zxd*jtL?`_+iDOX$ft##zg0nH<-#uUHHsXb;$U(fz^&?J$e2qL#{SR-qBfuIKUx=B7 zu$Ntf2aQepA62FwEXtT6Xn<~@UVb#qsp$)aVC|`!Q;r80jz;bEvDR8=Q+YblXWi&Q zjoxCFccAjvSI>XGOCLrX^Ggv}Wndy;!?>POD4 z(+>s|?x3ymxWR+k`K)8&;*6_`u8~igxNVVB-`d;JBQEMhPQJgsJR%1K^@hDzY&4>D zX}(MNF8ugS;-U$$N?Fx{M*x{74gDJ5U4>h<={a`5P@o(ah-U@j( zJ%nGQmK5oz_toElipwzw_4$$`JNr1)m{z5>-9Ut%XbX@e=e!8P02QaM2j1ivy?d!; z6SQuoqzmKW670-QO9T%T|CNJY$5f4!ZJmhp3q{S^l%5ABmoVM$sc#b0()B=;Zf3JxG(@ zRNR#6MGk-XRiMedhV>D1j7~0ynndR=RK-()0W&lWdU5OO+$NpyVBh_{=lqZqs(ofb z^b#CU#lX(?SnuVkm3?pLnz|(f_RsTSx*be_{3=y%Gksvj$j#$+K$%(4j*<0LkP--V zj(PcKm1(1oZ%HhzaYi1ya6Y3O1z9UbF$p-XJi@T|W4gpOkndg?1EgF#xq<{sORD{= zy<7snF*VegrXU~3k1IieeU3IL;*aXF0&J{oJDkWf#D_U=?%0Uacy_TCu`Jc;-%}2; z$VIE%R()!@8`*=`C(}8qK4b;m5&_xhsR(&QX$G7CzwM+3l|B1gHD`y1X7l=-7{-bB zI;{bWYpqs^Qnc}1@n%jwN{POjtn^eGIn|(#v#v~=Wc;!ca$`6Vu!P;FHjyUZu?LY@ z{VRy}2s+>5z^Vb@d|b)o`k+QX-!11!;gSN6$#}L_O+AC}?bg0MHX^9AGrrBsmyHOI zi!xjV>oJt6ljY{OVtg6lMIKlFs0UlHmk_EjPxGG%f#T%A$PvvI^HDpDdv}+?Rl&Kr4C& zcOnV49Lm}KF-qKq&~i_r2N_B z{z-~Xe_GOm^ezX^pY$1qKTjxO5ZpQ7A34qkP4?WThz$e$fZcP-jVFKY_<<0u7koGh z20tz|G>08GG8ZBZUy_YON4`^+OIhSEx}0HatIY+IL&Fec#~;h*nNxmCuy*~Y{NVlI z;i4QvjjYM7(KKuDvk(bkXF_it%gz#nolAFx&;yg%>a*8%F6|8gX?D7SA2Ke70#~lG z5gUWxnp@LaUP=GFL`bo6uRYXehK>sS`IFLZMOnfhS{n#wVgF9(VAHxp2zm=MGm7@6 z!KffKD0{kkk)Fp$joXt(k?2JO2qinPR?nt~m- zsZr_R!Fsz?27nW9^+UFb?piH|@~{%bhI3<{N&{#aV-!a$3M2Z+^&ajNh4Gh*tbOqn zt)5Gq^LFBTu8sfc<)0POiu;gTvep7dq+#kMw}rp6|m5n4NWppU`wf!xQr+yQTm&j`ffQ8Myfa>fDheo0qKlgGBU$W z=`UkBu>eY5_Xc(-+8)b3ieab8X&?{#R@V;#fUN2BkK@k@ZbGdG!Vn6#QEcd?DMRQx z@$0h46&32Bf>WjTdTbG5-&e8gNbMg=<|`|L1CysxpA2I&2*a2MNMZ4lBGA2gZa%MX z&sV)e3xgTPbHjGSrpWW_}E@7pC}gXeL+ zox4d^+k$cA+uVweRi4?r=Dk5pCV}?VGV~Li3dGpcE-kwVm?QN#Vaq&rzW~;1#y0&( zNi#W2IcTVaN|$Ytr(M5Plh(}>^ORT0I3$`|_C-GrqBaq>n#S zP?}U&Htwg9Q+b^Ew!mQ#Rr-k;JwAmhTyYzUwtc9xhF@@2D2-KJ5p})3{$j-T`F8_L zUU|x)mho z3?7U0)4$@A^j$oqpkWjfAVY1O7ptLWSL;pm%5&?6nvj0WcJ`A(4p%uP%4gp zHug^cs5dDzrJ6Q=t1T1Fn%SnGBXNK3k8k$yPnW7Z>D{bl(>J~T1QUu7Kh7RSK(~RR z*3u~w{0?Hx7a71*)WEsZ0djni^7U8G9yMIN^mDje3jFW!(A8md$NP7i9K4}|X(i`D zg4|^iDBbiGP0sJC?*p_D9;lLTW*d4N7!TT9(yck0-!HcfAQc^8p$h~}%^TBNYm2_~91#H5QTui9-Rv3<%Ejtr zcVtV^f;FW*Bm4KD+D^Y2rmZ$Qu3N{Njo0}!JmkN$&PYNHjDra47EWs)s#CR5UyJr% zNQ>vtTm&bksg$NTU7*8PN7Ev3jtzD)r{QWJN&Wp2huhW5{Wm}-&^a)%dzF@UJttLAoq3ggOt1T=`h09CX{po zbWP8fX_*QQVjgJh110&v9U4JalVYh`QG?9fVst6LTFr_%bL%b?7FGJGWjM!BG6lb> z4VxGD3*U1c<Nl8G;Mz`Ufrjq1VppoSq zYt0lz?BimcnA*uh9|q!@xp_^d;Mc_h-hX4ZnKw=7KE<{jMlhlO`z-3a>5ucbJtB-n z`aiJX=VR6W3^bt_e|wmQM2lOQq2KiYmvp;|9CFo6hMfP}F^A><_5&-vmU>7V5UqK5 z)oDs$4{x~oyq1vH@{fv_7AJV%*X-p|Pedss{RSVFyBir`%c?EAMux8_>@|MVfq3_m zm{q?ECoK|vCl(+LF^^!449e$8I8o9t%z5Vu>Ad!$K}QL2gTx;P*Ci!TBIvZL8rxjv zhs0G;Dc0N_aul2J7ppc^x|?dC7@#;U?GZ1^7Y{<&uET^z@B;Wu$+22{j8*`*>NiC? z6POFnf5*CGGBr0dN){NO^$T4zDD?q}(5o?z0h5i%Ec2X`H%jDPRGu^MB zNWqWYiMqDmLNdxBzG)V|L#h=v>7S(I1Mg)mLNYGl^}Wz*X5jT7S+P^*L-pZ?M`cLW zK^?y;MDRNum?OGHI!lM8>53rcQ||ILTOQ@@M+9?idr&)ZaKmNOggqM>C3(t>o9~)s)`)?qQRywYgEpxzwHVIufE4>#ld4& zuo$TRZUVXJMOritW&k4z>0$^HKpp2O_uGM=)7b5U_^+C)wxNUHk(ikO+_bIvY7TE0 z>Mrh37tE9ooM+M>({!-#f;}{b=k1{1cw?R?>?);@6?Cb)Foxy=IUeZ2V)gufRS6*s zEa#yl90I^nzhy7E>xS6@zef>56}6`Yw)zk*%`R^Cyaq=I@f>!X9O(*c{{iz*V@v1w$@ml491E)zl1zDPH|$DhT-TBX|fuw?LA zYZ*zna88bPZ9r#GZm~e;{?3Dw;*iJ{p|lis<7MDXP>pTqO&raW7o5+-{4Z8k?S4?V zEUlnHZ$Q}6=^HP?pb~NydeE|zW-E)Vu9oVVJlDBFVK5mG0CQKLV3lFJA-G?(VAH0w0g}wJ`vyumi(I&Q?Y4hjeEaB-kI!jUf2iGW^ zvbX3kj!&7@#EWs;UCC((ed2;2UpaXhQhxMv?@=lWBOqqug4=+$kQ%45t^eCKE&S$z zsM;6m%K_f!vw(!+bKuCcjeL|CM_1(L0~LBo(cc`J?->SE93mrbr1N>I)|pbYn=jlb zkM>NqnCUR0nw|Co@evAk(|#h;^qtoH)Gj`JS66|d90Aa^uqYGVHDJF%hwoN2l@6gZ z+lYDkd~L%6CwJCtgn@yrLiLA+a=J2VPCKoa)kO(Sn? z23T58>CsI70uQ~iSjwc3Ov4Pra~y)U1C`Jb5_!BUA-~O;xcUfWKSeA!s*DGC^sQ)1N5kr>-kO^p{Nf_n=hoYIC z%X67ELFvgugiv5&c8`%vHin!ak~dz@wY6paf5T@fyY_QZ5uV#n(l9cnMGba&gWb{V zEnBX%v=%02K)uOpTC`umQ^VecIdbHb?L|$(>$y5)QNH}Is>$YT@9NZ7>Qt2YU$~`j z+kwa+Ntol*F?^<(8wM>>jd#qOm{~$@F?!Lm@2hh>X3*YHm)44+e*y)ZWRVn5pvVO|q@*N3mA z{NwdrRkxLToELj2e`~toYxR@{a!eV1dAevqU*%y;XpWA&?k}rc(t8wId>~7G`u*#u zSwwQE&I6-sRJbsgTheycx8oxhkrq6yTOL>6^RoVwSfe>y3%~mp74L))EJ0iWr~GgM z629aQ!esO7A88`!l<`cd#WO$xth=FJx>GE#F?z43zEDq0f~C|DOVg7;8kTTw4a2Hk;C+wWh3)T;R)rtkG+sR`=Xf1s0h zYDdrOYyCx~y+T`dX!w5B4JTsy>F=!-n&it1D-7s%2j}DCSh9rR6p}k5oU(Rs8LpQ@e&*BFlqB2w!t#Pg-a-yYTV{qU3KyTt{p-dUe4- zci%hbFjrm_DFC#GeYn?+GqRnp?!uMH2AlGye{+j8CnKw|=X)^4pgUz-4Hk@4&%GwCRZ%TSIO7UUIR{8y8Xv1jClw!&6i^||aYfK79>G;of?A?mx@hne42_2`W=Iaw z3W0Rd^U!&5@AH4`k$jc`33MJSzxoO1f9`b&wkb{cYOkXN)jqlU_|amVAfLr|(PAs* zf#Y?~c15M9k4xrvvP-3>-A;fn0JT~Xn_ZT$CUmzKwJWqJMz)Sfsf@T*_J2dj7Z|4k z&Q%Xp_vWwHI-IC$X931`fDn7(vg0@dX#AvcLl@~Qnr_U@x!XzDXVJE5iovanH1<+| zAD3oU2jI5zmLC#~1&5`u>+X%_@I7cz@>UPVlPv_)0A*V__-+-xq3^7(2-H@hRiL>r zZXHtfR50*Vkhn?wS@Hv~yesrHy<@2RlVMQ zKm6&K3g1gRFdv@94_!GI3Pqap+!Rb5vE=e2Cs>lzXj{<6OQ~@P5FTGGmRtxHZmayf z%-j!MB8^lA6nmlj!6!+k_7TFd+lI0cx8n-|^m?K*lSDXL^d4MaH&;`gb=WE~Pa*sq zi(FJ9%uoe>wLEFPg1%CFebvuoA;ZqZQ{D!=eN=>$p+MO8KUR35)dP0JQv%Rx{pQ%d z5GNq~NqNrMZ*MCI$Ok!p9A|EtxWRNMCPaa3C$?iVuBJL(zjB{@8<2?%o;Ru3&-Y&Z zp#%5*{PkUwOqOZQ&Ov>vD`Akj<;I+CuK{ z7#MsmZ@_6@3TJDXg0c6+q3N}MVLA=+hyQz73TKz7)eKQ75r;%?Mhmv_0 zA23g4%kHX(py|zvSn)KJG{nooReybTLUp@3T6zb>88?ys)}2XKfuMa7)tv`c=s^KhP`(@|9QZ1|HT!yeaQNhd<|J*pN9<< z!fDMT5KTcuLrg<%upi$d{g)@ustyx|kp_DtfZR;_B>LCw_T-rv)3BXlYMW-@BLBkU zI9{zw6NRzdtlfLmX(hi0ZW7(%z(}gkGtVOK6(A0;JUSI=y?F%X{{)cULP(7}7JLh( zV+;O{;jE+eEA=bE_1esIX$7ZkQkK&q;cDGepVc)>QPw__^LD;{^-Zl z@RVN>Vr6}aP$nj&$PU|4ayQ)}v`fra<_#q<5WtphmwhOcie$p-$;6@@RT^%qDkl@) zx9gjr#4BICNZ_FbRl$ZEVk>Zgl9PO?`J1{{fs9BqAIq=uw^zJ!+uyVHOUEiOfC6jV z^c%eMDIcw!!@jcFP_K8v-T_V1GQ9+_sk!5f?s1*R{2sE1Q^$ky#u_Q2~|gY zv&G<0aEihcw=cG((HonQoLltx>diD% zC^x`AJ1zM@K+^I^n=S&vsuD_+SVXbLDMU}+tpif-oF-S|fv*te5s@3&P8Z|R(i<#-8j&a#TrI<%p(*!_ms@=TbuDKhgxtS$Id z6NDb0K1uuK1O}R_0cwl^?g??(Lbgj=`yAwVNTnVwZQrkA;kwYm`iD{{gx(V?&vNhk z#`e`tbT!W^0=eJ%F);Mjs5)m`Ut+tWh4lrEItZH6%#7}X z&$trpksk@7Fq*$rhnTxNoPo6ez{-toI(o2QyD*KGqQFQPw4#jf4)JnbvMN4n`lYIA zstta518t}{NZn{o+X$nUR!cPHbV8c!T}Zb9?p!4jNg!F$w%VILhNLNn89C}voz{P( zmYbyJpyz{~5CO-7j5{c-VlJ!@1|J!f;e+ipokyWu{@lr?NI|ZnUtiuLk5gMkelJjO@6koGT5u2CV0qP)ol-wZI4)QhWu^~oa(zE3toTA^>$>sq3yL({PY}f zMAo6;cM$*2z?bWrksJjXg&D=7lI0Cef%|T1W6IGOtw^_77O3VqtNdK+g?Lx=GlvPz zWkwFi{Hd81*rRObR!5Xxyn}DyXS*rh^Jj5#asM&@)nJ)Hh*dQa^4$Fl9DUa7FS zMV^*NOc&_~)Fp+B#x;s{Ir#0{^~(eHe-?!f*8Lc3(^Ty;lR_dDnBQudI3VMkYP&@dJRL{qQ(( zs664Y*xs+*;~)^~e!J)gbZ@PxpPcg{m^gW*IrPn)dpf2ty)flvrJLv3-}Qb2D-%T` zad|WOsj{ZIV~6kc^rZ^gF~xob4j*xIB&y_ZKQ>Cz2*$8opLrGT+9Ias)rFZ#coxcwye|Y8I>zpEXZK(dO&!SN9?|tiW6eo0m?!X-#HCwWI ztT{u52bn5YURCvrP$IP za*R0}v@_5L{jtYmVjx;97HS3$z$^yty;Z6)o0#<| z5b*=B_ep6$t(@wll9AU}S#Q;XZ*WoEmfwa8zS=81|0Q^8wDqi+fo`5+CDI)*;$E;_ z!x?Oqy=NQ7y=FSzKZj%PqsF{41{*iltkkF2Kd+0@9O#h_nFlEZ^s{xEWG%th`RaQy zVnUa4<}7n(HCloXN~A;q&97^F+iHL7%p2auO^+3IyD1O6#cS0#UR=A|JZYkfMww>o z%*xk8fa1?T+F$Us*jJgR3c?WR;UF2Nn*i(KE6b{cFgTDeL<=zfh1<|4*&)5nN zPblpbR#s>0D(H@0p?1A7mBcW>PW20@yzkk4v-yIBq33n$SW#JNI^R{3`?6cdpqg{Q z@pRMCxl9cCN=2YQ4W4s|x3iv>fJ}!_D(}n+Sck$Qz@l;fmI7|sFsQqqL!kNqscf~x z3qeO45~r`vnY)>Dt$)cBLf0IAuM9VZ&#IqCdHKUU5Hz>NFzS+_Sz+>ZG;^?=LSq~6 z?X<(5!&Y;Y11j~ltEa_EqV5UsSJw@xt=I_5?|rS5LQci@eg51^mrJkD0LImbhqsr$ zlEo_2KW6l$WcW=MSsX9*{3|+h1|^6-h;(o)W$X%K0zeVGVu$GK=<$e{_NVie(X?-N zIA5_-?AQH!3d2z8cRg4mHG-+u`~8yGLsyvs!M>Hx-Em`Tk$l{+x-2Qh^Az}TZK8h9 zgWy(}jc*5YX$ z$pe_>Kw)8NVd2ftf>>2XOH9FzhNOz(#Q`3}h-=}Ljn+d;bY`ny+$M?pc9EIP7M#yV z3>q8FLXyZqSjpz4g1=4^lHzY>frj9hW# z4}0DYiXUlC9t!P#+=r`a=JOdwzj_CwBUz^$?eq%5*0&-JSP8#LgHp}nI;MQ!FpREt zN3(sQnfU=lZBVgbO&zk-=Qk{Gx_Q{zCBnVwULT204#D~Lb$DiZAh2Q(8%}pnsWcWC z5Lr5t0=B&~aP%S>0wHNm4hjj!Or&oO_gtn$8#3CqboxU3bm405&Ak79MP2%B?|BzFtg6H3 zERHDgXfSsYSg<&nUWY0nZVriGKhXB%k36VH2B6?wH^tzuet7 znUOXKP{w1|z9LNSPaiq*A`$?IOqBck2AcLT{2x39mI0(u&SLpjDQI4*^C%~M-+2`y z?T;26j`rVX{c3T-Q&-jokhUqB7Zp5m&41y8p@4Y4)HlZU$Nd$FhVwWYE}`cM^Mg+@ zQUqY|sgok#zCiK1%=*XjIvQRFwiFTmCB5|~_QZ_6-!_mSUJl*uP4CLl(iX4H|heQcgyu0;Y)ZzfDHF0r**A&o@7nn_5TVE7QY?3CpYE0&RgTKo&cWR zvWN44QS;r4779NnV$JAtrg?rz&!dD5s|}0lFjUdhnEsEs0hiarHP+;Dw-{HIvSfN6 z`+OcAhoUe{d+kl(3X-5@4l?GNj65FF7W-FN{m zO?VlxjauR8eyupZ573}TktnhY-f^7`{hx51*ZP*_&iUy0I`YTQx?e)g&%N?)`>%Na zC+4S*y6~L0-X%_|>)?|3Gg54S#al1x2zdck>8@zRtJq_sT40hFZf*eu?!Z(B;y>^# zg8CE$Txy+V9fiiy1&*tVt{+ zjt1;7vtz&i+4topkDTXTM?_e1sU&$fV{N*|9D%V@vZ`o0B-A{}3&6>oJP8lL3HZg{ zapFHJas500tYvUL6rYL~ps`kD)@Cw_>Oyf{s2?~L;Kut8S`DYUl2WUos5%o5eQ=OM zFOWW0Z{ZLU5y<(4J5Z>m@}uk&7@J*Khl+T%~^sQE(bY=mnjdFIqkBF7$1cXoaa~dgz)a2UA6|@h1FGWHtIyL}g(&~42-@ocg1i z%uh=MyCeI+b9+hJIgW5)*rN!yh>OgWO2}W=eETJ;#Wx@n_1~Y_Ly_XL9zA_^52Ytn zoD;l!+H4tY_#MEWj+K2H_MN3BnNI)x>1>G9xr6g7sP-h}?AKYL@vu2rJ0jikb>wKHfzp2ZX;hI<~&Cg6lLn)?%M77CSA8$C_S z>U*LcSl8;}zd%-e{PE?Jf8e-JQ`~m_@q~{3XWMV)zdn8AWX#PEDnAicv8h1XUC3N*G*a9Tc=t-HKp-Hw<}Z z(iNF|PSf*m;{yCl;yzqP%mpf&Pc@f&n+f&l;JrYEo_V$}bwKZ%Zi>;DqY zf9p?h$*1m5$jqnp{%`#WmiSNiC$#0$fP;qJNm~35xl&15YRXzd!i5{)F=U zZwEy4Kf>&({sgG}xBf5B{-=(3{r?}2gCbz0|Bmi|KmNa?duV`T%P<18q6ubVwwp$i zV^80bf&kLkf_eP%zAr^gTh!Q8EKyOK9@ofS*TF?V(1!Si;f7fu>)aX&(FEHc!>%~3jDke~p1yn4 zA^J1#z5ZDU2&5DLgZaQn*E7V&HE$EV_g3!oEgR+ex;|wpy54dM?vZw^XBtJ_%mbcD zDrAj+DWVb-b*_u%qL_E{&wKz2$ot~odbCK|I(;Aw+L;f8>{At5slr=b4jVnwKC=3E zi>>qg)f%XAJ=><5tv;=z8aKmbU1r>r#zq6}U+us{sH`C|{d4^hE;Pa`dutq@u2T0n zO5Wc`tjp&A_IL7tHhiq?>gex25P1C9(S}>H`|v3I&?*zg%KjdRR{24>DTJ18CVbIM zM{SY}y14(>r8aZNMyB}5gK=qDXteIs|zD2+>W{obD@&2J)}~t zAJm@DsLRTAh?0G>{iW!>+aAZ*POrn!L(WGdjViK(iY~iJJQl7F`OlzbgH?BLh7Q(YOE4)#CJMWiDw1w~s|Di>jwxPvwLglla3ABmj zT6wMyV+c1@t*|8TqN{(~2WeWgkN09g^|w`g8ys?4(3dG2G2(KvO(Ro?1Y&XeX%%}y zEosux$}Lz2ZTXkN2aR2NrWsCzi%C)#!+_?Sq?QU2XuPnkhy}0Mn^YrRT5@fBM;)=$ zQ{q1xC(*WakD|&`rxMY=RyRoyARC(t&w@pBM@^6xBNmV?!@Ok>uZ;RXBn>nJ8T2EZ z#b8yXB!c?Y(BJRHrPRCMJug-QInD|n&)0-~YGK&@oLh1z_xwJR=(+N&QZ_mzzhhaf zPTX4u@a*;0@8W7wPXDbJsyw{O#?K?p7XP#sxt7P7+LLX%nN$ppGbw#yK1o<1ev$*JbSlU1 zOnr{lj$$?hNz(PWf=uJYBDB|{9cy1FM<`nJaOJfQk|f(jj}2>y?C~->cy&MW-|+@! ze3F)RJ#;>DhIf90Lw9RsQoT^MMJqiPBd&qj{~W51-{Gd143iXPToJ#l>qZ!$L+f#` z=T9liPG5lctvybWmA~`3NFO~5xtC40iyCuV0cqpMf&n%L=H#@y^M`8XJbpejp5G~F zun@seUJ<*P1xaUi^L#?!7-){JJz>m;X~m1ias4Y;#RijddTf;hDk6!y9GKu16n9eN zXA7z74vHx`RnbyOXIL+fa^MPrlW_vvTD8Nz>yapKUk;gRsOx5w(PcoEN^M4&7;5qH zv_x3IVLaRd$^);}W=WK*oj7=X zB3Gq%`#Fi)7Dqwzr1KJiDgdxl^5NR>nq@EvOCBSuMF46RjoIk@9G|!W_wnd`K(6{(5-gXga2%xWWdt%py6E0 zzBR&5WtN-d+NGaK2s};31W3b`A(q#iz*jyW5{pBG3L+%Rc2`asT^dM7(2UMYBU)pO z=Ukp`mE~ej^FcMLl*k!_0@Kug?9&1d*MJ}A;K{-xO+;Ip>hNRnx+7e^3g_v zV*Wp(@oZT`4e6&tUKLSdb6$4|yT(Ua8$3eU!L6M9ll!P0DGNDTJ?c?syy3ozuG-Dl zCF{C3NQex;O^2U&z-IIBdhn1)p^M+z&TQ_rGjhVBc4h*;PAL(NT_tyL?T3R zKU_52Frqmkw!(X87&miC z*!NC#W4zJt6HCI2CT4xg^U*qQ5^=B(@yl*jPatpa&+JrCXfQ~#`i^w3#4fk z`p?xC50KSIY5_4=?YIo?vP~x>$t8?$21OQAO=OCP`G~?vJ6Ufkw(B5c?BY!l6s@e1 z(n*9r(LzXyhS}LM?Y$dzlDa7ncwmW%ApJk-i>KcXZ|eywMlr^x$?})a=z+Blou6K> z_)qTzMv=L3)c!tN!Kd2~n~!&EEhT99KrP|acHOaqFrgw;&M{Fis`R!j4jbKwiC~B& zV8CSFJd9N8FX-XqZyFCFAyDfN8NO}jI1SbcfwyI@%;0f~l)Y{4qPEkhypM&|F9KXt z(?~y3k(yyFsFB~a@yOehGR3tO0;5(V1#=?b*GIBqs&pWn+Cam)MAl;9PwCWIKUe~R zkti&M-e|)jDKIY;6%n{VF#q!C!t6%T4y>a|t2b87E_xP5SZ9}V&X;K`I5wQS1J(ZV zW=ibWyf<3V71rt4zhmbGD?wTd?}?#P4e9!IwM0N#w@(pvs)IPNS{4h4LyX7oa>8nV z<;;;~M_X5S(`Zyc(Fef=t(*81Y1)XjPL>%Mj?HVd5R)GiBI{)cH`cc{h898N*R`%2 zdx+qkuKU`9_^h!I2x4keqb$UMk1`^Ms%q^wFz?!g z)%l=uUOaX1e-7Ubcsq)1ES0fJ5QO-TrW9T&2+ zURuVjl7>X58BE=f7nDry{sbU%$?BUp`?tJ7#02mm@)>V177bowD6}Dh5?dw6-q8lR z4oUL~OK4-*IV!~6BV|OlWZb(kM_M$OFboT1 zDhs6U!8xcCkmt&7iZ03z(^6b=)#XXvHLAys7-gjg)t!39pa!}@^l#}J3QFV~Dj_ra z5}_X!%&nWwG(XzeKHV|KZFAGY5?&RqF+d}df?wbN-Kg<3H>Fj`$>IB3HDMWG=HoZb12j~2#U`)NvX zA61VA6ScGxW2qSdKiID^PdX#p->z9;OBCa$LX1%>aK}?78lH@!@^vA#?a!Z*Ype6@ zpWv_G2%-InzT;R+6k(_$_T#LRa9u`UG1D-jPa63O?&<#u_CunPX}w4!y?=zRJ@ox&GUzQx93`67hKX^=;h ztC-YQhP~i?n>#xHP{g@SDu9Fh5vlf5!Np6~FkFG?0vYkKIKFqpMJ-Xj8rY$(Q*v)p z#nrOQ2s{;q#4>L|$o62&t8)fM8z4YNxa@r5`}=nmX|87m8Z-agw_szOV1pGCv(#)% z@aL7Njb5&N^Z2))UUNCS$i_0dfR2Xz3F`1#Es3hursrvFiqoQC6UH4U6YLkqf@J-!k7RA^*y5l~#E<2kiYb&Cb{VbB4P}+rV?PCGZw|PX zvx&4#qe^!>sgEb@<|`Z{8~3q{efD27`Ky#tb!d|;eOKk$u$O*_odOJm%AT0&AMqso zg$7-Ccge*5GM?P<{YQ99Wu=X$kyxeU*0we_2ar~HL{f!76*6E+tt{H-2JI~1TUeC2 z?9{TFlI8OvWaR-SsQw@$mzMJW1PiuJyVvX>Z7W0Z%o`b%0=+%D==5sTB3kNh+Pfr= z`0ps3p>{&EUzjjU-EFFSSe@XZZ_QI_`>lW0{B3p$>r8VnKL!@mdpgow{$5=9ZWjW+ zA9^EmR=B4>t`7HNM27aT0`Bkp)OQ?yuJf=k*U-QSTQ#`YqZZD~4}kw%uJ45ibpW2` zB|IuobBq~;r+17P`rFz~a2@DIpGaQJDbuE`xK=wk3^W5X@l38q^KZS*(z4CvJ$>Hc zQt3o3r}pOPneX1J7?6Sy_bZ|D_xhR~7*T(ImdPE%ZBpiulEGt&MI06gClmwFqq6vr z_(L)>DdmF}>q`%cX(X)=0t_g3(HE{6$AZ+9&@1-aZtXAIg z)_8>>?k^Mb9KQyu&(P*hgmF0{dWe|;4W}=Fc4K+ z+qk~scCKnsea56`k6m8V0S`&>bymy-gEIJ7)VV|Cl2O{kxiL~bZW?JIpp=Cj+-Bjf zw8hoebg{S={KXSZo{J8>i@WKuGHRAA@W-r0gfu7h++oe^$*y2!BbrODNg#EI_i1$0 z#^F+1D_4bShhJFE9%UEzF*c0hysJZdmH&Vam89$_SuBWQYR?W56oNSBZ4S5wZGz!cw{C<6sSy(Md0G|+E7pxDR)%ekg zd%{yhsb&2S@>e8T-wa>=3V9)Jaq@9qnAKVTS)MADqHY8Ew^}pWZyS zmX|A%mP&a#TGTYUv*ZbXxU3yZ-k?LSqg-W;|Ap9GnJOunnPe|? zO?7=YI_8wtV*fpqFsAJ9%ck2dW#V3Vh8t5hF zXaMyb&s>tXrWP&0IAWSxYM*{aBSvx2#&x7TL?Yl?2P)k?OO4F7$0qrRPY zD1F^^gnIvVkI4XgyHZ%Y;xVqe9eFxt_h@JL0JA^ya6<`+E-Bn!+*+s!g8-51gtH(% z1`r*79ojC<{1c<1$}cDG&)bGhQ2bE65G24?SIjVby;wZbQ06al)^ksavmg6mU#FoM zUMN5Z1iBE*xxnwb{u?@coq-euQ~n27_dn6Jf55^21FY|V56=BRsOA5J9sfTl;s5TJ z|A!zX2;}zeUl%AC-uXY_(*MFc|1ZO*|5=?DKY{yCE8)l>FIvh@&c9q8rk&7A_<#MS z=#47Ffz>xyhvaAX2`wR%aCuwL2d8tQv$sRZCgFE~>0uX-Uo5UMrJt>Wf@oQ}IH%aT z__>a_I;Fj#Y^YeWS=L!)X(+?R+VPO)g@gs?y`a`UYnwo{bl2Q}y;1P$jg6gj>wTAbDgJL{ z_@5E}|KmdW?;Iuno-hAh$-hzL|1A0USp7%If9Lu6_fY!9O7N z|J|-$F|UN1p$V_Y-up!QM_6^LYnt0}!uw*?O<> zS{|ND6eKwaCW_aN1A7Ml_=oIzb{^}6DEol8A??}T;Bl5i23!?y!UE zm2nIXe&DAm0C!Tt(+vF-dm&6dl9|zS7i3c|nDvw=@JY}5!H9DG6e9ob0W~Omk2_1_ zLBBX0s}aG4q~OtVG&9{XSH~H*f5;p(xm%4Q%dhj&YH$!e+iA#eRC)ENcgr4OR@GMM z{aiNq^2!xt;u?lVMfdP~V58Qmq6U=aRlU|&E@yW?vUAeO$zOESjO7oRsTS*3dP;Lo zj22z?xPpAPc`en^*eokeBnpFC`s}%n=P7i#*dO)p|MN*lKp5W4+NSH49gR&G<=a2< zmw3GqW}P6lLf?voElFi(K_%|(K4~g_VObgz&1n$;WPoIx_0Trqi=Vs@MFdE(NC~|#JIObj`*McWR z!KMeT;MXAwq*ngt==kU;c@KuUrP66Cl`&}(Sy05jo%%P<53m+8CWm0Q002$t9;T-I zwjARpt$AHX8sksuFrWNhA?aiB;g)&M`)Z)TMzIA}FEUnsy#GEXgH>-pjQm?a@5NJU zg?xUq)3dv*CRc#8b@)>9ceEKfm)Np1!BFn067S{UOk*#)06aU58>f9}KSr;grxYW9 zAxIuZKS{O%{8+!TCrHjhSr=R$4U)tQc`jW#0u{3-B3;jFRGxjy z8b>NEC9Ti=pt8N!0oc3}-=Kx3WW&S=T_H;$)J<@ihBq69JYndp`-^nH#l?c+^`Tbr?H%-s;XJf^ucGC@l2Gx zkf%7ELdv$852GO+qAb=3Bg9VkF!2Z7wMmmph_k;OOy7(RMfZGpw8rE6w zFzLabu@5b$7rBX(V3`z<(DGT4v6!~gl+7H+@8{a8_0`$W2`|JLU2Ofv*}Bh($Jv83 zKZX(~nkk_7L!|m%j~rk#S5+xpdC`NJ^o7U!&;azce2sa@dZn|?B{N#Ihcuo|w8+G$ zuO;%CoS(9>Www2RDGmx~FyB@e?Cbm@&_NSm8C=usJouMa=|Pt?#Fjo>V}z@!Sx3a; za@qQVVf0S2R!18T-=g*V74h>3etC(#(0H(&TO1BiAa~=00^=Qcm~E+XmV6}W^1;*g z7*)FeLAY_vy1DJ?%g;%3@KmCJdD_@|GqlLK#Qyda!{OJ5`cNrD%9<~eS09s{gyMq3 z$+H;>l0JF;nvmIQkj)onpt}-y?my>93~;Ty9a<2P?$y1y^>-IyXf~hhmc|#fAd~*` z<5|t{+tx%?^5>c|86oeopk7N+tt;peg!U@JrpV58iujrzmDfTD02M7}TNMA`(?jR{ilY({k1rH1i(9BaKV!MtE7l?=$oMF_&jc*(II`uQ45-<(a}DgF1eTrd1gKC_nrq&AiTUS(I;g z@2*}-ev+fFWV!~3T2N<`-IXSJ8;`ZFuit$xq|B`G`WWg*ySr&Bx;bIx_vzpW6b=C7 zg2jf}iI6Wbjvs7;AQ^q4Gw1&&p{S_oz_Dtt6I&XD$Xml}#<& zKn=E8!EBpy%~XPO4(sq2u8NWy=2>)r+MCO$Z(U{VCAv0qw}Ky7|KVQ#m&FbSYn2ID zEV8D*caq700~D)`^*-DjfGz*!{DBGHEh7_LOKnHUDcnmSuDn4Ca>c zLrejvAuRPvCVnPIhX2(ImoLp&|$($=X$zM#vew${9oZ`lWQs1 ziVZnLX@Vs-e1H%HS)B3!_sDC#|D}rdY)(s?;w)1Gm>$I)_3z%tBpGAk|Fs69&^GKY z90t#^pqcE12rnMW*#jLMg#YMA1YJ8gDs<&XFG&wtbna!-?2^o_^*`TuH7)_M-~&n4 zs58c=$C-fXAV#Zqchub`} zOZiu39s_Tk*9NHLQEj?AUO)5D>kx7lzmUgdg!$^J1!d1sM6vx*O$WEv5)KZJUMy}= z|9E}X%q@L~s%-t)NRf5+Enxlg7byy-#0E+Tq)1G0L=-##dD1+rLu$M<{z;hQ{wnn5 z*8SyY@X9y7NmqKizwJdRK>@>t-tb;_bH-a|)2D+BMC4tURoYr8Z*|&B>>UTL;MY>f zV6sG~^K&OtijJH}WCE`%FRISRXF^SNtKnu+U5ZCv;(uMKKG)FiI30yqcPTD*Z5!Cv z@!xh=nLW=M3Vs$bQExu}KyKV&d;Lvsi)vn{Zy=+;qnLluK0J;H~B~kp5WkF&wAc*K+X3xG^uJ zCxWswI;#l2ul_`TX+@khTowxUnIA{a^1OAHE{9HfeV=S*1^R*YvmEUer%!TLwZ_VW zraH15L%!7wZm2%|1yiI(FlzR8f_e^vGW^9vV;-;jr9m)JoE0Ve{cB@iQ!V|D289$W zHn}&Z4d05d%>K4z`e0@Dgma<3)(H=`IH7%}ih;mRqkdL{OV)6uaP!|NJ6svYX7(vz zoiu%?x@CWvS0oI{;JL1sG>8%lnHyZeZIov*y0dYG61PNnZNBMpu1~^pIgiM$%n8%j z=44*SClg6{zs$reL9gvQ?WuRLT%muYkl^f8?^;bJI z*%`f72u7`4JYoQ;3g)fWsw|Jt2e-Y3jnlo2r`aBS1n6~wCXGMlF#yvy89n;s`}URn zD_u;SnwCMnXMfoH1P$S}FN|w&X1A6$2?qHL7d$LWzY|?ot>JC^9|)n*t?$cs6F7Gc z@Ehp{0^99mZ64BTO+=hXTz6FG+-OfK7;&7Meyn{>@FK_HMqmUqlx=tp(-sa?9wMi^ zrR;w=`70kqi#v1XlO!aXkuX4b#Kt6R4B~(s^q)T{5-xbPno4jcjUbvo+i~h!Ux-!^6 zYVROiEoH=B^P?6{+|XkhCi<|SX)dK|L(#7vPH8$&?Ih5peZK14!BY84_BmGyr0bno zTR0uS>gxLNX4BQKerP9H9aLgQAL*B9`t=dL(`~xQW-TEV@pJ=wdc<^TAiF%)nG2{W&FdqJum^a~83|@R5 zf43)y{vnw5^^EC%mHhMgB1hm(iFlbQO~ExhL0HTr0}DT3fnzk__Z)2TjT15zJjx`DgtqsFQSZ#n@j&_ zM|(Xcpog{ygVk+W0a)>&OdLyMbMlhc-V6-5D{CLGq*WCE?I?o@n^Xl==#I&T_}ZN*o{hzpnvK;O)_uE!viyR6AQ@dRDY zNii%oMV4Nu@UANO0CA?YtvOJ7)z9>EuUwmITi=h03f2)iSZouRm7z8bD>b;$r&B3# zrErx!^1?hF+i8|A#2i$mPAvs^PgsbcpN^%Q&&_rTi*{R`xQEt#Qur=J1*Du(iqx~? z1X7|`Prf>Uv881oMXL8~>RWFnq|Nh9oB4g!3#HclhP7fgnc^wT^&RpWEK?*uQBC=N zMoom>z;!po9;Aw_{>b10>wpg-kS_xTY7&WD~pl25r@_8+W|)pjG7O-S%5Nw7KN}B$wZsDroHIuZy#@;&f{#~ zD_%?OCVc^qZQ#%}eVz7OV(cK%r6_m_(HkDXCVvUl?}%;qd0joT^!3?xa zvL{R^sx%^*0&bl6^?SP|;}ecM5FfRu3_J`I@eXp*seTFO1`=?t{JCyZ_y(d$6eBMM zTf&GA38OT(a>x+?vLNHSbqrib<`f|dD-c7|hk!D}u_{(MlDky)W@ANa2L0_Zq#%+r z;{^iC4+VWne7U2VFDXGhLU$cIkFBPUBV&f7e)= z$;dlJfOR#S|v@d`u!`OU~Y1R{NU^u zo9M^qe#&97Jv{pas&D4cfld!cwmTCKGh2nxhFV$;{aZ)99|pySxMW~i$!wu5CD}|t zY-_3~aHPVsYA|{pvzpmS921p064AN9uUT5Q*YYQW=F1beD>cbKlkmeB%tZmFclTtL z;tG`L6J!YLyi56P=xBm267C1~!B%iEdGt7q9}FuqtD%cFo2G(N%y1Yo+Q~SXglQ1r zQ;lOdX|}XYYHSdFzYKFS{$?jlwU#@BRp=|OoeCr2Ta#u7#FPw<;~P=k8BVLe zTWGnLMF`zf94Wsu84g_BcaCvb`4G^qADP%-qNA7lgrq#uueVKb$0cpZ;^=g^CS{#8 z=5uU3ivT+0SvCgg=FYdEsZSf1u`%FN<38hj93jBn{;bwsl9+IiTAg|6K&sK4Ev~>g zlviZTqrCOcT!PJ{5pKlqrUG+%6(z<`qsf7v_d+wB;LnEziJ#mUUy%8~n=B~QVJi=! zD8M=K1cO%l1LA>b4n|$JE6I6UbE12A7!dr_V-uL85C>kw~hvzw;XG2&qG?i=|U*I)f=HyK-FOOMFBM|%&{b^)1gm@7RIjJh*mI5$b+q;J! zm*4{^>nG?+?Ds69%j0t!OE(hdGwM9awe{_paNpWCXmRUs0WqlGn(XOLou}ieFXVL} z#lm50Q+c02(X5n|{9su~y*`+~*0a?~#sXnSk)^EcjuC!RL`zt`^L>#Tx`qopiCb{m z^JT^zdKGR6V!&&fW_r=OVA^-lC%mXmJ)J!QcOfsK+vP=31;TI_3R4hnQ;Y619{jn0 z@1locF2ti#tnXoX**AXBW#pF+poa#KgG`QAw3u^^r>+;vyBDiYiX9B0-cco(LMN#O zY(x*ZE&sU`7`6o`cI|~xPRM)!*lRIsJh2vCgvTVJovJ8>vs+Umf7oo+fH^aFn^T9n zueXJwWz%^CHU7#!8W+P(P-AG7KC`;w)M=%&_#f-|_}}Ee3)PXN{L%@cL#K^fYrdfy zP7wI3BiuGJ-X^y2R89oU>Gk4YGD*+)d`Yy4zKrBhXp%(0{QMr z$aYdu{4yk)j-CtN{SqzS0GWL<%mgAGPf#}E!U0GVB^rm(AfNh7DmF1Q%rM-~&=%Ao63H;FY;oA|q3LoWckPAr)fn%l8n)QuyP+2_SCt}|?%odt zj9|G8g=pPbG@M>7Um_U&vSgLU(&|U0vv@UZMtm#oeD_@}aaaEblLH?!M6JWF>;6lT zVoR8383t9uFXWG1x*JEjEAM^K%{a&k(6GCjw2=56N{cV!C@?BZ?~)qw5=-i%~~~-18fVQm;O%gz;Ve+pjK$9Cfku(riBrPtR3vTJl1_W`|tU@^<3W(Oj11R?aLD%i7*a|vPBCrTx zi^nSHrc7Aea2mvWFjM!H&sFz1{8E4fI-j}QGJ&TTG0d>$Uy^CutNf%IICdYlK#Up5 zFbi(DE;|q%*s9jYZ2Btzp@mai2dG!M+Wp1bTV2}{;__FLu8_+{Y*Ghnt-(5w1=st zOvBQ)Zz2A+a+G^@v*(y(GJv`#|4u)8IQ$Lvf^7jxl{cLgUPi~*B{zh>)755F*xntD z064iUO0?sYXX{|PfC zrCbPm&?@4J(TB?|(Z7PzYu~&Npg5IcdE)PB#mMZsgf52l4FF5EG+vtTmF+0B2w8zJ zLUvcE>xsmPqwtL(98h5J_B2%dtn(SEB2A!M6GzHEl^}ZPL3ioI4i?=qyoJh6BC_i` zXGd!UP-?)qwb`p%KtKCzn#*Qg-bAt-%+(T&Hvu7~__2|Obnfr>!w%F%Cur@zAhNQ~ z2 zI=S@~Ydy``v7TUxqmmVn4~Mu-^RcmY{oMY`>1E}qFre9y!RK>JR7=urizULykcgWP zPKXfiu5k66TxpW%UPSNU+cC;g$4~)h5z=FZkWf=1ylBNgW1IJ?G{jK7*9gfloaLE6 zqRTXB@-{e7(;eb1%D=gLK|{5y$Y!6qT8|LcTeQWJ0Jjp9yT>6Jtu&79>NJs?1Sx^1 zb`rJ3;`qFU%+g{*RbB0Q{)S1%UgE9}!y;r)h}8$M!`+b)fNjWgV{_s+_Iih91g1U%1yg8XSn}DprIS+`km|pc{e) zc*bAgA+4mLt|!>4u;954ZAC!ZJdRxSnEKq!$;%0ru#PsqjJlE>Icm_r%Eg-hYBin0 zQ2p&Dx|4mLyNK?7^##Nbp9^lw90h8kUHnf=<&ttzr8lD4KK>3AS&4Q5$PHs%qk7bw zh)!({;(OL&EnRkOR)-T*+cN5uAFM8JwIH}OPKss0*1_}qTORfxV-Mt?-r2XA$FEZg zhYK(`>oH~lIfqR8h4QXP=%n>v1}%}SP}BD`kVaLALRhch?XN@a$G%5uOq}$%tPPhA z2frBZ!K=kx1`j-K{I6~%lZ%p^g_ge~S-R8YxfJTYJ5;%hp%Nvg4Pr~4jCj2wN$ld;eO77TT?fYGg{Sw;~ z9I{HPjY;6GV9X5pEB+ItRF_a1N=}en_(yZzDo8m z#^6WQkpJ&-Wq{)&S!|kgJjMLMbrtxQ6HRKQN|Pp+FJ&0p$$}j>{Zf91V-0LbO^J|| zMi9L)6^dn%?47c0T(n8YPMn!yNJs~)L9kh@k1nGJL>|P_1*+9#s(-OIJnKOB? ziRgd`lQq)VWx8yw8U`^9Zu_gc= zICEhx|ZyVg5BGbC`wXK3T@ru@3krviZFN7f%}q->Y?M&AEv3Y7U0 zlA`uK&Ul2WG*Cq~ek!(fp)_i(GHW9LBjzGCGEcTcdS*3e2MTnArE$%peBUiDRr4X{ z1O6A4nVLwe**qqjoJ$*ky(2lJ?3#xXM~2RaQJ~0!&K9)d2q1wUa>;)zn>c1$oj>I^(zyJ&6s$VgV*M1%~FqUG>r} z&pH}U`9~dYY1qPkC+ez9nc4DP52xelXm1=4sgII$)#j99B0LHcG(K+IO4CLxI6fxH zV(0K9FQ6&L2D9@RJ2kllbd=Q;V99Qks8oR0k4=(>i{2EcDtdPkHMxmvk#-HT0YZd+ zB%%6}`E&O(&5ND=)Nk|kOEG*eV!C$Rj&AQjQ7y^J&Q@8=?A`7HWBST0u*jETfIQ8G znxM6|29h>kFW*QiC|u5y`2YZWS!0{d5R$`I_{#;TS?xj(k=m~|Z3k?} zY)zsLn}qGJw4Kb@2Lnf$1BIIx#F)P5@QOU`N=%JuB|SXnDUHb4gt?xZB&Ew#NeWIv zQ8g`|J`f1v$cZp8Jg-0~A_R_yttF0L_{OWOPl{>EW|qXMyU3-RS&Sa+qoL2$2^}}2 zF_PTUY`DdGmQ4qYKSTgufmmTZ*l9^T()9Gj4SVR)rca3jy75eqnER$GGKoGOBz(`; zqG!>+E9#%{<)*b+00) zG-8Fda$-zKp!>3P%{<~7#+_5Z1B8g=t!*dbyeagV0m5Hsd)+3`p4b&`!)sWMA2ENr zq+jHcC)iCL(@I*faz`GqC+0kb4-PTY`;HH)J8e94gm^ixQPK2@IeX%H!Vz39d_HC4 zN-ZE}3l+zNSlKxmI|CT0GoP{FODoFK!(s40zp_=2*j| zuz1vP#{!rEs|G!bU>Kh+zlg3enH#6)oVJ5I}Bx?xCF~RA!Of#`4sBdG! zpfENk|5r5zKy;bOMq65$jN+AyE{vdv=+HMXwjuJp(Yv^v5qP&CQ&s$%xIa`~8!|ts zXZ_R4eu{lBkAqA^+xx+*$2pB1@5$dKxXze5T?^swT*GVJ`{dp0sKR=V6)LWvxP){Y zx9l^_zB7f}c#Bd<0O}t{}Sl@w!RYDK0dd(MTP1zmz&TPG4{ZqSxJ*EDUR|c6|Z2 zyOez%#*6Oy?2Td~Ye#i;TkD5er)+G~%CL)bOqlI@^^CWf-B`Z*I6kK5LvSf9y=0Xl z>Cyi^f_BHkb0Mu%S%%FHN)QOek2V+yigQHF{qe{{(OoTjZvypm?SfAK@yQ z)_*ksG34|7;>p1uF=2wywfhw?={Z=rQa{^P8=#3+TDv@6?>~N zAYx5<O&`{P>yua1{HEGq-|EUhXWF2y<#{64 z&rT(!W7a7W!OeTa@ERk7rZvZOG6EVtmU<@A89A5=dhr0! zngX=t>|-LQO&J%v5dg>*^+CUFqnoDni(-&oA0U`gIF3uSxR+NuPCeX@c9gRxi6_b; zi3adhZcT`yzo>9!jwFql&pTv<)_>~s;!o)*^@~4s;T&mxmxoALz<`=BIXxCbq zv>g5{LKu4vC^5naNceH#@yp=5M46%nqb`UK6vir!Bf`Vl9SM%@cV{~foHv&#ytC4c zN79g9Wc)-$wuMnc+Q%9(PZt9ZUhW#J0}mG?g3`1vK@e{|=0@EnPFpy8z1fK&4aH72 z*vj4#FUs1BIo_77FoI!S%OJIDMFaS{ZmtHjoV$4ToqBv&nceB^m!CKX!E-L+sRJj{)wqYo_1dtYPxFStU95<*{zunA&lBa9%vmG*dp&0Kn2yFJ zUyKk!it3$b3XPkHFR+Z);`|pH*HDs1Bv2MlYs9Fbs9_7CcuF-gysOeeT6={Nd1EH2 zRe8}19MrG%u)!6i9?CC*PP$y~nm+LTc{x}&w)(T`_^PW2g~f4pwd6D?xSwOS&3uNx z`%&S!uN$-A*#$eNv{-XWN0xOZ@06@{N+J-lET~AFv1<$K(w=+F2RDI#= z7^ByEuXtM42d# z)bUquXXnENe;Bv|N_7>}2XctD+G}W&zuqnlz`sup8^+#9YAAXdeOT%+Y~`(9>s*x& zC&7)ATakKvhZKUtaNQ7q4NNBE%VO~HD)%b@+r(Bs)yHI@Lvfa%SDUAiOYS#h+?fOc zj$hTC|8;N+!zoV3^5K3w`%abc6K5kv#v7$RkNTv0q&88GUhAZk7B@oA=x~l@7!`ou z$zrS(9f>$fbWIaK@XkMJDS0v}%pTwfr7|Hsb_%A{cnTFbC>E~W^tl9A#Nj}_SyYP(^X%Tt=V%H0zI zD%acvF5L?m&%VfmB(*^8b`j}i)3`Z!;iI{dBcCLl#{*)}P^F*6a-514f^f>8%+elf z4CFkh_WwRlPh`dH$HToHrGp*mht!!ob7jTzQ}Yj16Q!On5TyZ)5uBe72>4!y;F+K+ z$=yATeHbyej)3LqysKnw*S>soC?>h)eBKk1ypo6(l-(B*lt-Dq*Il`JjKANeikS;8 zeJmNkrYv7V3IuonIIDD8AMIj<-Fj@0G%&zm*zh;mD!!p63)YGjD&xS-BJZn`>q>)3 zqo70YeyR(Eg6W_0A9GGzS4zn)Yg@!)%5kF;?~dll=*W7gbd0-ai9V)=r^jadu>rYg zV&%w&u8r!cyT%hEsb-#3_>2pI2C&KoYx_D*7TcZ&GlyU6yz}7HjqS-J^WK2JlhnOK z|A@z|QH)wvMtu{O^Nyu%fLKPWQoYXnG#0DQ{0OV=%REM!o<$Np!#QOB{jbYs0A8)M z@)*fg$J~Rv8^xeGkZZY8#NSaFMGK`CGO){SlRP6#5u5==T|6v4eH6#V4AA25exwrZ7prq0J*f(<$Hc8hPo1#? zM_g&KPkH@Haw1BHj=5-yBIK0*s-;XU;bi^+;lUH{^tPVN;~YN@Y8Os?M&9bG5>G{k z10)jSESAVSB4Z<~m=R#7VGCo~)m=lmNdWWe0X;_=!JDQkBN$}~CgC~NSWOAYE<`WU zwtt`UB(9@gkBwos)>mr+F0>Wkc#V3NVVWnQ+rP5G@^^6E z88b`Y-_5UIz#Se5XNMijkUbU2ygks&Yn!GkGXwI93C_^Ni(7c`RJ$Ng4}BP&E{T=V zkL)jdXY*T7%*iX`azIf0goPnc10g|n9S3*m%l9?2^f0@J(HS|9E{Je!VHv`lu=Tg3 z9`vX4f#)6Nk%Rf;zupL}=iJ<7c$3MWQ}3!vmJn($t{Z1vo#}Rh0Lm8E<^9R703a?` z0?RWhne}xNa~wB-x2(wYh}7oncVGQ7Nix`WeoyxWXM(druO!xhwf6%z11cHRa#AVB zMrn|kf*JbM!Z5s&vA~LI4AX5>o_Akq{5mZ06dCccwowp=tkYpdsa_n!F8faQZ>~Bv z#GKHo+Y;l(o4$l$g*TB)8A5B+_fDzG{2Jil-&XMF4}453#*!=b_?vAMPV^KPOqd5o z&h^dVbdU5AqALdCL~S$g%u#1zY`4OQVM`5OybX7~_XH zy9vv0|MFQzLWM&j?s+M;Xp8dem$Cp)Szph12E7e&Bq6?f>d;emy&GAv$2)lIZ+Y;I_LWmbya)_xt>_TKPv$IXyq><;X-ZzL^or#BZ7Shi3B)ZMaDYC@vK0Qd$?&FEXsp+W+^?3=hR(P>4r);6ZwuYoU~!Zy zuZ#AzCPlhRz=*6y28-@Nyev(7d=cG_^*69Mx)XV`XeDf8B-=ovCVqdtqS;%3V~7;F zVa@-mF^^Zffyxwd3J-}IyVpj0@rXt5-~@0wmLIfLW%i|#X6@TZ=Uk>9iERoT^4kE7 zC!6o8BT;t0ozxL1(G zxH);n5W*PryR6$r>}T78^&W5Vmf}PZuofKO%y_SU0n6lJI~*bA9cdG^6l~AQpgz3# z%(AkXf~@2QqMQvr{gy`qsEL^f-G0ln1&~3=)Z*9%;Ok&Zj=FNH!VQu2v8>8kb6Jw5 z%!X4Zi|lGm{P-E*u#u4i#u;u+dxgh$$I&npB0wDec~hPlxY&3?qQh8lvx<+XJnxJ; z0WS^{qSIe=5G0sSXu(lYTH6)VQtrP6P=dtDV&@BsGLLl#h1GHcL$0^@*!(>6JATLg z;0Xhg_V_-9s4o_KhkJ3zB11Y|^Pi5hh>4U&nD}XtdyteOoX2eacpE*rcoqT(QiD^$ zH70V1oIV=rEc%y&Y&+q#l+EG}zoc>P@!(PPXwe7513yTdl03psomVTzp;v==6S(uk z*oCZYDf&(Yx|v1>Ml#1ZB$i0d8%zE&l=$y7A-B7d?*n!~cAim~wU9)HOkU?_ON6_& zEIf(9DsLmMM7wP`N2~n4pQkEiLLgQxU^0oSMIH-uc`?_hFkhJ4H^yY z@g0$Hmfk?dS10hAKv~~2^P#HrbgS5~0U%hDs`Vl-mDf}31Ac%Q$J%DwUY!|tyo4ui z=O@hw8uUx$~}(JV59| ziXDwl!;s@MRaYzoDn2ir3;F#*S2+vC>3}-S=Yr$!( zCM|`O8rrn2;7o{F50(8f!P*xvvNzk2qdSfYTThsZ4q_+eNDcor)yI*0@Y4LbfK9M> znI`uRD5iBuf8u;2U!fCMlN$J*mMqX7yo!+1L^YWtN3qF?9)1WGfjd6I@=6@b0;?L% zvR@$4?Kl-RReqeflBwzCQ-T>KpA%agr`&43qWc~Ho>Y{|RBbescwI|xQI%VkqqAzX z9(*!+*6_p=j|(-J&ObZ|76dAr+-du#zD?XK{;7g+Y!ZL>7lP+%Nr$=%<8hg8x4E|(y@bZzm!-^Nu?ARdPp+@fm%9wr9 zOC>&m7VMy!rY@7Iw@zppa{-4OQHdJ$qdR3B9isur{#$%-VgF4O|{nyP0J@4P*+#mxWHX+d7Wr#!($CY ziP*!_G2{vD@jNA{*(!7?hl<3v;MGV>GfxmvOc+>LhcOr5cT;ZLQ`WCV@f&T#8c=Wy zFRR;T^)S7j@!}qMpw^qDJW|3;Hof&*nuUFPb%%`QJuC0~vb=r0QjinK&+4Q)EBCRD z|BNI?UX6=eMw#eP1!cgzq4S!j_SeFvOSX^joJa%}OV%Ozg1cn%3sQ0DcAR%nYg$?m z$LSK}_7e5R{c!wx^rwnUVHCwcXH9Aa{~$|ndH(GQ(E)F3%>LWW+g0n11r{W%{gvt4 z&KO?`t-DHSK6`hDoaI_}-{0yet3zexv%0J!NTv6B!_Dt;7NDozjzXPOKX$snQP!%x zV)wwKXI3G}CP4Z-d74ea$!>lBOBa;ud6$9vw*ZE)5D+cjvV)gL&sjjb5LgcCZ_htY zpifl%0-MN=X1ky>fQQg|mBh=l#}+;&YSZ0$+r~Qzl$qx+yPAs@)1#PPuIKa`FAbkm z@Qpinxm;3W>e)kvUmY$T!vqBvxwfz9k6|WE6i!Nv>@Z3%N~BNnpjcH!C2ArqBsMkm zsPFY<%bFd%NVU^_L5*ZI`)^}1VzcNCOpxQ;odPhpSM%3UNz$}y^z ztd7E*`dD~j>{digOl6?HVfQ~+au9!~KuoRa%u0EzwA}BqVk2BPVhCg+@u)t5xT9}) zFh^Cxk-d*897UOcC>w@xU(;V&AR6^CB&VGKpO*l!l#6`w{4^2;2|-Ky&lpi{Ou-5{uG=6`6^LeLhhU_qk** z3wsRfFl<#hKgVmgZiI)f>{`p^%{%&Y#m)41Q(_PD>EH$5!_WO(B-QoCw{S5T*ugD4 zz&lL*OW~k_w|!$6e?BqUiXT2&Ch1W!d$&v@(TiCT)xTSm{pN?(mIHQJ<+LWZ69XLE z3mCx!b{uW|dpB#h%?B?TsB*wG%$d`mk@${BaPFX9#EcaZ)(7LmmS7NQ6`u3KUNg*V z-#t!nn}Wf)^IPmuAx9ye?g~2sYkwq(9SMw_K3lYiR=htt!C?AR9`}!%05#AO{mmtVA<`Hu6 zEm}5@du4JbxFlZuo@L!Cmw(*<-LitFtwtg(^Go@(L1Gw5a>J;+(k%fk*1JoQ#5+W( zFh>;;USaqj7P9lA(0uHw+%yrZf9k29%H)7)D)&(+AJl0KD)@HA{`vwIa%pflOJyf7 zzkDAOmmhRJ`eiql^fVbln3oHt<*e(pWFYl$x86rmcE}BxQKe#PeYZ=!oCOOsF7FRy z5APQc62vD^@G*u{KDkEApOsjd+X^{hHnw|-C6C|-Mir6^X0#TSm4lwMDh>gs-6od? z{Dn%^o+_Z~LFa+0@i18m;P&-V%e#ra8vLpC+~U4ND&H7A3QL*Vi)Tv}_t#K}(l574 zj~_m;>uiJNbsd}~RY>m~j-E3dI$a6!L%#E8W(l4dt?f4p{>%oV6eL?Y` z{luv?>nl}1Ub^6%gz@Y=FONgLC6*)Bb((B$22~b#qdZd!XobQvc~J?XL2saQ~cRI5+2R#>M*XM2YcI{etkfNQgIw7V%}d3{{+HD+N4MCVi} z(;uF0qEIr`#I$L*0|6+Lt&Ydi1u@BmR2B&u; zjSyHP+XnW=l$LH-cOhHfu5P}s_N>hntxCC)a-X{F0Q@GU$80p zNZ&Xx-sP#7vuGk`RndpOH?I~)NMKMpfLlI(s~s}|z*pMX)+Y{qx2XOI99x?2v->A| zLQsIE*LeDvt85tJ6+%8%nS>md=9G8PGuIhE-7|L%f2f%Ok8+T9apO|Alh&~RWK!I6^y0f{w<>R z6z3p3L%K90KjD%Xig&s2G`|6K=za&Lif-^#ipE!^;E$Ibp0Q(e#Bj$`cbr>wKPTuA ziXykD1a43+#J#)~*(+ng+>dgD--%@F+wNG$Pp!Re_ulh;F6{UUhdJwHhyzQZj zNC~3W90mP$&XU*D0`Ntovn6%g(bs4Yb)`@&vA8Cl!v^a0osucEW^OBQ8`bhxIX%j8 zh@3ZY;*0}X$uR^xjjs}HJPiwD7xYH9emkt{*Plv4Yb8)Lib4$=yk$6(e(Sqj(&nG( zmV6aLXw(F4vdV_ihwOc|qm=wkjBB`R;c)K?l^mM*_7S5k$4h0%cs{7=Sx4|oaM^eA zst^z1tI|6*kO;v#NAZ=9YLbU;0uVM+lg*97X@k)7p{vmoEJ0_O@m8n9o{@u#)zYFx zC%sP>{=nahA_oZNL_T*Bj#u7; zDA4C;k9A>Xp2h`i0W0r3v&)|x%iO{<^n`L?0-)>}R$Kb5n!QFhJ~AlZPf>~3%PZjH zhMc(E6HVJT;nUc6USF$iec(7eux|Ha$}!;L`s5F!2iQo2qWP@Bq(v3a(psABg1Rb0 zTs(B3F|c2>iHNpti(K`029SZA>4kD-yv<{%M$Lk~9K;`ew0h*?LPA#y)@?`ZjfeYVXb^F@{SEW?FvQM;IwK#lLAz|(+Q)gwUgDvZT5K69bw@?C} zUc1iZ?KNwk@&%#M2*v~;-1V9&o9gt3 z6OpsMKRMEF`c-a_>@n&|q9e&~G?i(B@EBfjd~vTboWFU4eOzok;#)3|6kOIt_H~uo zX2j*4dL|6f%Q#T%awq#Z!(PQz-vaDFf_vy49@5Hp6v;_Njy0e$BXqGhyCJD=V(X^F5~g@oBr+&U^|tNi z90zAJR0-Ew=&I0M>IAX;nWZUw8DjS`3-?MA>=Vq#97_9{IOIC90YTgR#h~)fou)J( zmucBq*SJY=kmKFB3rYfep?V__Cby^!Stv&)dUk~kx9D!KlycyGoYgnhlZw}Jb84BZ z0TfK$U;TD8CvbmC3T0>Gnw`A3+bn&Am{2&^z$;~U1|eT_6|Y_^GW&Q!hRYW;(Bs4% zs*|l`*1tSn9YmU+ytC&mDoTnR1LHsMLafQ;?Y14@ZCqsg0uw7gse+R$0@+iW;l3x( zPDhRuveP=A;GW)O!N!qpKbP1}?`w6)PJI{Nj`u4XW)qW$CGkXkzGmXp0bEtQW=t>! zNoF8TmTSS@JBDm0amdz2HtmAE)Z~>68fd)UwE3lgiM!UM{t<5pl&oD7{b{8Se5%kT zU!ZnoIowEF_(B%&5VQQOktfJ^Iyj@j7$slHzAVsdc%T^>N(?{pn>74M;xG~0TIDlE zxT%QuG&@+ue*9gY{lLOE?#>(CCw#(Mn!MN8NDCvEzz19N%KN`xpO+FHjmPCs)P`V7RmZ8w&Um*doGxz`R43`5B`U-`h<69}YEVu+>G7NP(CDW3W8 zV*3{GjZB@2iSbJw%?LP_j-Tgk1z4#E2<&2wiIf(u*F}v1&kEa7QbE`@d>&-%tpPjr zDwWGhNNVT;pVmR25XxoSmRRfh)reG)L?k*-0qg|D%Z7XTf)nl}%@i`SADej!mn{&m zFiFAhQeg6pv#A>U9VP^ViHMvmfWol5l~;NLY(x84@$<(mcR9qYhH>O+p}tXhVrSP9w;sq7-%YD8b(Nzycno@OFn#H7a%dR6yY- zc?Uc%gySI3lO4H@ohgB9e@4dho6Jo){_W&~>*7m7kp)j}RdyR1wK$6qWrE%H^Bl?j zEuqmcCXHot#VkMsRaX$E>TL+WSwJuam}R*_;+SEXxWM^IyD!MkSrd0I3j+;M=3)Dbb&TEs;l6V@ddjr;^y zn9ds4(aw=H#bi0R=a-M?FioM%ZuV>kKKv=S$A91Z<}~>V7-3jdhNz=>p$GuJ9mgRQ}ilFRN~?jC8DgL^+#Qn-a4nUl-4$e$`|O( zd_8U?KY_>JT^8bc>b!HU!`Z$0zuDYHn7AMd-?9!9VNsp>3)}p-sz0v*h$jgt6dEom z-Y}qD$56z=$!huRQLy2w)3}lD7sm|_{S`9n-hk{%z}8pYZeFRrT(5LXjyn!*X%JDf z>F~oCzVjLuMH(cVu`O=n5hz%Mo|D_Ka}@NPMGEHukLOlu<%6Q>198dRm!4}6j}~nq z?ktJowoDE&AFVSxkhfaKz2?-~Hq9u>L6N8~ZYx6USRoM#ecDO`=w7qo?S^Oxr0r9A zH>ngiB;AO618^0XdUT)V+d|E2?VDb|oFIH|M++b+Fb*8YfwjoUcHJFSqc_*7m0;s8 zlbjVW`_qR5+i5EW>TBuM*2tsFK7IJ<5phCWM;ReUuFaT4CRc+c1_e3Zk!BcWn~Di^ zV9bCBipbL^vy1y2l+9W}-&COa065Y499l*hDF`~%I?#?)!lkdlv}~#W?TVyUTtPL# zf%Mv+0sJl>ky8{2*X7qJ=9S2?T6^pqcE4v07}~d zZ%{{nMR{#?*h;5C6gzx-?Fme0b6<`M9U|w8UJLUThedO^TThi8AqfU9vaHt-zbhPuBDV{D0rhsYfJ+&>r_?qV=z0~}3W-+JV;D2Pb za~i=9QFvXDyU1=ENW^hFQ)PxWfW(UUgXhmvwO`Siwb+Eij_yV;!TdpUk}cMQnOPVwHjkx*i{kLs#}luikl?dD4($0Lh7P|NA7bkpq0JveZnG;DBs#9d zsdAPH{N@aGWVv}hTd0bRx;^@Vc6d*3cM$^VEX@-^kRNJze=#iGE{>9BAX2_Ew1$l1 zY)2ThSq6EM%8WQ^tY_GTVwge~ml=&Vzv`-pB{&(Dy)a{mXFP&p;#yR_9nE2=2L z$@zp{D?-S+_QFR$Tt@SYwdaO!Bcj^4ckZ>O+{lM#)DS;CXJ5AM$~(3yb|hdu&qbu?W8~vm}ub_qYH$>YxmMT;Fux zTT0tRljziU<-x89O!PB0?6{H7W0k%Mx)nSj8e4G;@0sN=i(J}6<&ym&KVX*Hxz!AajzTFv*Z z{Cp{xZ=MFY>|!4MfER4Y12zMq7p5cOEzRr-hCdOf;G3pILv)>CMee;86GGC%(w+5; zq!6_w66OZ1N%K4yPxK(vy67878WA^xF3AHiaCY3gSdOMf>vH9!unG>+qkORK{)yKh z;~5HBb}DM;_sN1m)4L6 zxy>I=tfwOq&(v?*&m7E25;n$GSBWt3>1B;)ZA(da@=$MSXnm0#2{6t2UI$8|ZW-b^lGLSok zGrjp>@Y*YeI@iM=iu~EnBzTFTLi@nV{$_>=9rl7fD@#;vQd=S;9tm4zFv3w!V0*V> zkkwdA`$pgSeCizqL70X)PhBTT&>a;+7UTUF#z70oB1oi4w40Qa?Ua^^1r9rEjdm(I zgWK;xCF+*T($z#u5Z^RPtUI65T?+4@MYmJR+AuQ9rg{ny|DSF5n-tCW2YcF4$Bjlw zzG;k2RiDwgE;m=|;InoS1D;5a+Bf!HX~8=B-N3ij^W{dC+9S*IEx`=*ZEt^y5{ysZ zh7h+%A^yJc6U3_;|BRq(ulX}7@kD5TtZ^qTykL}LQmFIP@6CP>XI%iFp8B-8YP0aG zN#rtYXMzxp!}@*6gHcW}7LJ%zi-XarX;IYEOu9RET8i$;tuO?Jdu*#IY!TBFWHpu4 zYI(4*-RW2K{*hEXbFp*Gt%lmo#y7{m@5^D3Anry(mrHqKn^NtcQD!b*`?vU7dJ-&9 zhU@12gfa`~Sc(tlFKe5su5}LnygHl6cl~zLnXfcw+p1;n+m&v{D0R&qeP#2ifgl_t z|E>`0csEPUhr@Wjh%@H}30tgIny0j)6sj{WY;dPE^+Of3f2h*PTw`8ST`5F4q^I9Tc8`=Z&*bg#6^mYiF_ zzxx`#Qg{`z_BFo|f7e11O}(DyD=8pcLeXNKHxcV?02{iEyzo5 z3Aa7ulr#2i9j;_4Em>rfgQ;*@=pE5D2xPF#U|ttLbfRTk(syr@@qUz+S%&LeZ?n$* zLl?n`nOr{q8flxNGI*@b>Eqx+C8OMNUMXGc z_Hm8%Y1$CWJW<1%PK!0Cd}#LZu_=ben&10hFZ^n4eZLA=>)wCv&HxBWK(IF*<{*Z zeB-*E1@Ed&EN9F6qQj=+we_xNmWsTbt>ZuWdrFEuUshey_nF$QEAZ5`(zq8@jJ-;x z4S&6ulst+$A?ZsQ>aniZ8pZ{cmDy9+Y^H&E5 zI}L{kVM3qf>*|@f##3N{mv5=CQx)j_bclJPVKTqAjutVa4o8I7H`C{(@xu{MAJ`ZAytDjYnt^u#i z+Tq~4V492Q*luYh?&6`FR^vCW30kM#LUd7Pc86GJDDO@jj0&`1!^i zckWO3Bu%>;N!Qg;LELLK4qOa4P`@Kli)cw}$dgkP@e{PH2`}}nrrCBgo0|b_oUk`i z^#O%lYdsPt7zg;RY%b=6bg&jvBzEuUkICPfDi$Y+I6rmRw{#)tDwkI4=;XAiTPt$T zxp{aj9m;%`?N=9}O#Iyp#*TF59+xX*Xemr7fQ;uaMnHNAr&QRzTnys z$wLkaZo4?VnviGY;m1LIi2*Xi6IMz10`z4 zK0P27Vz6mK!W9=g8JpKO_Z@Mug%5kfP`}*ZrtP4c*1D|neNuBA=`drkv7K-h@p)8M z#U#{v8oU1h_Zp5mJCbeG%6vMe()~kBc)%B{cT*oj?GzT0h~5iJB1pgHW-qqCDCg1% zEVY*FwAgbMWhlw_NIt%EByO*8dSW!XoYRTjkF`XF?96$NY>&F1CkN@%N}EGl@oxE@ zZDb4meqEQMVx3D?4nOB4-_a0p9kG_EBgU<%^ylSy=qN&$Q-6sO1Cw9pHO?_;wqOd$ zE_&J~gERzbiBw7~*cY#;^D4OKJ*Ex2aDBYcgq$NOos+&bzXO2RNw~|A)gJ>GbK@r$yYx z>D&~j@7`HIA(gtZnhK_8;P5y7VJ?{l?qF0)E-z{r_}oD=TVGq$d%NM(X< z=y@fDdWa=ta+tchzr?eFU5$Aa1sA0wYiA4zbBm5>lw^6F_-drHw6sv=+msMh&~>4m zJr8*J>clK7{+DsQvbir-Ni3u9)pyAafDlSSF*i0&h8Nm1Y)jC!9??+|6NP(G)=rZzC7O z6{3p}d)scdfd3I6xStYNmKAjgO4hjC1E0k%5H_Y6HAwJI6J3(h3<__zF&FWzoW=~r zC6M37@VdMF4W>XR6KD*0Oka*fg%$ZOIS46CVvc*?lD^pCIj}Zcg*t|H8RPHFhi6Ey z9G8)e!it9QJeG;z{S-q#mC}oya&qBcr?g|?s1i?UDe$V|ZYF&)qO_)?IR@V8@Aq+3 z5H-bxeDk#9Mn=|*^qr1In+^`*`E=a2WD?0}({p}7)+^YsCJ^>*GpLIoP*gJ@IQgA- z@TP7jrozDPrB;9mM3NENi1a6@*?MX8^d+qkack%te<_|XkyHg7kck@RHP8d(vqe__ z-7tLo63>vhwWo5v?esYe890v)(#U1)EW008<6$pw@3#CSF zC)cRJu+$9R*33FSD2D0p_Sk~%4ue*S%f2M?1RwJbPnINyYQl{8*WVF@lxSv|AACQS zh8Zz_2SM7MK%-&bNl3c6T)MLw3^tfE8Q%yq4jsSciG>u464fpnWnR-v zzQoXRyln}80=!X#yJSpUyWzR8=DP-eU5*vR`nJ7$IEH)$hABoN{#b=aHI_j;H9u?y*x2sMm+20gDH|ztiH>=TDz&uAdnm zEISN(b~AO6k+etF3Fu^e2o&TAi{id3rtRiASsL<-t+hv}SWLRr zy)m%$`_m|x$N)^*2F?M|`$NEw27fAQ6m{ zE=q_5BNU927WZM}8Us|rN{j*~MPMaN13kC?5HVvo#Jd^5(!K2cploB|Mx&S7hG<+y zd&F74?(feC#?NPen&G(KAv!!wSmtNi-L{NR0QScChF=O@i`@3^Sy@$0UfhIAFV>!3 z<-Km6dWCNggQ)5XO67V!=tD1yH&jm#_Q-){h1O^`)RnS!NQ%euV0Lt;W`4Un%=J8 zzmLJ0zuHlh+qkIwnEVD?CG5>QI3G1DM@+>J_vfko{BSZBjpcWBvHNYdMgu5xs8bog zE!0q}+s)?qoxy!l{}TlB3w=9tY>g(id5>1E|$i zMfFY76_Yx#Gvs%X{iyzzx?r=Ao;Abx-GdU|@!Ev_yt7&A@Yh0|VD~q&IjQnf`J3a#HI@%e`s7hPC9<|4(4?Arm06DiOx7&PTPe0jID78K0h%ska6 zHvn}5#6FpR`6%|iFhBv2vGs+4GBl#OIgYh+$H_K+n^(BOwa`A#p&*#U%5g=(T==QqG9L z_vLI;V5@aoYVwvT@kPC7BYvsU%#2Ira8PaZye`@t6~jR1@Pd@tGF3=Yl}hp4aXDA% zU-ZucT;U~6 zUUOfxPmS-TE=3k9Je|uwgHd&ct7YM~N~wqS7;i}F$C{7{t!u5{evebj?1%Jz__O5u z&C`sxVp|31f~|*hFBwrf0%*LQo4tASzW#&YdFExM-O}tw@B0ujp3m_=T`cFm#Rc0l zMx>ACvI>>H;ipVI!)zw@b@C0RVY5GCjHk#^j1G=tpJR;ky!hVzv6s_!SdHXkmBc6> z$+7lC?V(`zgzXq9rp*2=am)&bo1q*9iFF116HT30ZHo!Fs%|iE@jL{qj=?w7%x;c(yRG;R4LNejwIJ@^UlIi08fui*+RxDI>rMv`YNS zd1~3`#F6{aFagE8kfxt+-8))j2~X}|;863G3Lu+7y2?e3ZV&Ndi;pS&`wLCkiqXnN zi6ZYB%!|`4Ab&1?osOWJf~`tUw0-lAOjA;)hO#X7vG2z?#i<>A)u4}mF~39}Rp&f# z+dof$1V1OYW;x?Xsigkg$E%mkZsVFUd@5}bjhqdQakME~E4r_~)T7-JY?UxT2qh0? zf_v6$f=U6yUwiFksQ+nrOl=PzIPs^#uNwnbO_r4qUlAi3U2o_#8I!TgDN0IK@%qzI zG32NB0OEw4GQ4tuuQsh1Dn;unM9x+%5*KH`EuIu~3ZY5M^bQuDXCTyGIS8mel^@o$ zAR_A$hVGH7XwJId=kmbO1mT%>$PHOz0R0T7cvu^aKQsa}E~InX7T=-eK+?**_p#Qh z^@(^}KK*b~wGJw8Cf$3$jAy&-HgaDS~uGybGHqtlJ(7St` zi0El6odd}dxt^MjzrT_Lg9tz0g{z2_@$1;oP))=w3=de~H4p{iT-OWPaK~uptyL8P zNT+*tR*K*S(&u`$%DI3-!abI+R}dsqem)A|ohm)| z3Z&yh#XP;X0P_i!=&%-(XD2-r^?l>06yl%V2{c#{h75mv-5*rAGULc}p7bP0uClIv z{?N;l7vfLzwQ;Z;JMTSPWNo3>O_^06Jh8GcaiQgS))d3Q%CLE}2_1~mSM+_Mr`r}duAhM_B=t^o+d7go z{gz4v6cPMG?fKjY?zr~uNQLccg_8MpRwBi!Gl71d-o46k_(o}|cPru zNh%dh=DOlfPb9h*_fvJRCI{?{+qx5;!ar`PI*TaNVse>+JA&Edt1MZ)&(5ol>B5FBqL+96w zUq**FBb|BPN2=@jKON9q4DE9nNRYt>f**h%E;N_m2w2PLH{|m^)+1YmLjL%1eZx$M zHRZPg0@Z_{3ZrOghvksz$!y_T)!Beo=+=rfIXkHIe(`(x!G8%SG~q{WTwxyV&9njE zrWu-P@3_`H0iOAw4a8Rf*sUDA%qOXBzaI`R%^>m;PM^b>rig!t>i}H%<9-uSGrfj; zv9bB>^V|+OZ;tQVAxiweg=<2!s$LrT)S=}Y3h&?h9v}z6ls7keZSVv$K*pT8oITFi zUzL!szk9uu6rj~Ql;U=+v9q7>;~~61^pGyS`|_h59UsY8KCOqa^(a!-Ayr6@*`O9i zfuq5{PAa3Zi_Ry6|I;#$L-5*?Gu$J z-G!Bv>K~XL(Wt5-A!vn#Sd!U2br8vuLjAY)W5pB+>)MlFv2x6~u)8uZ*F)m|(lo5+ zhkg(GO_d>3TR6IG3E$Wx@b5ttTAZ5%n_~Q}z7>0qIX<&9BC}>l%-=2ZUf0LGw*E); z{wf`=1G}69;cVc))se+~z?WNW^tWnn5ROWX&B~1A<7BY@8psQ8{gQ_>`iY!>>-grr zThnz@^csy=+al6Alk>jZOW>P)JO25Er5N$IUvN5}R{*t#84(8{87UkZNsKi zZ~RSNwSX_+u)uuo`&RSM*EQ#h>=N-&t<4qV7Ad1i7p@r-EW}~t*$l(_j|%_tp)tQW zndnxKGsGxAYS`oxVlyyJ_^W+Za;ha8ZWa#tgV{Wo*5kzIQRc^az=N4S&M7|qW6qDW z)<>fU4EVtS9>j(qF~LS;1pO07wFU@V71gW0pYe|HkTL@;^TD0oWwLsg2Hx&uU(eI&9Bz(J;#%KJM>610@`ba<5wP`A@Qc2oTM=66 zEzTkMg0=A$iI>s1m$Zi{n2{ZZdrrIgwfCtuHsdJ4Oe$eZ&Lk^dMcm=A@{BZ2byHuJzwi-$zo#fVp|>I&ERA#fgC(vpl`)mC zy9N~gL8(h>_nX4`V@VQPpSrY;hc5KvSNqhxnIoE>EtSgyHABx#-$EX7^j%+xM5&ii+~lu+HGm9Mz2P$?gp)~7h%BtgyF3*;p|^3~Py zpJX^ilj4bMlX5}o>6I(_xPlt0{>qK40?P^2z3xqe1Cv-2&D ze;~RW!wnL?#eZTGH7|UAM-Rp{5C#*kl;PfP;IDBNx!qD9(OD0bwNT3&*x!EZb-Pw5 z*2U3kxT=?M^MUR!psf|vKmPF%+1c!p-!`J_%3 zAr^kr!3P?Hs1ow#MptM|ytG_pTwRVz+%|IxRM*4am+kNF7udk$ z5j1Pw1F^+goTnRT{HNMPGnRXvbW%Q(%|bM#OV(4dRQ&5*@s!(K^=rI?RdF95U2lCU zHMtYtbd4f5v9|uVxtdAW=y8Pe9EtCMwJ`DYcF++5^r(DKi0L=QaM?WdN^;t)sU=y{ zOW6Z!eU{kSSvjHD-J>B~K{MN3e;d8OP0}73qxq?8YSPut@yGC>qjg)0P&moS(D4G% znQcmPJmpFhZ$g8XKF{lw3m*4gw=5|)>(qsfGuH$8OrbnRw1yt`sXB(6ax$)mP$3HS z!wlDAv)@@r@^$^8pwv8-{(-7T&BT*TU^&WyV1x~v6?H{=)r>WBUbRGe=)eK9p4+YA zs1%D4XnD17zc^-ZCxf?E5m~jHMNJC?8$X-y%PBW3*E*ZVMH*o=s8XGuy~NqwZX!jj zl-PY@_zb^1Va3|)8?uY(yaWbgNAakZ0$)6*Hl(tI80*C`XFEr{jp;6}vp^W42=sd_ z6Ij^F-R!2&zYept_4h^r4|R-S_1Ns2XZ{=vNi}Z=*r}ku(?8A&NTsvBWTPC4-3i&f z@ia-wmA!^U%uUG-py8X($YSFFE+^ay^LbTi(w-xsm69hv?7^#^gqf<*JH%@?o^N51 zkgQ^?J(X!M?&f=aMH(A>3vDC7nJGBydQ&@GQB_&~ijvYTLnmFr^XCd<(_<7|?nz2s zrGnb%^{mVKb;F{9tkor+`I%eBYUt3I8H?)*+7(5S9Occ7nJc7m+D=x_jM=4n$s3#E zIc1qf79~lD!#JKMmaS5NznEK%kg#DPwcm=a`y#ARq|Mq-^-ovN5|Xg$UCm|WUQRk6@;de@ZGB(G%|~OQ%5pBmPo2E5)Xc7psSt| zOtR>3&~To9JSgCBwpoB{P35k`)v}uL$ANJ=clc-Tn==C+*-+V)zEiu)%-%{)J#}1l z*+=KzgZZ$h)|Tk$_eRWvsp|Ayhr)tZ_ka#7Hf!FET?Wq!gvJ&|@=vA}I4YId9Z1== zoK?m%K;NE3E}Ogd_7<{8E*_Stbwb^mUkrC6{fV!)FH$KZx}N@8>Hp!u1uf>nFWgOXbe@F2x6;jG?_}dYSZty+7wy;(eFM zFa7qeo7&p8W_yoFZl>+$GH4&}u_LwE&Uw?8iW3hOf3^b~==~Vy{Fc4aZ0+klyYcc= zIIrQa*ou@~sD8JB#bUW%{`qdws zn*z5ZdUq}7ZFcr^9_xj+)W14G74E60+cqV0&$V0P%rfv@BYfrs&PwWCPaAN;K%a+t z-(qpH~oH3A8idc#>lk51*cxh*ZpRXJ8XkxVq$%I;0W<6KijGmmd z{?5g}_hL>bw2t1S*D-_1V>#81>n`$dOa3@9Ms*j4)@n}pX+msIafpr8f72?cPHC7o z>AIif^02UF0!gh-J0{FE?ko5r?ISPahk(mzLLV?t zE`Xr^??~&v!lZ|~e}k}pfi;zXz*zW}9q51HwYLEJcnn;BLAt*f(!K=KJVe=lgLKjm zk$D-Leh-ZPzmxcHsPIvW2SNW$iZ?n6186%{iVhk#A9k4@tkwob6zQhppxpV4 zU*epLhqJ;sO`y!q(M5pa@oIIY#V`B9MMeR9TL*ge#M{s1Uzsws7)x6Hha>DVTY>Iz z{e(rub4M~67u9m>*l_NKwrVL zSNs37<41V($nUTEk3#&vJ^sJO7ijnZ)e*Dh=+=o{>rSHkqrthJfLQ+V@4(Qp1@)Vw zec~qFu?Yg>)>Ps;VPFUM@Pd+CCB6)(HFDH(%M3w%_qeP4p2R8&ykj<8?7P2J8QCli zf2hY8bN{ModAzM{7r&dJYB8N}KL)4Dqu{Zj)aq$HUktcA8DqUZ8H?~#pBfJI|A)U{ zQp^6J5=@%b7Pe?*Kp+|y2~8IhBNtOXV<%JKABc?=%+18g$;1X$1#|JS^YDSe46JN? ltgMW#TQ4eNkT$11qdo2p^6I9?V5z9C>UC>8$uHVDFN(C5$sCGI@0V7>^hd< z*k^Q5N5?Wa0mMc|IRO+<;e6|WPPzBJ_nr6q-sk^3_xazWKXUfU*=M(PerxTu_sQK_ zZhiT&feq+C91{=?^^n>~>rZkV&MEXB9E2Eff&qF+lB8rb01E)g|2_VP^guV%lN!JO zJ^ugM13@Se{-q)T@}06xGN&>t`WFc4q@Fa?+a1_8_g z0)Pd;5?}?e2G{^>0fPZ{fFXdPfMEc8fCFGSz!BgCa0a*lTmfzXcfbh1NWdt-Xuuf2 zSim^Ic)$dJ5Fi4G0UiKPfEU0UKq>7+z$Ab#U@~9|zz^ULm3 z0;U0`17-kb0%ieb1Lgqc0%U+NfE*AGhyX+aq5#o=d4L!|EMPt$4iFDm0HE|S5s(B( z1}Fe2fK)&lU?E@;U@?GV^QC}gfaQP{fR%uBKnCEC=kH(De8>!kC3u8ib%^@xlm7F+ zlrH?cj{lqT|6OHM_xYb(KJ|>_&;}ZS4xoH`il3?HpYr7?uKfD^Q`dfd{;Bi-J^ttS zKo2~z^gm{GFxvmxLFL!NC=L4SK}qn3cOLE>Zo(hm-!O#s11MdC zk6zBDYzM`yl+B?ylmiF@P&S6*M9RKU+(+pm#c`Bf3WLm4nN6U?lrB?NipozN|JCOp zxG|;M)ct2h#YV(LC4|SzQ&SzBQSg6~xu9UmzIiN3jS?hAMkxfLaz(5>JU$9dp~?@6 ziik;#iikrX?+7A&CJIVE1cU_q1u3E*71U|v*Q zREz@dIxi|EB4$xkY&4vIy`;BpUQsEr^Ae(>qod+dqlP>8+EuS53GxN8kb$a6lp;Jf z0qTSVQ>c1}L@iF0CqM~tX$kX?B3uuI)U!?5-M_S}DK!-`_4XIlcVEBvm#2W-Avi{! z6h+w;sz0fEp9E0+Kpjp1YP=|$Lgmc{P}k)FsIqo`NmJiZ{(>yP*GuN-J9UbOIM}=Q zO3G7^g;HX%Ux;_mRB?#+RI$HzaBp_1OJyNZ3zA?wqI|vn7+W!XLEV!&sQOZ6QODQ1 zhj=<>u_{mn4RBC?E89@QEi>o7Vy;ex$x11)$0o z0Vuxo15o`$=`0Tr37~ZJYada%`L)ghkbgag)Bki%jgxARGxpwkqI}+0;~dL19AYX0c!wMTh{^B12zCQ0yY6Q19Ab>Smy%@0EK`eKrx^M zum!Lcunn*sumeEp#5aIlfKosipd3&Es08c=>;dcr>;vov8~_{y90D8$9042!Q~|01 z#{kCxCjgXwo&uZ(oB^B#oCACdI1ji0xCr=P+x3Ut>eVcPU5Nh>ftNzQC^li9K1GRe z==RN#vaw#jes|DaeCj+5rGerH^?q-}q@IDYk4DT<8zEX)Z@6eboZ}$-D)$5VGsBbM zj?U}NL4Cg!WG3Uj0b1#kz1`oep$&P{3N>1V$PcY#+b4pFb!yPMjAjj=p20@~m_uZc zsHUa{y?ptSM0f7oK_^a}XhGYzZ?`}hD4L9pM*jZ($W`g;iUtlG_@?zW`K|+uO>vQ&qY`fe1jd`23<@WPG_hIt%GWU zsU}!zLR(GfKtfkd=&1?)fpj+HFMQrP{3b5NurBO7>=l^ka10gqnDH%<_Ufx}AKDdUz)jThGlWq%5DqjqEHpLa z(TezVEB zNJbP|PapBbsMsh_yNG|wuGyq)1*@K8faVJ?)65KujS2@88^sMNHY&Sp{*%dP;Vnxu zo^BCe?_d#Mzlh(jh|ezKbA}f2xkY?l5#OMQ+pO{3eHYs2%CKte>Q2Nxne_3&LM!o@ zu{kku&MNWwIcc|5@pA3r|^BCD;O#Avn{+8>2l6SxcSJf()|Ih z_mAz*J<3R#&3o~1@1oLfUk#?uiS$ zqB1x!`00Y-ariWv-*$n2;)3ys3;hK@@2My^7~pzC7+Gv&Qp7ifkvA*i58_L24rC1) zyXvm6q;~&x-&vj(R}jY4E{wi{WsbS~+^xi2eGTn}Kc@kEb99}Kt=Dx5+3aO)#2m?I znVix#IrUJ-aXkBZaM*cHmViRoXC z7>O`7_5-Gjc;MMM;T28zlHQg>!;)IC(a&cU(=8wzP(#nSf_>ENq-SU=8Ec|nwHFAyA|=>Gi*m-A9%I~qrK%_^7$iP6twJaS-(fO zct2}Y- zV;vbu@&`&$O-^=u3k_bv7Q~!@6*l8 zhU0}F!`7U)JDvCaQsI+PhabApuQUq1oaz}#Et%p5b~F1kyPvthWRNMH?lsZ^XYi$t z*rt|R^9FM~R%e6TnqP#4>^2iYtN7UqdBdI=0-Ns~A2_~rM4gU0LQ~;z;cj7&FjBZsh^WpBopsSDUS^O!K*3HE z4O?~KoGE;pPA;@< zYZH!5pCH2n5xLOKOR^D{Y@|unpxIJ0jkRNeXh3?dtX!7ph<`NDIz%t@DiL$KIx{;n z=|>%(JZI#mSEipzzmYz1{+L~nF1zeVWIR7{;%XO(3FfY8oIg^uDX}zBa4hjk;={y! z!i%;WU!`}YAa+&LD&t(2+_Aa7l39|-NS%|s-H%_7*z;4}~Q`Sd=8lnX!4N2Qix3@4u zh|?!|x7@Y{c{K)fW{QSw+hc1|aQ|{ra+1Pi`^kgz8XdYh!yiery(9IZl`+@0cs2| zol2cr(^}-ea9P{?m&Ue9$vug>f@ifTrL8kFv%9LrNHthBS~W!_nxjfoWvX0HyvX18 zej;XNJkq>y{@{4-Pz}DE9#10c z^bzSANaT6N$Hf^v#W?hH4hq&gY&RaS&S6{o6TsF3p&gQd|OZRk43f(<5Oy0yS zSkU6L#?q$8=w{&nl#S(}rK_6uY|?(2j-a&efGpcvUa-U@OMdRW&6K_bFrd%H?n_P2c7a^q6T zlXHk!+}c7X`J^40;5NfyT=)Gu3f-TQ#wab zYx5E2Xp;F%^LX=>=6U87<|oaso1@3(&E}uY`#B06T^zj}Z^{I)y!H zsxXm#tAOoUwda?e#~2<4r8_GfPdQ#lWW3$U{|%)TJG@9M9{(aO@2c=;;cFrKDAbd@ zmpDm0B-&NclEo6ls2W#wR8k|kFKLo2u3Axr2Fe-|+Y$q0i-H8M3uNgslrO84oszA} zeIiqb(i3?~mbG$B;+*OY0f`Q^sfnu-Ia?D~)V`@o%+dcCd^7)EqM%v%S;&_?J7*=()=i6VuVYv1}dwzQ{{m1n7^x@3)>Ga4Nb+-Cp zKHhxKF?YsE--|UX2FEwfYx0rhP7Uv$fijw&9&)NUp(%5oQys=uc~UDQ^7<&WC<3L* z>J?~ORQ;n^H19Z(I;VQ+FRKqZRgUaPp52nra>yxj4C~mFLr!@a&ks2tPp-nX!t^$@ zcyF$c+`H^WPx|0heogUj^IZ?lXyDvz37A};T2ddSv%NlH*~`&6jfmuthg)uldgcy- z>n@d{GMiB|8u~er8JhU5jRD<^$_3?xIjH(=q=3=r|6H2%Hs5_l12=;V+E_15`=v2< zbN#$!FURIIhCL%&$UyRt^T%Xdd&&!G#@qbyGa8K8?2zsCbL5-rQ&7_6dPPCKQ9@Du zl4UPFAqQK*ex0pb{dU8B#!6CwJnj!9=5@8!zi_*LnH4sK_niHn8BD8QI*_P;p_OwC zrw=C|zZhl~>+ms|wZd_&dAWs|GIK=f=GrYk9L{b~A-2cl`iQeN^Elhu_Xvmpz!S{Gs+)ZRp$`$&%Cb+*4uIPLaxm%C$=2 z5#w+HOVW>P@Nc1% zk?0eGmRyBWZvLqyeTA4o0aUQ2@Ayko{!VwrlTe5``;YE|LLB~7h;$IuT?9`}Sf~l9 zrJAr(6V__N<{$PQO!)rk_}G&JeHhtL;K07VqZf#1yZ&?E*+P2;_qOTxk)geOZ%OtI zy=X>y-_XOzzroGH6Nl;L*5eu_{())=48uwXJT>7iPUi?{jJ`3E^1rskPE9a|sR{dk z*an!p{?j&$Bkc#{Qy~my*w+SYXv1H|U;68tR_HQEHQ@xk=By@M)PyT2F(lm8#0WJp zQcaA4#Ar1!Moo-W6XPH;UQJ9;6GE6!Lqe=3JV3X4=iyRsHQ}QsCaQ@^@QtsUn5-tI zs0lwv_^XMjYC@tW0w58nCW6#Nu$l;ggj7w0s)=d66LskfH8E37%u*Ax;hQ;XVy>Ey zsfjR1$kjx+nut&nk^itapq~GwgI)x-idk)S3LA(5melGTJlO{72qp87O(`yw^57!pg=#8Ne} zOie6@#0oXBQca|*i3|u*Wd6g7oBfT7XOkCv@$aFcvc8H(LdAzv0r&h-@xKNf|1Y1h ztJE{L3hw)blCq(k{69Tohn8Z^8c-KXNe=dE$rmMo1Zc?@C4mHJ$rmMo1Zc?@C4mG) zYrZH6BtT2PCfMS$03E>s8Hdb4j|&c zeTd*n`#49O1)2QeJm??fq^{Ws*Tlm$Q+uzWBzp0V`2F8=QIh2#840Y`cOOdf8%Xwp zZWw}pIBL{258#^J zzh6U1ykJC*|3z{VBsG7LSi^|?_!mhLNb3F~p~HwY|3#7tlFq+Knn0ojZbe@WsnSDW zj12!Gxe5}?-z9JfqIj02CXCbsA0B%~vjzxq5QJ%)HK%OV#Nd{8WTz&E!NaEAtm!jb zItQCIQEqhIX3c6fu?D`;Yu5BV`}(L(BlYQx9^*hguiN^6Y;S4r$A+Cb3@h#WPa#wo zIR?VWY65QR2w~*@YJ#KsJOc}vL4}Du=(Jbu1{0kdx=*+UZuL@(dN z#E5@|#d@cC^FeC9iq^*dzFBkAgEeUnjb(UOc=oXQB|c_C2(dM|!6C%7;Im)zc5ht9)>aACd`w0Z&oG@^Nb}dbcouJ{`m>9$8{tPB!;bP3y z-~`|dfPtN1B7QYSPdkCaE(%_^)e9LtCOn#s1)njDkGb{BimNm1{)!-B8^8kCI%ahqxp`ZTK;S|3`0G|PoN-0&pX-HpPMzG2Xh7^ zh^_5O_IA3k+xF#qi*Kxz-c(HWvu&Nlix0^s=>Zw+0`_k9X`OA)J7?deXMO4ij|Vv- z*G3EtMP3nRb9mT!^T)0~8HK(YQ{ST1Eo4uTV-{Yp1cRIQ1MGrV8EkCiNwZ1NK^ zQK`gh@cF# zG_Km$$NMwZS;VWj!!TyEX8qm%AvgR-GxvA3-d6j##| z6OKi$!fL+fJ$&5jD`f*Aq7DZPf75yhw@7*45ACm`hynTmA$=L(@Ut0K&aT{_Zr~Sn-rf;DyLni17UiYhw!^~3855oF=U$Y z*(-Uw*Rf*5%fGUvHuhV${#uOgz7E*Zi1YyJTMT<3WkEohzgOIT^lG&@+#q53TbL=134`~~jsFCc3NzzOp!zi>O<~@-B92nfznl6C- z5kR2bt?y&BQ4$TOs532^!J7G=0N(6qp9Y+%jZ%f^KQra}KQpB+tEJsayO%0IXkZ|@Jxu$Yv={FO=etvq!JNuLY7%a5(!wlLmwG8%hH@(!@%SCYhmC0Y1#nl^R<8w(^^ zHx^j(3rT(emi$7JL#=qUA$*1_9~;jv;+0L|74f7184ru%G4g>W6MrYk-yQ=?ej!QN zQ6akjg(M3e)4E%UK^8q$6iM~~OZJf@3HwTe!Pv-2iY6(RY@uj!6>N(g*(?*l5YQlDn<5T`f9XZwvuE}K9_{TJe5tAKX zV#Gp|-Wc&PtHZW458L)C`7ee!9;|Mw$26CzRQ!C=)HvY=3}HZvlTTpyv%$&LV5RJH znBp%c>Ja;N8;~a!`j}4RzkHuOBH8Vb#ZaqT*e6aD55<|up_=#=UN{ro=RI)##i)L4 zhn8-m5cgIlkI?mX@G$kHg=1qxQ9KvVo2Cz(**2(t!be8egi8W_iMhntJ5P-6y05gU zcYpbQ&eZ!87faTAaz=}bBzt|ovzdKf5;hr0IKN30`~+F8(Y%xNGuQ?!U@e`SxV(>3 zxz+d{ic@_e*_J2u?AV8e5lO(Qwiehgyx0sJEJrT3mj0d1XzgDeif%sdG`4Qm(6z$n z;DeDPwJcK?cHuBsj$(^>y@sPO`q!cHrWaVOSU(`*N#2YL!YRykCh@PhSS8L_ z|G0mM-JH?PxpN%+<(Ltzs+Wk1c=d{3c-RI#yKF3Q^5OK2-VTz^6tvPtYx75jc`j4d znev^DRt|EU9kc1X6>Hb|Gomd@(M)>(!j)D zoLjv=IkznEY{9W5`#jt3m7hrAKFYJzEdDg^(pk#0{jZt0n4CemwtZB*v7GiNDn2Cq zgNpBau&RHj;(Oq^(u`est0>2n9Ona6m4M=ueeVZ6;n<%?!H+oMNYTJad-sf&fTCXT z5>fQjXPiYy{+9ER8}OF*&Ahij)C)`mFS)6c-*VD^;ibLhyyPYsTMhv-|H$=LbD}zU zhd4mseim!7F9bfr#;&pJNAAn_eXM-YD(&_cR=xzRd<;BSr}Q550x@(xv-PxCB>9kx zBgsZGYcrEv`}+0f&1-X4!v=l=)x%uo^F-u<_wyx?`RA{^C9}dfjJBFbX)GXyfW@ zQ3m0=KUsu7+~?6v6UfR(L2jaqtuK|t^yu&7V;_**^aiS^nFs;vt?(?C+iM|g`^nIQ`U>XmYP!1(X-F`W1o*r zS&tT&%*c9O?B9)tk}@91?VhFUyL$rIH}?$}YzJKpwxjGMI5HcsI6Ik5!UPN`Bb|$O zPi7r+h#4ZYItpI*(ZPf^c-==0f%wxc-f7m-F(=jMbc=M0eQX+Sp79EUfOXtL?l-Wr z2FyTcj8(RV`SQK!{e;QxVrUAoKhSFeCR&DjqEJip*jZ-NG@+FNjCeH&NmAnz2e)f-@ad*@N#Q&qDUef9}Ud)gg&-m=m7&iBHTi5al zr52AoWxWpd^w<5Tdo!|!$sXg%YT0+P2eKv8@3Q(&cV#q}m?om(iCvN)5Uk@)N`Hn9$WUT>W_n?|W=0|R zm&CVW;SyArZkTSHe$e->yCPv^u5T`DR_?jPgWg~4&RY6@ol>yRlrIl6ulak4-y))r4C9#g|XU!GP$to27g;Ao=OP zkl|G|vwwpPfPDt!iOr;mRWW9D*1sE&R5X^c#1ah1$UXyd2|}B1__XQ}wD?`G{h*?s zd^OF^0Ts%%W+&b)#Lqi?H6VJ4#eCXD2b4$)z|h|_AoPD%bY!{q^Ui-$bYz4fbLiLD z$W|&eV)ivQLWM@U<0up3GzG=CfOovWTM&g5BzXN);3K*U49+4?fx=f{zrF)00tNG? z2)wMOTBJ^%B1ju4NL?cEvr3A!JO>74u7x)W_Z1|Fe0!rIyS|!~{SEquU{dfU7J@1a zKU<|WeK9FNgGnhMvD#H6R+_$w6r{te!Mg1B0ZcmTrlXImNXrA=#%yn0B^^oV*i(|k zTsMH<u)Oe2r#9uPGEL?-Jtxw_({EdU{jkUtoSlSzxIPMp~3<-1K z!`93)J0*#h=AWG))M(E=o?{ZFI zcVUI_IZY)Dv>RxkJURi>L+ol?vB03_d5^&ho{DqI6|Z3g_Q8mSm5bSpWD;EYjY5|YY4j%X9%0tRRo)ohN~n+!HloH08ifOM`?jIh zk)xQi+}CAO5jV40R_^+vT<|Rr*z&Qdya1xK>!ba^E#$7w>sz=?SDlFo|oAPI1#cAe=9hk7ja5j&`|( z<7Y6POE?4bT5Qi#VqPg)a~1fH_s2f4Tws2T=9Pj<6b~#*6!yFlg@PwqBvPOUmXj5C z7TNPAgJACR2bL%%<+p_j-WAB(KkI?z4C9|?@Mt4G^Axa|wx+qjIHSr*v}wRiAtO1n zB*jJ`;Nmoex52uaO}wk4=`pLXj*iLFcC)sqa-ned{BV-a0sEmEcnOV`b?9ImDYmF1TFF64d(>?9<8J6SG?0S z=YGx|xof$3;o%IhKqxQPlinZ?vUxPisy)$SVXoG-(i<2m9l!n}@25=disaQ@&W!xv z(&1>y%z(AbJWa8AVTf(Po8rtYnmN+auNeEewBOOOd-|<{*Bz4Z-dQGC`yExu6ML=w zAh7lvh!t?ha>W?Oo9oRzfou_>^kCxVt;m&r)+Zxdt9;;B0g_|O-{gA|Y zvV=>jkG?}VBd2x>Rh(GQc{TJ+Ri#zvSQRW4a&MQ3F>k@N##t`0C#R39at+C)+l!n7 zdBqVB-OON0H?Y59SFw*@)?GLBHx;{DaS@#{V6lxuFGqVrr6gq zZ~CaUv#dWe7cv*-_D1enHVb_QbYNj+!ixddcf#U@Q`Knv;JB06)(-pP-JCF#pnkQW1 ztP^jk&k+eYY!)3qwt~$4d|r6RnV2LNWvr*4CY$IzwoWNoS4ZD)ul>NFdwe*3s(b$| z>rK6F#?R={Zm^%Q)%2H|bf8)hKs0%YeuiK`ij`o^5ZRI78 zKDQ1oGo+1Wd~1i$Gv7fFprzuoQRyNYZ73}Dsfi8<(6(vryus&voP##VwKgW+#ao+x z4+})@;G59e=O8xoPf-Ez+hEBn8p5-5O)ItN1))@P{V(C!SM9JWqHh|ChxGT*lQL#t zja_L}JOE;}Ncv9mK~1y^OX<&rJ{!s2A-*K^A+s-sIrZ23pd_it02QL$BfO$Tb7l91 zXxoJNQ+B)#NkeqT@;9yXLh0RDBqpbiHxF?zc=?|4%eUdMu6Dp#VYnPWxr8+^&Oq?( zdj@bihUsp@Fe6yNhfPz7_J%XqaqKMB=lwo4Y&o(KI=VpCw2fD@Z+LB!+z*4`D&! zj(Kl{)<|a?jR9eG5kGC;7uRhYxNdt$ObM==>LVSsuxNujNU|P+=FA#+T_j^7IOT)1 z2O{fcC6O2=j*dJ^eQ1|w`5Du6;@?XzzsYoMCQd);Q_)w?HRK%Y~^)$0q; z8c^@PG;5?{XxTm>iZmrJxPeguOQ=9LSi=o(-GO!28Op>zlh%1Fj0NYZ8iHx7~*z?iX+D zq0qqafwhs!!RV^+S->sr8?=N6h=%($VGm#q--KCV*mHb$JZ}y!G8CU{q&J7JXFZSi z7~{|3?cy<-usRG?7#!yv;niam2G`-Ui4r`8lh+`42cLnI0Jl8ClY)Q`pED@IP&k8 zG)n=%4KT^xYy>vIY(%QnY|Avu&5mV`#~t@xwPvK%p}Rxm4>8(##n|^QH5JaV8Rh+8 z*eG;Q5ayG6gQONbjtbM>hyEyFWIo1ZTB6E1m}g;lCo!BK z2kuYzork#8jCu>pQE>LohJ#*MEAh##`c3OSAKw9Lf)GAH4woLnb0G#NC?Ki@Vo9k= z0T}EAy#NJeu9yCzC5p>Hz$isK{Ln!?GNl#dP{=p-HNk16;&hxf5vVMXko2>|ajwcPnu=!801EzEN zW+P0VhMJJJrg#qDG~MzsW)d+hJl(Pm6PQE@;e_W2FG@Y3+VbF}XJ3eWB!sw&aSR-I z7)Jb{=P^%D_sO2&G&a($*cBuW6yLUY@!Of;F$ak%7c3EPi{`yTy5*a&ZPA8Ex457V ztH3a@Z0XUJa|6YNVS0Wp0SWW~X^=D^O)DUBpkf|h%Fpx3%d7Axdj z@dwRp1A07fo-ysZ^~TJ5b|qP9mKpJ&ab3aoUbDpa?3y)e9t1%#v*$-j`xTrL4%goT zKC>Y2@SrV+JW0w=re4@G65bO~?`!eIQ%ZlNdnLnOtbL*M@eB?JD0Bc8gMSiXPWuA& z74KQt%%xj0@(%_cpY|goQ2a@B$RN}uAVnbx5F|}+fuGYDjsjp=<5E_V75vVJlVO0% zD0SZc0BX4)EVtM(S@ZzQXB?pqs>+3}j6mFGtOqa&3(3J&z(4Sqi~@}U{S>YXb}v)7 zp{2V|D7MSD%LxT+gwF5D@*P>eJGo%g(cPW+=<~broA{WUyIpZ#v9CB5Yjg2ycV2*@ z@h!Wf3MPEs4R1nryYx6OfdMh)uE4k+h$j|fCLX5Z9876hW3dkF4?oR~%(dvnHdkYpXW4>yrpbU+xxUm7Gdnb#7VO(0SkP zQ7)WLmQRbwn0f3(TJ*{L3l}PhaZW~^?Z3GGamD|QwZuh@( zd%(~uw+CLiZF1$dY2nRu`8uK=ZZ}a?_fFIJ#CN@_ZnCOwimHx*H9ql&)KpcSL{;a@ zZF~}Fsj3T7)maBNJ_%9PeG_=F@kywvZknoYx~gtQe^s5S=Yu*L_Tm@U&Yt_owXe?Q9swXGfrfF|-H^sZNUr`ldV-vjeXwJcPJPAa-76hqZ*qPJGL3#7>?g_p3ag&00r0d~U$i#i72(=vv!v9K7-L25P?X@rKT$fsckh8uMuKqot3~`bXOz z9ei|ouw>rA4sW^Wm~ksA$_ZB-E8iu*T1KnLM9Jd7$ys4p+kB=;OhgL3D>WH+Gh#i@ zuQ@E6U&xsS#CvEH4y$d!)=fRuffxv+x_`rxMrl3)Nau9s8uTpYo#Y zlCsY-k3V73-ZW^t?YlIgVOj8x5mQ4m#zZqCqJzeSPctx>(v;j9VAd)WC6#B1ZyHxu z{_+NeeujMZes)+CDZdhB8X<6Ar-<36rX$CdRGIT5MxUQ@zTNUEHLZV#}u9aWLO5QFxzec-aV@3ojTaq7^ zPebM+75%$f~jI-gY#0L=E?TlZPm8Z98 zO6I_{DI4;_vYb-}4jr|WHbumooSaj*BylP3E7Ffjf0fvksGDw0;c}32z$NotJf?B2GNfMc3W)0-I{dTeV1a(MsCt!`Iz9tEegXl zE6Q%K)Wo%Dm+c6Q*<5C}@kFgWIiG1TV|Zq_1*lsvcq5cEGUcc+dP4L#&;2pwlQP%e zTT-y8z+{Yi+*;ezyR(FhqaV`VUrA!fJyt&em&82KFL;=zvDC2jqxFVyoI{#J59gn9 zoOxugGJ3+#;Ue3S7W#2Paq<;3d*^=9(fKq>duR4atv!(BL`R>a>DW7Sr-gDSMgQRX z6YlJSyqzD>;^cQ|pKzAPCrS_>_=q-u=j^gh{)kqfxkIxX2?={=-lS;Ww9tpHNXq+7 zYlcCbTw<_-r^8=h02yW&?BKCH*2!lW6lf#{cIO~r?`$wF)L>Gy$`w6=48K5zdj=$A zXrVIvVqj)z$NvQ`zh^MkQpbmHI4Rn2TId_s8J53kp*dD@@?fh>OC5f)ReazaD^zNE zkUz(&Koe|r(Xvr1FwrYw{!T69X`yYd_Rjq$MSJ)6;SZP=%Jejr51bUeZy;@laaduP z`+-QNPE++-Rr18Tu1Dk>@>91#rFfLW&z%+LG*ek?)~E2{oL8Nmpc_` zDn1+tweYN|JkWI6!_Qs!?s&cB?&paEf!_wUy4$9{7BVWOrJu*>+#SzZ?!Iu%wY{zG z29<7_%8~j_)d8XdflFJHz1=452&^2*ZK@8pO)g7tX>;>RVpI-6o~Tf(CZzmQy|L72 zSUcCr$Hg#F`N7(0{`_E*p!xD3uhIDTw|Xkh90<&}`ZmgcqjO5pL67lEGFiSegMM>P znHxXz`7uAuvG35q)uA%CByONz%cG4oE%#d{H8oGl9IkQKNaVW3?rp<|Yjo)~bE7>M zyiSN-SlE!EaZL)-4Ve>0%HdbFqTm;}V9pam&Dl_XDmP54#u|UMST2jq{3w#Yb4}8s zg=tBCEDMW9DJ5Z*3k)(VURbmY#Yh=fF2df682XJ(p}bHV%skj!u~Um-DhM?lGh$@Q zM8z1F7Z&j&Q)VeJmX1sP$l)62*dMK3Q*@m}`WIN|E-4eqMex2}Tp;#-`o~7;%i$V2 zTAw^z13P+17X|oRrZ+C9zHZE2VqxD#D4kR*b|B{Fg;?h6FzkIoMx%90^7`a8sUvgW zE_7(U#mim1dsM{tncnf)qHrVmRU^3#hUPfWaeiTeCRrC|Z&vu0We;tgd?fpXg){Qg z_-P6>$nQpWT=FpF{~%j?xMpfY_C2uylDx~ls6iXW8^s7qL4mBI!g|=xYg81~Z@4Dd zvgp^#D8#XdH5|L5xSGrzt}#kBO4c-rr-c@kwq8oUlw{VMjcueAPpS%`DO za3|E6T+%v;U7nrA4g1NoYWRNEn5y|ji|bzqhRv?zgp`Ca7FUFYM^-JY*j9CDcT2Ji zr!7R-S%Pe^806=Up^TM7v5Qr;hXy!b)u`{`^hYDS0vh(x^Wk>S+o~8!W95+3c==vg zS^uDtut~>KZN7`6?+Pm}7mP$+%1~vDa;cJwZ&H>jpO(3tQZ5c`*z2)5?a?!3ixR6H za@uirr2)?)ui{5}f>`NaE30)%u3hZ1wsw216FMM1doklE6gKpIYt`%8kF|D6Nmfyq zd~4WH)i~8uRhX*ADx99CTBCYe=Jt(hPW`0s&zyy7sjjJp@7H^-n)JR-kt{3YkE>0q z6l9l4V=KZ0w=!Y{8L=f*^sz0=E-_>+$t_RI{G8Xn(feVp*A? zG&G|uers4tMOc#{LmKINeM(-%g6y)WZnBBZUUrG;%JydC&Pa3&N)2)P=A{a5ad{P6 zKy5!8a=HpiWj3)tu)SF3S!Y-_<|ECYmW}W?M~9uidHGwGG<&i6dh?y;_9yy1=bWtZ!X=%sGzEb(QBXT!Nd5y0U(%o#nW|QCnqlSDIah7njGCgvFKHUv6X{ zcD&?>PwLWn=Gfxs%URtr#p9cobgcqz*b@h#P#7p2a{A;kzO2k(V#muxC1IrztA(ts z!h^z<=XHM&J`+NBnWMAf@6Q+%JW&pjjFqr!EoycaIqtO(AiJW9X#XLsHIi+T6&Li* zOYTT~IY(>J@xbrT2nt?A=*zY&w3D?zqp!=9l?_T2j=CH#OO>pa&K;=C|HtlhmmL|8Ix)fi$gS)15Td6N+COfkH`!n(6ckt#_Evdp?XhOqj$G<;gc)tDNnW{77FI59S{p?^? z$em)+>U7z#Wt44;o6<*F6K*g|xj@O;P_|R$b@}QIqh;(<${R|rW9%z*FN=y+P3N(c zPfCt{t$D3;?XdvEiM2CoHzp6mwRfxICbj#H1;EJ)smC0nJ0AGrN&3Do?oZBRdqxqUO2bI1%=TiZ@)`;rQ#T? z78Ny$s{`@TD&v+R^>!!Moqtu((f`QRiykc@El$xb;)RL~xN|r5fC=;hXuW7x)Hn%W8rA8j+Wg+H6>W^+4FnHid(<4`_y$Z|^+(gyp=jvv9 zd$iQ{dwxF!*<#hsSkvVe^Zg%HLmVIYx(!pC9l3fPw)Ge%mN<$R-h6ofC%gQi<+$Kf z>aUK-=D=mWp_|LA0}ADsl_Oi|c4AfNeR{0oJGU;MK}W7?VPk}%TJ~JwxTs?`j_Ut%X2BRMEuC^ajznd7_gS55+N;pBMFjK+I$bK4U=7l^SS zhq4Lz3&gRWt0cveeUglGk{cr)NDRg&eD_JBKNF*y%sz1K#c1J_QMrzK--O*;D9Gba z6lZ)uDse$G=UD2#fRKh7TeNuc_u)Ze zG)%~A&CIgF)~u%Guc^K~e_7s{hKwW0u@)(*^OAM9D;FQWR_wAX=2~jFA`l4=U(+o$ zv2Yz+(5B2fR`s1@i4#MxBf|XNvOGd*STd{h-Mw7+MOC$2OEGSH8M7knUYWjo9{fs; z%~1Ee!nGwUI2EHCwiVXms8XjgU&P7FQ_NMw^Tb(M@cXcVE)1!zMsHWVS)f_q<8_s&bSw}xH7sm={Z9mG`a*F|_DG9!{UMZw04(oP5)1vfk^^J~TpFQK5-TD&zGFtiCJ*f3Q*& zmsGgvbm&h+#qG-n^=iw${pp~TQBtWYu^bnwfgHAoO0vu*>C{{Q~XwZyRvz>OTG2vKf`z$$H%ipB>-!t1XM=?%k4~ zzuWxY)qU*Rve0{lw<(?V_TEuVY+>8?Od6debOlZ5f?}lE zH3`iIhOS~Eq1lko6>HGhfEh%3a<1F@pJI3`w=7)jRY;mW3>qRU=zu5wb)XIa^lRt|OQg^(l+if;*X& zPfHxIjO6Z)>g}g(o>cY%4c#LmxIzX!khRD@%En}>awGYCIZY0E%R}Vx@>KarIT?Rn z?t+JU)0!JxOOrRlkTFHZ{anyl8sDP@=zDHfc?xqe=u?&62>%Z|`_(2W0D^CDrge#x3 z53SJ8WuK4D~%JZ(AJOV?6is(4CBId#~ zS=h-dJUH%k*3F|{Q45l0F5=%_ldQct0|RO5i}<@zQ!DcJ%QogcqI#ZAtSh@) z#uTKmcI3_}){5*p3Vx5iiykJ8ofBuH%y#8}7ok<|eHUSdE{n6zW9v>eMXBf}&+FW) zL;iBGB?aQA=abWB^mr~vRKc>vZb!#AXOuH;F`h99?-+6h9H<=#g(sceafoMH>@;>S zW!IvbYyvm>*@>UbL%a(QM7o8nEs@YD3o6-D34|rT$f4A2X+3BcPC9=tZX6Rwkc-sP5 zrc5ZSl1hOkD~{N~f~DkKp59Km!BH$|zDR9$WP0s8_pl( zqjE?EF+^@PEJ4;GY$+SzBEM5I?DJw3V-^8QV9df@SsXjj5{B0$HfN}@JT%0NLk&U2Yt`?CG%w)TY0SO>WL~vQW|E`WOLG5E)ha%$c7qSHd#f6AF0dZu;>>_3?4Ij zE82$+%p=Tb_!}qrb@XH$DwP$&3W-VFwDDEu*0`l(wGArxNi#Q$gowZB7WTs9dUm$RUsZtbw!-2NSrOF291&|t&+u7^7=TYHQ3oN z17a$9C}OhMHqnSbKgBC|1rMBSZ(Cl~WaieDLg}ZWUw0>EsaeIs$BQ@*vIh8793DUS z7zPern2Un;n_f)H%_gMeX0wHqBM1Fln5-Pz)vp7$m_+g?kJ{2KHoT68Qk{6$kA#Hg z3`ob<`DVrEPRW{adW#S(q;Y2Zqx6%kipa!dCTo>AF<gLHD%q= zFMAHkm&>x7dr=;Cm)@f(8+M;v4?W?F&FiA|?nq@`2f59m@zB%Dx&GmGT}L~mGNIUS zGq71z%x<|;xgD8*G-vqiL;Utc58HiUbhA)$MN%W#8wx{jCE095LX^HHwX)@+8ABao++GnS~;0AzPIm{c2L1}N}}*L zsJ){vIvrep=Xh)V3!y7TmkL=@b;T0|>&xdYteh~605KGPDY!ma9O4J``!Zt@Mi(L`s{MiWP%y&YB2RCFx!a9zBPWCTYM+l&X`P*M08~8gjRe2Cj zbGsi2cVEvIx1AHq_^eCQJ$OL5Pj~`PIu}_jEEDbt3w?wg88ja8IjTqU%QMVD0A=B)dcRlV&}6{fG*-%Gdem0iP! z2hQ~dDQjN(NkgvrMM`;p+p>tiv`(@jDs`cn5|Yt%DdPw_YjS|K$a9hGX6lm!h98}rDtW2k`ZC@T%RepK6mPM;$X(b&^Q73-?I%9*YtxSvex3OPmnODwlUQlC0 z9j$Cp(jU79S+jF}Xr2&~Lt1h(`9it;;rbsbvgh*iZDjd_o2_^JUk8t`iOk9jx?$yh zN6!0u4vRRWK+2FS-`Gq@Ss&_Ppz&Msc}5Ka&Dq{D`B;Ax=`kdvG_g!o$b(^v@#@ko zYR9hGeU*e-7XJ zPPjDL7kc%tUzBGzfgz*5#HeDH>ve?c7J0@I{0Z;8v&6^B;i?51lSIb?Ty8k~l0ozXjIkV>)#w(AVv?bg1wIESc?+itJ9%b&-&!1G#jI78!O$)^C+TygX%U^CpJR@$3IiuJmY6cg_bW)wt)k;s>pVjgzx*-X%rbK zQ~G;`S2KU!9)IbfE7yZw8K1LCqLAFnRNb!889t1;=MGz%11BuK7oPmcq~-UJ#cTbggo zrW`h(_?4N3gxkL{mLs5%a3b_MXA@HYk?Mw7i#z=3W&TStJ9wS|zyIjMiz|nS-81gE z2RQ5&p4?j~GED;s$JL}K@%M!@E4&nuhU2@imgZ%I+5S`2%ji41;*B>#X|DI*O zSh%rP_%eS#F4QidxHTP5D=ZPA&xtik)Jz?T272@Rsb}Ah+`qPERNU70^XUG2%{#>h zikvhuQw_)ke?2Mh&%SKhD?$IneQ$y~#r#DSHs$m z&)pUOecsCUy7Y;(T}soDOiC9^Ok@tS#50&g$?8N|b|>Mxq+W8p8NMq+joh87Hy>%G z;7VTE*3bBmYAyGbW~R=JmOd-p+Jhzp%QHUN6t5u8QPNj~wPw&(5YIZ=%=-SqXyARX z9d^iONuGAG>HGoOLyZ1!yf&nn-QO*oeFs%_OV8v33>{8(&eQ%Yg`}G<0D$L{>^@T| zoQ0ybYT)%2QF~qTw3)b31{4-ehyhQYCSq?_PCxBr-v{0+{UR*4$5{8HFJY3U1#c%M zkTPa=Rsy=TYmrn7?&@L@muU5vPhf2yN+oWbe;&do_VW%WuOu51%`-nl!TsIypxsLC z#qlcr-8)JV1d!&sy|mZ*uhfLV#Bfpm6V4RJ?b3l@+(JY zt5c!d)aTSkq*wp+KQ7IN3>YU!dl_jAE5;(7H0%Usju%5ukJx0Co?%pwD1ZwzzrK21 z0s|9h7RnbTChV&uEaLuOcGWd#U{^hLvC;CU)<1UD?2U-*%(iYP@Lv3rDB#Fi97tcB zGQRk~M1gATGAtjs@F9Sf{+lQu{err0zZYat{lg(~I)G2r#Q~ay(xru>zU_wG*tj$h z&@Ef8v;ei54egrqzcJS7JOPDa75)EVShblltOApw0%L7am{$MwtRjBY>9jqBOruW4 z?IA&yG^*m$mV=-cYNaAs;NPT{hd2j&{FlKg7U`hIP7S@E{TN_b4UJRYif7|5^o}XW zo+*3iD%6LO?FFb5?WM#;It8Q@?F;`oSt-OY-@g@3R)zuou{QbBuo`W0upgd4#TotM zC)NBiC__MyJA%O^Px(8y26Z5i1+Y*WqcLE^mM7quW7MB;i9TfX7`C7x7H4!bG3v%N zz2n(E{|4^E0Ajih(A4qcr_Uiy}cLreL zIUwU?I?2WttF%l>VeI@g4BVVA1eqv503wJrjA_kcj70Ffxz@Jmlx}YJQJ`o?4@x zdZV7^+D1JMU=Pd$Hn_U;Xt9F-b|;6&Ys3G#jjLO!Sun~Sy&w}-VIl-~@BkSw!V(DF zQxGeG9McCya>>yL$PrNRTS$?@_Vzk7X>kSkJ(+6p=AK+~?1f5lD2E)8XJM9!V#cY) zn{PNk23ecFP(o~;1u(y%ndo$TKSz)3STFe1*r{!!Xb+dJwVv`uB$O|)JO{VtX5-s!oWcG!@wl=Ctt`FTgp~uKLPz^5ZYv$1=EdI`i5e*aNJ|2?i`I^V>@3% z=UAMr2Z+@-^MjY0?7SG8i`sQFRs}`hT^H+$vi7p}^7Gy!i}eE0Dz}Yq8-tt~_ODn! z5cm{{OIH0l5T%{$2hyIR$;lw;DaI!GoV`h>ai_5+xXe|_c<-+R@xIC7sJ*OYz{R@m zi%8Z2hq1kF3%;s;`%{y3z78D7OV<55u<4*E8AF5)iIYJ}yVc>l$#ic{L-GteyI0Af zGXYuEo17H|Rj*Y(ZIX%d{X9X&1QX%$6y6gu#uijbn-A?Vp=u3rmz#TYGb3Zd@YxGI zP($2Y?BY`d+Uiq@SZMJnja-l8fQ=QdE%5VOoC||)&=B`)w4-oU;b`;_aFSZAK84{8 z+C5E#{wBI2PDd0jcQruYMp&OcnPJE)kI1zCNc;ON3|7RH0*O13O`-ns;YR9GGg(*}uqp zlxRWpeUi?v15RUyl*3^VbnD)@WCQYja?M#c*L;eSp?2Z_)}lYx(BSuN1sD$nNrzLB z3c0tR1ul$)mc0Es0PMO*BB+*8n&8^L!C^_rf9QmfKo3pZ3ebta`}(KQiPCo*OO)m| zkZL-OPJmFSqGj0gW?KQk2!zONH)QdfjhhQV*|#+GoUdD}dV7)8)lr~~uA zFpB?CCHP-eYWxqQK;b|&JtN%I&+Y$Hl%|dHfABP+(p1Z1^j|GXp-!iUJ;d$MnE7dV zN{ia@yVXn}HH8+jr?seeO!r*xSXhA~sU!|?5HDZ~0l2q`Sk$=MC@2I7KnN9o2myR6 z2rSvDptM4SAQo>jhad&US*DnHTuYVNr{Ziuh$tt3#2Zs6)`D;a+z_D!C>xS5m2^P! zKdpmTtLpWY0$1@=sMqmH`CulpEfMgmh=hYoqB+R;Z#8I44N_#-3?pdH=&%xJbo82# z43Wr~M92n>96IQz9trxy%r&S^1tGyC`9oAhh8zM!1rvsA0qi=Kv=8QFj3O`?Tjr-D zIAmzCy*ADD_W1P(t&O{v>;V;3nDgZlSc{BTxJaBIL?h`mRE_xl8$yPe!eN0vj|{-m;D&~Ln5Ib9QGrJW z{!!>_pwh_l>FU5ZuXZX5{FR0%@`Dv{=;TACX*>Af z*A|?l71Ps#?U8tnEL24qWdb%8f<~Ef>Vzpc6@hSxbO_k>jwx#aMwLJ`h4g3Ydjmn# z*3#6%EHyw=v~sPrJaq^H7*zgXLzyTcL-vNq0*f z!XdK%LX2McMr|A0I1+)7IG;>(+GU4{vrE%IVGv-4-DL-&9?}TuOF$5V0Q^DGBc^`t zxYf!q#UsFWE>z;A+>BEIrzm>&q>_uH+QJ2Vh)MKEz-^|;c<_-xu0RBWeW5)Fq)2$M zgBEnsUBJC})Le_XlH&}T5s^^B@&$XRni#PVnd;(a8%Fc6q6IHsz@AI91T1=plOH{L zBQ1^I$_kUIutIVg@)0*%V-xC(qqe<;!~Z1%4j>BvT5N}PN(Q=X`mTa zfB_n{0%xj>%7R6vs&qCAQo?Ai0s%xgin1vNiqOIq!V@N1t|-ZAaCH_h92_(qG|JL+ zpz>m{u06g8UIUrJW;A%U6Nr;Ls|-6X3H&k`4(~EG@$p~ix7hmYK=?}KIXDz(GDHx` zJ--yW9;u*>mZoSgT&aQJe_9MW1e{rw#S&bBNrrHadxQ*24&DC9(LMB)<_XitOiUJB zv;z*sw6Tl+Nd4FkG z0RgPCg4ndO_^idmU!-T!W2|U-uK8rhw~V$ggx2J;Gf@Ddl$42~7}cFX^0wyo_l|+K zh_C8)h2oXfnMB=HRp7m7UrESnI?%l3nZrJ4rNNlU$j(Qpb610sV z1?mTc3Fs9C4g+7O0U%fabVakFuhSVor3Hq1=tQ^=BslyR;P5{p_5Z)?^ak*L|Kfg# z+jJJ&eK6)Ujr$=ZQ$aw*o~uB_|b4u~95nMyd2|(~{O@QFd(>bT$3a8`QK<5thidL!W#Z~^(St|kXUm?hC$Uxw6 z1nAX7Dxd&mNeD{DSBAi#ErOLG$S(s(7rC|pL2iqIHkhdxK>F}96dys30%=RFMBt-* zInbwKPl%6T54!-V1gQ@F3`o+SiJ(XEK@dvTkEGz!BH-ly?(_(JBFsYPqEY=2`qfki z@t66lS0$|3RhM8*8-!7W5wg5R5!eC%e75 zJx5n&tJ^?TP=KeVONiFKLwn{;yEG z2F#)NV4(#pd&-l^2YaB; z0sgO1cb};f2umF7MJ-RWpr%>S(kwpE(=43ROwtti-^gpwy`2u*gx$6h*(PD&1d&<^ zCdpc=Z!OeUS?IA%L`H0j+XfKZl#>)-Wi)%KK>wajp?}fumVNjK{j&tk*|=v@=wC1AAN21D`QjA%2hg@naE>uR z|LpWOD$qZOw$16d-~XV0FDCy*|H%3P{X3~Z|9(1r8QfUw=J7B3*NT!&p@01}(8LDl z-!0I=3?HZ6_y_%iXo=T)`P1lM9K;NqLjNXd;lEdxDFO7)I1sx*rsnmoRLwu?AN22( zQ|TiG`d3%#{CNman2^Tg9VaEsNuN&V=St7WXJ@{FXYKJ1-+1^9Y?K@X-mnmF)Cd0r z1@))gi<T2g%N9t7V7Ff~)ss%Hb%q}Q_6v*S|0 z3qopW(IF=~onAlB#C;p!Ci`e5SU3k2uLY?k6oU^v9`J$y7c3E(f>*%_VI@&;jzvOP zR#ss!(0>IR*aZd<{hzB`gi)Z;l>De0Y+5JcoN@wLf2diYJ4c(G{{~JliPXBlHYw}t zAL>PkRgP4csqjEWt|@G=31EX!=j`YWYWG29KEMt30C2F>c^Sz{!3_!lIQY0}slFh& z*4y2XCNb4NG+AQ;n|RbmffLl5++a$R(Q#J*HyB5HLQWixq!~}%@n(Uqr!6+DQ5rCgh z%5{4aCKI59b>a+w66yk$P&p*XmkTF=e_5M@uwGcNf@pi4>L<|-WaDaU9y$sgwVk(c zS#_@WnnkOtKW8uYtJV@N393eoqn5I(?NLkUOX#8ScRTkVwmX4E=y-KU_R5lKC<-36 z8?%jrVJ8$l0akN@)WN6RYxmxk3*R!iZJaXA5vBo-aN~|UUL!?%&j3d_nhi%@f@=%n zF#44vMW06jM>v`jc`lh=2LfR*hZ+V(kmFQwNBA^wC2bx z>14I{_7p-FM0`v(k}5z#EGi5Caq8ZBak#vY@w#%Sg{T|FbL8???grb!(P)2wG%trQdgg9X;X894ug;3xjq!S(+w) zUsqE(SUrgM)UWjRdh+ysg=jdcXrg#mXo*n=J-WvW(1!eAxaM9Wp;18_;;T~L!IwbV ze$6v`H~N8-1@~ERBy{`wAmfJ?Tsr(jd^3VrD@Gz;vzMXy?J9%l9``JV1|n5 zkf128dsm=czDlMDodp&33)*-yJh6@?s{}c105{wqcpnpBm!BmJDyi_w%Gn!_MHzaY z_R30)7IR8DzJatS2Y>K7_CDZ#jeC%h)yBI3C(Tq&M;m%7&E%#i2d7cfXLxEj@^5e& zIBz)^P}41rC-Ck_d%<_+5AeIT^Aq?PsY(K2FmQ!7wa;Oy9ZigSJcn5nx_Ep+<^g&x zzzt_|Co`7VDDyfLxZzB0aplKZ*(I2XlMTrLH>`fm4>~2#rHkr*^Q$}^PTGlG#DAjI zx0J*ruxK;Od%TQJ=M`p&12?{xWULR^`W}bAngt~XEv!MQWiS@jyvW{v<@1RH$Hz}W z8rQYY9~YQj>dMQws38GXlmo}(E-FlN<>Jii4@!=oqRlEqNk_$B#fG;9sb9?-(~C0N zQlj=q^X{g19e45mDzu#t;!E`PE-JGn1a4J^*j>D>_;IjUmpD@f&6AxHUlvQmP2yhh zc4>fAb;b>h*^OQ1;EZmVSwuVoo_*4f+yx~pepx-5!h>S5BZx55&&D2F7G-MhW!J7px1YnZv1i*s`z>tjLCBVUrd^euF~@$tC|pn9 zGVG!AxUGRfyn#s1Vuoc{oAW2uOoRBDKIl`Qn8A^b+P+HFH7TiL?y9>gH}DPkw;CmB zbY-5AtMW3MmY1QM%Iak_UbB&_<}z0!SG+szV-PGFNeKG`5X1Mr0&gQ%wIUC7ch@@! ztg&>~1J{u$#87Dce64Dc2f^Jnenaa6S08O)u#H)zQC;Jiz#^|j(fu$Rd90XK#Oj)n zsLG<1P7hsl60s#J1gJRYRgGEYvHrh9bq9tQDC;@BXOD!LLHm~m_j&t9UvrssdU{?{ zbR&Ufx;i~>Rw0~}yWd+QjXPr(#$+5$+O~Yx)mkH%+r0lcWWmNHc6%8i<%L;@2&w*U zW*`_IaI6iqHO#}JhX?e5rF?$8^NO*R@xcRk5(iWX05PW3;pHboeggFfe`n6Sv0L)zSHwsKWswUph*qBZ@bU8oxNFY58f z66HSf=x{xwet)AxRX3$1ecqtSHCv^mxjfiLxg8jkLyRKh!-$WDF7Qj2W#Le9<&86H4AN6w1@Wq57DL;5sj2tJfY zMLTRFzp`WE`V+UGj+~|pZ6fysMaG6iZmUcQ4~pC#7P*}8YU3+!lr`rDnsY;wa|7Su zeWmFi%J2y{47K`Ov2S)C^W`tG+D&DB$*z#pu4@bCF1bqZ4U5F?i;fJUOMI$8VYg}t zcbYP6@m#uaRmF<-+fD*j&UL+m{C+q(ymDI`FWLPPK7ZS zAy-g_3UmLB3WxpjrTu2*kAMG^iR5ZG)d7tAt<;ITyh3k=%=;>tRk71LI+9$FwX;8Q zXN<%E;cfgItMLBcs4${$pTH`YJ;?(X^!P923{rvM>SJFNFRxJjsbCC)B8^!#(J)>c zzqJ8phj+pAZd5D@z-to9AB(^)Z)R~v$A7~|Z~R+{Kr$f##*j-VZ=W!u?H>_?L)0=!xj19^AgS>O2b@+3)Q~2* zd>{orRz^{DC^sq=T2r8d^!+1uv(0n;C~>3&ipoRH8WR8+7R+tMD?Aa(>CSH{-zm1Y zt{#7W3>=l#kX<2LOkGO_7(}BV3nUy??l!1dU8&L}A*E4IQBhkPR2Ak43vw`G=)Hdg z^9uYKZ&VpZjQI>)y`WZad4=|>mXW;%HSCyRhH@O^C}Z<8wR4Qyj2jhJ6e!QD{Bdvg zbtx{;An==XvZgSKnq&(g!>B6ftyan*f$@O>&(%)^UI?@xXUHHvtNd|)_O%#A0aZ=5 z@*~V9!SagPUJTu)HEgfI;6MzUz3GKUHoKHNvvC|;y^Z;j zs~X;3RUzJ-5Dq4u6YyN~1m^v_Ln{4tH@s$-;{mst^yMzaY7UWHM z46C^Ov6cz-k?@spBXyU9uFrC2G^!M6WD=jA@~tg5On( z>&36dxOydG*+iD^hI3k*=n-82GTcV{KA`>5-JnLh$*_bd2{D!UUDQ&bwMSR%-0`negnvGY*3?VK)x7_5T&M402wZpu9Xrj$O&@ers6i;jihX8 zsnq2dsYu6VQY3|2YhzwZ316k-Qcc-0rjD6xq0E9jr-ZNF&HtpA-Xe;X9eI5olP%NQ z*FQVb^mmTBAN_rQ%r=@u^Mf7jGM8`fp7fxmid-SyfRrRJ^8R4*FQ&4aAK6LWRTu+kA&DF3!IVJM|L+&WMTbk zy1b&4!;!;C)&!;)xr*TWZ|#J_l|1wX#@~=hgn&1}JHRRKHH{JV%}#h zm&`7Pvt&KuT@IJKV>>GL@Kh^eYZ|35zmXz6t=#0G{AX5rOFhJ(UL?g(XCFD+hb>mB zoVdn~y+eIxex&J(_3p|{CaVF4sNbj#SvmGSm}m!~%Parc=`A=|A9#<*%NR4_jybAp z*EW$=E5zMaWFwI?%cG_-)-yVef$B6{%4ZZiBc`a&30}uGG@&mu2A1CnvTRw)f_AGF z?QV)L@mCYfj#O12C8f7m4>7oN2!X^yety4m%$IJ8zeA4j2}lbkXGywuFT=R|to8DS zR4WXN*Lu16Fax6llLLXkA6S1G@6QkYdLq~o$oI}BozH4Hc^R5JI6pl9LH@B zU%LE=y?tc7DYJ>*?Zr9rI)VfIr1s|G1sj|oPe0G~^)>WO-b-7Xf^L|ELi4Zk9&z?} zn$cd_8?1+1FGsd*fEdS^>ynv1Am6Px(X%6wT#)G9lXzi(>wP(LO-G{7#-c=l7luFg z!#y6M%OtZYa&cm}2k)bTX8io~z#)hxNNbHSPHU|ccqS5A8b~ZXS>O35r%s2DN6x;^Xgg?R!wsM+9F~WHkF8 zv{DJ67q`8vQfBJXE4pItkd61(G}f4g277wNM275icJCo)5z$cYfU*dcJ7BeP56V{r z2b7^h$}^kbUFyu-0iM9kc6IJ@0cU@1?uoS4yaW5{Ja`Niw3C|F+9@`!$QhCZ)-wNU zMqTeA$0oTX_e(yNb(XSI#15VYBQ(EoS<@To>Vi{Raf|w+lgCGmj)f(EHHt>|@y_f2xDCOO+q}_JSJD z>OLwFm6<%s9e~Q`Q6807Lg$u0QrcU0pw>#6pO+(0$!xk9{3y5bDs(CG(LFA7`OqWv zs?4S<>5ukQLgM^KnkUveuD7-ChAJx`eXoRcujif!^ho*~lj4&WlcWzTVbUXO5+2qj z!zCmpY7OMlMD=OZJSu=F$7^eY^A7zj?RpLc_16~te5Sd<}cgPY5vw!!_VaodH4OUJ-eJ@?ameD$;xH7WVN*= zXUzz~pG!;5etHN0koK6Ps*R(gH+zR=F%PkTYiUvT=1J#Q_C_wemWZB2zo3kw(1StC z3ZmoUnSPP^3ziipMi})(8}u+Q*e>n6$=z6DUOR-#9bi&x&4%p2wBB|IcVd8uJ|rR| zdJl&-i5w=5Io;-0Mh6X|Yq&H)A_|G3;{~h_HV>9RdfODt-}mY1u8v|9iiugD#q6Ke zO_9^zXAx0QoV&@#EG|#*vM6tOq5svOA?0!7l*!D&he*_9RPEcP4|wWK;cJ-$8SlIc zcaoLhfNf5u-8rm5gf2J7FTxYn;8`D5Jy2)zqu=M}apk3%c(qTiTbRN0hyn-J;u1TL z(yVASQ*Wd@m=5pHU%(>1@7~H_uQPe+G74D_ZW4KdtH}|V4TXXE$4qdQVaAF5Ffc_} z4`z%hS|O8o-LYoUdlKyAS8P5A9O+gu;H8PR(AOD6U0-nKpEclV41QO~>=7*#Q<s^Nmw{_# zJwT~5??0imQ?l?P4AxQd5i+1!QCgYBDwAL83a7D-hWR=h>2KsDCIMXbkTw zS_er$-bkZM%v?vHk6N2Vr)@@bzIlGs!dhuEATq;_;ll7=>sx->##eNLqFg=a0Qj2+ zgw#r^+w4!pqN~iASVyd5mvhLMo?zdD%qwxbPe(?}d!oNkP5w|PqbfF55`1dx1DJs) zqBr5Zcz(rN^P)a#*AO;3ZDbsM7}3=y3mgb^n|*f4%NTtl_Wa#G-@Brr3TQV`o62NI zvy<7!+2`2g+wAA;ya$e^3@)o0@k{bUXZIH4c3U+h;lbJ0r8h^riOl7{CtU+mInYVY zfxu@RVO>7=Q%LF05jK<~?;2em>cR)bkJO2};s*~Rx^8|73C1?_L2ax_T~o|Ub*{|2 zx<^?!QwZtdb*v@`rW8jCQTOhH%t~_fdueMD<2`?Gn`@ubtPj?bk8c`!w=ZYy?4!9w z#j#&H#q`paL9X^Z@7MH*6^R+et9UJYD-w>ve^H z=vw1{+o=+cJy(>`6x&Dh+qp|?cdJuuFD>Y{6ZsW=H*vVTvhU5=pcRSzbQEeRMz5>m z6C>k$rK(SL?UJGEw8MiqD0*vE2GSGJ74Gt=Aqoc`lK4T_D?3B`;~Ju*t>(qQDwnlR zq?OzS1`aGtS!-UL{@{sSUQK%itv?Q-c}y3JV<-UX7oJJ|juBm_2fMP>iY@cGT6L)X zhBbPm2Jm1b`ziOCXdtel=x(%mu^MIrAgSXja)|Yq!-;;+-JYuZ*4*Cs6l+-HcaYS8 zGpyN7YES|z0i*_H!YPtE`<=T&}L}zN( zJl627*;F2-^~!&4jmVAMuIXFjVAr0#yhc>sz2-6Ax5inJGr9f&VH&1Be<8YS^J6u` z7wYp~Xqc?1i;V8lkdfXJ=gVlF=e4|LA)VUs?K;UX<-XsVwdyntU*PAxAgrhpT;p+{ z$jI&bpTX-4bkyY`oipOwXC}XF=+&yzC5CkB#<%MwHy>#vN+DCJqf`+cv0Hjb%9j>P zp{vq5X`A$m^pBJ)C&|vrA!k{Ge7h_`mLWSW6U%C4&Pr=*Bsd6tl6fBRE z58w;rx}R z1T~S03oNBt2R@-f>jS@0C#j&09m9c9!QRM#TG$5}W9%YE72^@(72`7l`pt;v%nm%s zaSFW82@Bj8*p(hw7&ryj*}z?k*7{UIJ_5{#DxWGCfBfs`stn0Z@3yO;wb1D_)m&H$ zS_?ifRL;WJtSk(k^vza*1CFZ{qGlADh>lkBj8S;Og4`P?D}}f40=(e*y+(eewxHLm z{+?&sRSfJgH>VPf>Gk?tsoIfkjTOPYUM__}R$ijIjfR><&091D{7PQc`&+fXZC5|C zy0Qby^m9f}c4vL8N%8Hu>TPU^HAM;(MH{ zcyvKSHtSjgRrg+cJ-6PxvcK7DU-kFO{6s}X#;mFbsU`QG#8#mwH>w_>&HjE-^#F78 zMQjx|{myZLu4;SkypnvKD}@DT>x5>E8y^a0V2gfcVI8DuY&gUze`$C7GEENKlE+ae*806Y>jD!_UG~ z)L?ie-V+~$kHsIspTJ+hLwE5Cxc~5%Nj!n%Aa@`QyOGvjv_D&|@Ioqq1l+nAzPS&E z=Tgc^5Qd^hnM0A2SnKtWWX94zkk>W+_1Eq%VJsazF8Gm{Pl3);?oeJ(=Hmw_XsRyN zk_v$U5$aYdo64o;P;K?aRH&Bv0snzIN>yQ0tR=bDF`(ew8Gc@jD~uXOGe`ltMuPM? zZ%EyYp*5T0U$A52KCw}G*_!#eO02Tndyj(s$0~P>-FhJ`%uz}RgpLm8_~rzvp4&yf zsAjQ8BbNQh>5J7abzZVU3qjSz5ibR2`4EZUn_to-AU?gKmGx)Gv+blnQ#TEyv{rfD zzz<-QC_84+JE`}Yx^FI)sboRqMly327E3Q?JYr;nFL!kTx8&GGB)tpJQ2B|3( zn|Jb2!9FkInrMpP_ddVpO0iS5=l@jp^Paq!mA`l61ny+f3BT~;3wGK*3l;9o8sXQV zA4`!32Bo?2q6%RRwzrp*6EA{MuX1C(x`jh|detb=3a??S{K9Kh-iLNL%*-5hE2%9# z2aVnuKYHR%{dMtP;VSW_D;P|D%%4&e)j6_cQ&nr;rYf?7Wcc_d5&~@haDxt1flD z@%HCZ7PE&l%)#*u_||+VY9${f+~ym^kLB-UdxTu$Kjzck9QCDyQ8I*j)z6M=9VkOx z(XBIJ#5&)q-zt3eB=`YY-#_Yc)|yw@&`T|p#O;nh`M4G+rH#Ijt)(aHd2k$!J{!?X z6#KdTpvK>M7jffl-HiKHSG{Ea%hdRz7y3RcJ%GL{UA zk)_C%^Uuqe{1-BrtZWgXh(9V*kst7O=E|)Z%NZzxGuqZdW4T=Cq+FSjaMHjgDqUD8 zgtXl$I@g~a$7;70Xz#-h3!j)gwD&}4>ig!4t>W%W5PbMW?E_cLA0J#MDJ;MJNODE2 zvYyvnr!5l?TwcSd&94~IhAeL#V7N*orVkfwzxOCC@j57*l`T$_QmZ&Zur%(4&V~l_%!4b`!{iI0+tMZ5k5hk0p_e?C>g!@Q%2Gz; zPz00`$~DSkO0JCDN4Y7(Q;n%PT+EP+Nu@=vY-KmC-yJ;>R#&LQmM7Y9s2nfO4S9on zU1;xT9YTH}l{A<%l7aC2v$Osx6+WQ?J%HDs-;0~;Fd(y)1<;Z;h40joZtQzZBB(`E9L!7$0k3m z-88k`G?%8m^EwXFpeCqkh=b)nk2=m()#0y$7QEBOQ23*k+U^-jL`W0=V;!%3iDjGf z9_0nn`?@|&x=l{slZKjO>`LDVUN~O(D70$b5*|aXwsqkr(g^A*qQRE(@!%@u@gN9MM6(gf>F7%iwe-yjEoGG_z^>dW3`Y~Xe!5y!S&Q`$JOGpRg zI%NR9R%4AbBgf?)pI6dnjRE+YNW|vs_RsPT5|3W#Jpo_oO~iB4xotV_XPb;Wzs~oA zb0F6da4_SUlO2^gkVItJf9%8V4XX`D-;@q2e?(bd6aL$xs_xa+gw`Sge8gyV&!L}* z@u!n&cdm)_yKLF!o`_%dV@?K_K4Y6)2Y>KDYo2zB!@F6KM0R;e3hDda;@N*Kx=Siw z2DRoL=YF1{9yUR0k{QqSX@%{?GOO0zv6Lpu{x>P5CNMnsIHL)YX~`5Lg{x(a0kUw} zVVUPqty40HIVsXP4Nu|M!17on_%!y(_17~7Ip_Qoehv8RRL{6g3jDep@YiLf-`n)Y zI`oFic@t@u7Mm;ZYXtDuZa1Hs8X=*t?o8p=2;i>)e$D+NYuq~-`!M~S=QAUEVjxHU#k3Q}{KqO~G8dl~Mn=ZK!bq?6pOj@O$&v%CjJ<%e_%D z&0d!*KO~uEuYKJwNT%8AUG8SoY4$pik( zqM?ge0-@NQgeD4_fMAz|ZUIARb`zS4E!3k&)P$mf6_PTIM3`yqt_njh3s`6J$gk7 z#h0LBuhlpvoPj%ET_k_lYktKUrRkfyCHXH=<>A&gg0AF5n;cxnSyDzWc#&7z%nltv6Lp<`; zF9;L-qS$kMoJ>5ht&;ki1s@Mz1{%z7&+KT>WZ?(2jP_ z&S zkt4hf*3d6#U30L&pr~GT55lkDf15d5Vfx%t5WBy=VK4URZmxmj9&oi}*_|K|*M^X!raV zo3T!^O|qta>!MW!1_{{nV0>y2X(6%Xeek^Q`$44li;1%C$WhdG>#)SOc0uiT3;I;X z^A^(c&d(hsjrzx*f~4B)>cO&o@)L5QytTGcPMoOimH&`?{)_W$hhB!0IZYEs+b8!8 zB1y{o?@YnlWsgRNjkbRO{`&ohTsc*4>0fL*U!L*E5(bnLnCiF&3yh=P^O+$Q&9g9$ z+XI<{bIbcXkrFJm2D@;h)3bc5A%u3s`kj_ONYTwwa|~O?x7^j*JA$ME_agU=PJS1< zZDJgch19oXmAdCx#87FM8*NAC9A|TKay2T`5A5@^M#Bu24iv2=t>JS@8b;>ugv_B9CTv##YBe55yo3a zKV!*1zRT(vJQm1gsRTqEs=~0j%i36A3%Z1}n)8kI%X+nq%-V>P^VW8wg&E_DS&%5V zO!xGDl8Z-KJ_4$d-0}ebc!%AI4!<(P=bKMkXU#v6kB^yYvv}4#aGo+);V?S5ry zRH)52_a=KyIKro|bCGzBRk<*(Ah&Gp=EZg+!7zJ)n4mMBHFkxYb<+|)qJxCbu?~^( z4k1@zU)atR5Ek!zg^P75%YPTU^JsVM-gt*?-7zBxAvSuSTN7dv9l~STP-iI~PF!t+ zKN2195W9?@0CbW|;>bvpYz%`JpOeGt;MxUl=O-@p_9+3ukT7~)xTgLOPCO9>QS*tB7_dpWTyQ$m<&6& zl_>rX+qB&P-eDWzts$Xg1y;`cjVR%b7ODuPE9$|!r)s1bs=%o4rLeDd3t+?0zS|g} zmTQ6}+yLy^2|C4Q#t?|QsF$yohI?UxmFYdy`v3{ib2n5GeQ*BY(lp+GYIPkN2b`QO zDP+Oe zHRh^i26EFD>)kN3+8hEmDh7(7XJre+Wv%0Ew;;$=J!|Sp4?R_D@lq=fJ<>8HUE9Nz zkGZ6RlP+2XzEJh*f=VK!kdUG%1XvcSg7R2;RvND`7@ql0D@dPXz;$)p1;@T26$Yen z3SvM8decR)w-stCL|ZTXa#0ZJX-&o1&)?*a ztBdfb7%wxirs6mGLq?rK%-sN+qTMu#H9bgyU*EF^)*{mf75~!-O(Ax;!A|H!#A%#G zcq)B9Q%}hzxWR%;C>Y%VB9}8E{0&$TB?0cM*ioz4KfV3-_Rwe{2*7SU;H{nRF@TO6 zi%M`6@C|^dIX+xY2b!=$JZ(K6bLblUa~Ck5Aqf8;*DGQFINfo**n&9Iwq^jK6{GaX9F>Ds~pKCib%;dKqqdc33#B_y8~Z4=Wv04GkOe zNCoBbnwDl8o%)QmA364(S{-#tIG5la_Diu19KTJq1cl$Rv)!Q-8t>!Ep zUc!pAsTVDPw5CMGNXu@eZYP1t=as3O{J^8S)K?@>70{NtH}>)8RA>Y6WHdEmnFV-? z%dsYbGE$C19?2|O(NCQa60wI6>Zo0clGQ+HkOT(_&ggV*h50Rn3#F$sEfDX-27iuvA1wzqV97mC{ekyIr&NA z>>}ba3n6jA+73Q#+<1D@DtwyHmAF1neAP8NXc15LJB4K%to@h~3qtOE%kU3o21kN8 z#3AGh*#0qNM|TF)*>-@KgN|F2g|pUt%(%;CuoanmLBs}jLXe;|4lD*k`49pu?$3Nt z)|0vO!9BXHoRfU7bXy|abrh3sZJ#uhq$WbXbEj8glcrayXAW7ZEua}qee#JrB*a+N zHo8<<>w#;;SU(xILeI~iZ+}_5{8$!8EldXF>@;u9$goOZIY!{yYA3C zSP3e&)VJ_n`1wEP)A*50TBbQ2S*GvXGBvHu6V4hRhHoydmDPetN?5ku``VJYDeuRg z(vOvg-zyEb{FV{fouS>durp)>JOTKyj~VgDu9{Lk^yb1H#Sr0NHa#isG1T1V%FI)% z*KiZtkF+l)iYDvXuZZ_L&U6H~<-E?cLe4eBtl=M&n^i2coFPl;pPJ^0e>Y$+ek!hd zxhMp~HZm^(8ACd40UtAVcW3O-2H%%6V-A#$i?hWVj*aCVdp=a0{mcss)h@WVog~_V zKq2lbl3lA3;$D%?eo(7O%c3J$I7Pv$sc9#}U)!xwhAXRHI)-KIXDE$d;n(Ds#@5{U zA^zicYOeA-wI!G(KMem`-8j`|b4z<90ZdwjWlyP=;Qc>lM9h*emamet`j%uLMEx7k9vAH(b)- z0Rq%)5xNGbSPp6B%xnQ_m{gCCT(UI$0W zXUJE{|B{OW!YvJC=O{ottJZ2=bGT({^(FK!`V2L1L*cZgNGvQLv7@%^(FIho+&q*S6HvA<6gMD+X7RSwA&7o%Fzb{+Xmf_vK8SLV<>- zd}6<{d&Bz5s(NXWKBR3Nef}1pay&R|Ia@hU zYSoP|IPIK+qxj|NLo#m||L-kS9DvC=uQlf)~Pep+bli8;WO&7Z%qhtvoAj zc^n$Nh%Al6Mi=_!M-zCOXD;~sDvhlxMZzikQOVPP(goke8WM@Xpk6RnNB{3fiPeob z1xLbs!Al;pFFQV_laSvg21lLS@*7s1-V20f0Y{vh~38L?T{%&{C}XtBm5^H_k! znup_bjnxj=bGo1`WN$A_FtE$9Sn~+CsW!THCGWdS`j8=(cUKyT!p=7N9k*{3yx>GmbLOG8U9PC%Mhed{gO>O?t^Fw*Wni zrIXK9n!i7XcrU*`eh)E%6p}G(i#~y2*KhvWg50W*!2}ueh1!rXh#FKW$ zg_wtgVZ-8NrH4bn_5flj*NIc0U$!W%%lw7DI*1*JeQT1=PZs5cZhxWYQ&1Fwq=10q z7QIcL#TLob^js?SNX84jUJ^ie+>8<|ws1Ojfg5f|mRB;a zn~yF9Gub3}S@~y+P<{UhlDauyyofK%Tv!yA$;v9d?eUoQol6(Zp_N)9sV!U2CvBUT zYAJ9QXkX0<(JsykIo$i|1)Vta@?otF*DTJa3H;S7(mjnc#Q2uKP%4p&Y)H}miL7LRk&ncBr9eppgh#>}dvw?G8Vj^EgrN@$76j76tfSM4Ma}i$eBZKj$!WKc zjk053D^DN1dFJo(vQ`OA`Jb7LB%i`_decl3!EQT#pqsH))rAD}kKh z?A(_*FhRo*Lr=VKUlB1K7**!qUsr4RXlCy2+R&QbCP1`!OjDn4R7o%t!WCz2z~fqD z+0F37^^a+{cu#p4I--%6dfdWJX2vz*25>KAg1k(lN@I(Wx%|a3SaXm z@WlJ3Z_q#L%fK0k#)8okDsuh&Ia=%p%nSX^uK%yDU+TTX6JB2izD`+;-v{4yfCiD! z*I>fx%fKT(4SRpbE&z`Hm+My%gx5WzpzBvIWY=%(0LxXbU(5f6t``*!m=zpw@W(tR zGN_2EcECJQ`##XA!$c$q>j>PU-Isv?`9Q!AW%sBC3^VN=M&Yrgrqp5f8UV1xL_WX- z&|KreOz_IVXb#?!Q2k|~(OJAk-PhO+>*hrO6EomWfCGkY)(i~*z6`9{he^fq957!j z063YB*)JsQM;@U@*g1m(zs_LyV=|1eSSlC?$J5oGt2toGM>apuyjl&GK{$Wnbh_Gu zY$NQqZKD93NLPDAaKNyNivc*7j)8!5pql=F&27I7;HSVaT@el*;Rl_TiO$X2r-0Yf?C%PB`P2|tN%-=L$*w)!hIoIuJK`v_Y>U4VN@0cKno!e ztOzcI@AmkHzzGK=ZJrJ#LT3{9MYy@x+m4lUl)d(#w)=mR!`ryI*-`i{d zy*&lAhC$$=m=jbj<7)|933~|@#|h^NT|cytz9C)wBVl;R{;0ln=^)o*aFsE0ZnJW48Z?T zTR$tbZs+6PDf@;d6cZ!%`baKs5ytm202{vy9B{Y5c=*xq7iWWqOlU{667zjF@Smip z;Sx5fdtZUuWl%7m6@TJ!5iAZD2rmtEVuL(AyKm^%&tb(N02ni{;QPmsqRVE$b`+ha z)IAJKkT{wtPjz{=n$g}*oE}tYVtok!d{=xN@%r35jDz9KstX-}+XHJUSU0r{9lUyx zddGNt0*vF`nxqDk8omrrV66vMSEjW8YC(!x;1ft2rf*2)c{iDc3H18yc&AJ^r+KLY zua#9HGifUS2G#WaUvzIgLsJd>t9RM`cB;AF(~xc#IWI0yY|&$W%{G%-$8-y*Y)-VaoZo@FW9pT$yhK9iv5(U<!x^Vk8U>*lTYhgd)Hsx=t3+QwKiw)4~VvZvQ`KcJ6oZ5!`cVKlNlk=1r z|I!coeMCB{lQcXJz|%60%^+Z*Hxg;~Hg6Zi_z^zU5Y*w}>2mCV*XNGTBTaR~zH$6N zq>m$+_YLNEbt!dTwy4u++f9^J18;53Xf*CVvnMavBTy)<4cQ?@Uxf8Eg_ogmhJW+H ztjItFS4%gwHis9c1GTnMOT8i!ES}O4|IqfKtz8`X&cS=J_=>$z zI0J7-T^Zzj0zV)hpbee3V;#Ef^tJ@#hfDfb-9Vs&{7VN%#U@nbU-DjzB{3El?Xk2V z#v)7hxY^K;X_(wddl^wBFxfR=6$5$%qv_*#?eopy)Ba7s9y z_QOSVLoi*~CZr4DM~Q|>MX@^ZGIT#&K42bF!Q}Wq@DHzH-t{w&0^xnk3t`7Z(I1)o z3yX{$9Pfj9$3$yD%z*i}&Ag)h`S2!czi{VF%jjMXa~wnh|w&MO)1akv37QTG1pddP>AfwErthnrM>`=iCxCNJz0uMdS8zBpCnUn>T6%6OQ z-Lx;y-}#Zbv%P4qW_fbqE-m0%Qm_^<@=95%k=Vl6VLP2=w}utYf&s}N#`dk7Im&Z= zXlvngRyHJ&(LW8^6+NzHim?{^4I~dKt+fC#pyDGPwMib1d#3zw(k20K$3caM<8+QE zVRZg;yt*N68n*+t*z?l9@ckE}JV`~9j(TN)MjIQSkQ|h>ThP2Bt$sk`CKKlx@ zQ0Z%7K%;HC6RAXJ=g*+g9IfOUdaRO6^K1jn_Spp7Mf9&>8LFh&ALqPvMouVE<_(X& z12LQL|CH=Sj$r2|#3p?2g_UFz2R5!dgbXd05+JxT$Dw8TK9)ZT-6|~G5H;uavDz^{ zr-L>bj8H@YO^Vt<>>GueF&60_>}Vgf!x4FwT-QBWpUf#$ZU#+O2^eaXft4TD-qOJe z={zSkxkW0Hz(V*($(&;fwi5bPyOL?N%J!o>*{z1h6vVH}R_2r$G=yMHJmSAwtx_w}m->4G7{CuU^kdA7`*aSg+mhj@@#1s`$6ADuYUYx4EJ|=x0D_zf1jK*JM`no^ zi?^yHow=1hcEJaSEl-b>k0>M&GiRn=Zhrb`;QiC*1L&ut=M9i0D|@H*E@_ZwoZYHE zHae(0`gGo>s3qQQKTrq99rTRWJA_nanE*AJ(sy_9IPba7Jf~bQ&!@`2%g~P8ch(U@ zv|BQT&QW~ahndPI*CrGM+*vW|TJ*?gURa#-SEW%`8%l`PJgj^x=|;T_3H_s#kt^=D zYQGBRD{&1y=x5JOSCz;?1^V+Fu6}eN>At&d`%lDC$@=mIHJ&mJ+R!mSqcuy%8%OP4 zzI3-Od?GPck{5nMJ&IU^O3dG#sMX@zi)Oy~_{!Z@nKdWRpdT?mPIrSN`V#rZ-ue~K zw|2}-M(RQem9M;1_ATIFN*$$(GE7lJ|8#Bh|Fmr%%l_%NEqIYM1E@#tizyL>hc^va zwY#)WHq!OyKhkU3o}_SGq0k)l01bZe+>U{b1^2kSgYli5PUyy)=V*A^VKYKO=KZ+G zMHZl=gGQ5$D-`#*D_cES!y5{VqJ0kzYe5ZL$#yWN(bivSIt9h+oPm}X4UXoeD3r0U zG^r^A#RCmmq2C80{AB7au^8-l?Bdf7aF$Zw`1-ViXy$2$glREsFQuxnPL(XbDPbfG z;FHdiSc3zBg+J-p6)_o_5lQ_Bw{>uU6(V8MCfCMcY`c08Y%Bdp=U|K5Q)%(V(2&fF zq4dHCx55Z_OkqUW1OJmD=U~g?WzGW*c*%Llv>#-0{hK6GO?l87A1uC=p0YRu>AdtH zpwT+;BAEEqfvoYzfNVD6lcdZl8udUrXPrJcv@!ior8@4;qvaG+w?f3MmEMmT3Nec7 zDnv-rTj|tFa0mg1vO}mck0JOE4jhV%rOMR6p+I$TNCO%j*8(F!2&*_S+;3^oxbRE)Ljv#UV>;bKV+m>7E_s0jqN@3$*R7thazu z#!~yAW$_bNrr2HCFikQvFavh0>#zwye>axB%)bztnXye*bo!Jls?fc{)(`r7)ooEE zPP7fPymT87Ey&+{KNh2Z7Y)~&a?;h+H>^mhU1naNH3L!&3qv-9y=baxz@16Y3i1TBE zb#T^PPAbr?7uoqGcx~<}bPLXAPa#$r`s<1kaH24h%%YQ-mADVu@%R*KvWdj11r_4+ zSH8pZr%9~ZF2o?6XZ_%XWpXbf`p_fHID6Tru)0?pn#2QPuaHWs?W}~Mkk4^Qr%drG z@b3U0-f4&8;9|7j1JjNTQ3HH8Hf;dc*UAdnTUWJWfbzJ z#j@6EGs5Yy9MsR(&u>UZoVeYYn5+3$^NE8lQWvSE!Vw%PJ5sh{aQ=nvy|LaIei?p6 zu?r>L-N}odck}n^Eec)~ys%*`cr5sp|5Dsgx7dFf7>-4d*amKxA9$wsoV3-?Fm{-0 z=SPk;=kg1vjlqq<@qW>zjG@g4peJt{-tQm9jpCBKZX7eY1>uW;L4!H8^jL)N>-Qt$ zbi!^$Lt1=igF_!Xog2hWk2C1Zj!}2NEz&rW|F6`xZ1Rkfo@kkWWE516bdSYiTHLUT zn7G}FAU%H4=%d#;%7o#+x(;?98MWtUD^rI)?=X)G#us)aO*#WUAsD|mA$5E;q3C;2 zf>H^TQ_R)JLWA+$X*B#J!wCWi!M(mY!yp1?e`4UP7BYe6?`Uht(8CH32wV>0F$=iIc_KW z8vjuptx)WjJP^vgd#geEf6P4uqbCcb3Z6Ghb!vS zhCX%Pau%h>nRDlu$9Hlpo@&<>G;AyrZS~vcQaHyiJNDlD(2A2zNCn@QN!xzU-tM(q z+Uq5>B20*~OG1l(7$j~ce3?M4X=an0dFm#EdP@f4X-q$qN@LD3ukGBpEZB<16s_>7 zi0yLP1W8m-;N(*wisG8Blx?SI7HoD8E|eaooMb%FEFkAqZIJC`L+=u%U-kxBM&Sm< zCHIYtCmH9dg{7ZkvKM*bG0Q5>eU9;Qi)BPIxD?tkUmxjtZy)J3PoHNQ8)SNq+)baY zTq;#>qc@bzT8qPM+{xA%W9(%dXPjr0GH^1_AVv21jTtO8)(w_DYhVd$_4*pW^G>R$ z^sW0lUM_d1KiS_Q3jZ8)mg$2FgWe@=bm^wKX3yM&LD0LTjXv4AK^9kttJr=#0D70? zvFvSjWai4M9C?TlQ->}t4c~wWLJ8G zB=cYTFhDXtBV_5pD_fUENOsgEzTO6Wq#vUS!&mRCgPHEjA{OieDr=SPJm#UaIG@^c zyyt=p>HacaS7N~W&lCL6P0a<~^ysW`-p0ZRk82!9g9=VVsB!BK4XVH+c{LA5;Qb|i zV$0jDP(`E%@%S;kWL^&MBF|npUHEPn4%549dmraa-m-|YJoHG-DG-p{n)f2ILXadd zys|7}&K0m+vOy9g{wkOhM*=yU?L-J=uas6DfmH3z(znH5bu104T`W0 zS?00RUfi!;-*e+ObSR;|P8jJb#p706KJ1_oTFZaDaRv3|<~Tbb>?A?@*Ph4g1`@+J-44v(9i-(G9#c6b)Zn{;)1sPZNP zxnmnN>e%i2*6I>;>iEY}-lQGZKSf6Fxo?`E?wH%tbE6Mkhc6TgKWoMH6UjY)Cz@W! z?a?p1CiUvo!L-ttrujoMgOTRGn+mVqbW$s=r%XiZ`m4K{hEFRrB4rnu_j3K71>+4H zYz@p_y$V=T;XmKx8$_dIyy9`xmBv|p9fqW~<8Xi|2I zS{&OY+oRUhhSz%RdQoQVvC{OdpP5DTYBbiwA$OLQnFVy`=vOP1Tj47w)NG}E#r_KR zeeHm+P-joHcZia}vI=a*q1DBEKdCD+Uv1eNY4Hu0^ow-h+y08*?i{qb4H0%!>U8}i z#6n*94wp{I)4s-kKF!e#+J+js&HeeZ;Ql)vKN|PjJt=QSXf389!@F&Y^Modc-tSKItnVle+luUZoX+fOnJ|sqST;72BZx*_~RSuDefJZH2U#h10r zs4QQy(kFMTFFqnlt2DHQX_iO>?+^-g*fa4pj z;48nH!@c2dYhPjo3e^*R8d!nTXe}??8qh3G{Mu_fF}zAOAud1I$tHQ{CyqxrNaiH2 zTZ*9?2wlTn*N;}|tWW^bq6Re=%5{2NDopV5ES){Bt{5#qnU7cuNkth#e>1Xb^ z+HcOfbu~?VVby9sk8lUhA>QAtE1SJ|CvLy(PrTm*?~e;PH#tu@t(-3$$gw8(^1xij z2^j8dA#f6S3f2j7jw-#>e|M^?gT^E2g2#eu0ySQ}pjvnqfFpS;{o)n6LUSQi=pkGy z1Y3pMlHWA737dkNgb#!i9@ulD{Y1t^l((Nm7OPAdK%iKa+&rH5rYT?iUR)>udBU@T zaq+%0m+!Ci+f#4u2m(4c2BbD7dYLh3L1Luo}*tq^}ZU3Mp2XurZx^dTi+{5%HeHM^*SCKeHJj*Bz3YW zkN2{|B+*9AiFMLlUS?l5kJ_W8v<&j?PA=i4)+joqz4nPmc;`zj78h0@+xjT*P<#1p zyKuf~ZDyf&%aN*0O*>Eie3Anm1T?jet_yag*Q!R7U9}*+Hm^2Af{5#Dk6tc2<@J&0 z)pulh_mTmv_Frc0H=evvMNr0f-m9whdw(}k@g!)^F~P&~B~ncL$#&6uFP^(zx`$=v z(-~tte_DM^_sK_>mB39Ye;?BJqT1~1L1BC|j4at8SF_;AljPa*3vvhu^nf$gE%KA% zU-GiVuOA1WJXnKBRC&)$;{G0b`{ zlHZzca(k^YSS~+I*ynhTASOU)81jJpYWZ7vfn+ZEu36?>&yNytg|e3%G@0mgg)A70 zuqhY?g_N)4Iv9Kncllj|AwmI}5Z?FX^e?#$vKUSfe`(FfB^KovDXW8t$Di zbK%C@W6^f4FaA=-xQz)b&Y?7A5|^Lvv*;5nkhHI(z5HjvIweZ`drj_HwSoynkoS?e zxQ$=!6*r^p%3YQk+Edz@N-NvDD9z$WmvUVer4glN7byvP9od&rniuLZlSiVz`pGLI zPvUJG^bLX=-#mt!yYX6i?Yskwq@pEmm$;(J3@J)G$_u*jr0Ry-3V{U!Z@bb}F1k;t z+g5j9w^r#*w*HLL%mdmNvM!9jaOz;AgszCqFHZ}i?e=*tHIxMO(V|mykFt%F#nE+1 zZ+@aQNrMWddvTQg3ANs+o7^t?OnkmA;*2>$HNmkN3+Cy|LKo zcYWX7m7l{1UHa>y`S$}uvNXO%R8uSC;Y|GuaRFomAB)FY#Cbe#*G&!(7|X|w%daxt z-pUxoPv#ec2r}yv%_$0&Ja2KfYgDaBeeA zRgplUAR(b$i};p0eWEBWCqIMT&0VGVFmg(M@asRslX686U1{pR?YPLQehn9IR%aPtRj>EyR}Xch z-X$&PtzGwhC#?@w_0#A?3iRFzT!g9e&UrgC+QQUB;^3wO)b&1oLHG^MmUR|zh6&yK z!@+J44|YM%b{X`|jXf^k^txtVsRYU__aGx9w0=eKrwZ|ySoHu@t}W!e%tiAIyLC)>)*X|$qS zG5f~fP4A82t4Q;8)N1Ja&{}PC_hqx|L}x8_gpS2Ta*tSU!F{QRp2+KB*i${kD%y3} z68qB@uZE-I6E?-@F6{SQ!;R#EO-O6^Jbb-NBp02vhNr7Mxu_c;6#I#L(gXENB+<6N z*zfw>mm}Xl!}}*Kr$+5JteII?<;D&;D&nv(IHpEhuk!Df&V3FC>YuL^@3q@&{ltOx z+*(UAbZJ3Fw>I&YUtwL#n|QKd^$+jnhv|6b0Q$3%vu^)xIM}T04GnuQn~*tp)oauooA*H} zM-AI`$F;O4n$?2T#LX+V--fGVN3~LihbG>t1x+*B9K9zDu6n|LzkCp8{-KY_%{ZoP z;+cb2MN>RigoV$go)i3BX*}^UBIv={Nc^@&CgvA&sh*Tq?eHZH7zVcN?UG=13Pw5J{em>vCrvp&D6uMg7TliZle2)^!3Lq$sC zVaUKLE0=YNb(i&w)yCqtHoZ}#IBuitqo@&r z)Yjjz)YQJ|iogQV%!~(sPFSNm;u@_nnBGRZK4MkhNQ%+E$-DygWMhJXW>_EbCrLfk zS&d>sv8V8tP)f%(Q0fIA1OiI_9CJy#wc=Z#{%t!z&a=8tT7;H9vkfamq|nOCUX(dW zAuc_8aTW>|qpQ&W>FUvlAp52!y!q8%@r3hFK95uVX@hjb&2A15Mpad5M1)_6lF)Y? zmlSLRU}yNXXOcFFCubc;kF$@%KEYY>6i4^7A$o#U;SYGKdN3>pJ9xnxsf+Kgx!<5M z9{c-jsY#U#FXVfiz1tofd%>nTC^6B2caT9=PDi^-QztuKxa;ou@Zw_Ruak#5UU*s8 z9N8UPyBA%B<)(DJ5X|L0bK2c-3Z*hrd8#rstWrC8DOvJt?7xC*=#NB))PLHZ_&>XP zb7<`?-9!Jh2~I>lla#Hwht{Ylmq0ZRGtJu|NEg}r_Ep-{5HUG&pSE`0clL)7J&^Qb%zo)>gt zX!maiVeP6O%ros*+y8B^Ywv0Y!|iHv6S=*7iF~!3EsvFhRJlZ;BPNSS1nR=6!r4Nw zRLBqppZiJrMA9iMJ}b%uz4~IQBy)k`a`IU{qdTOV#Yj~f=}nK`!Mp%LMobD+&WPCj z={L*r_uw={lUFUuF5eveds&HRiM1QgoCqWHM$kJml~bmu?tAOdS6-{0+u*D!v~BL9 zv;6e#&eKnQ(nl&&mwl_UyL4wsBG#BxuXArR`p2|d$7UrNN}k4~kNGxb-^4(oC!twO zTqQrO8F$5EV&C+*0_T4Re~0wM4}uj}AhZ#(EjJ@7g}paEHzOmM zbBqm@#52S#p=oUR2{Z`o(%I^3!K}07f5;VNXk9an_Zv1krn#Y5+44z6CXJ#iXganw zalLaAY~~PLC}25d17*idykZaK80EnMyaK3P@|q~Ulphq>hKIgk0Xq6s-zIUrAbzRM z!CZ9tN$`)L2K5ubQHT-4vu3z5fHz|!BZ`3&WHA0+JOAKM0Vrg3Fl_d}74{2lST9dn zih;8@idA!7z}hFi&Z;?G#{v(;I>zEo3}>!^xRcEBlf<}l0y+Ka+^tmLvX8xtAGr#u zjP4IPwVbg|&Jd@2+8Y?)y?^#Hn5HlXZ#gh$w`nEPv5d#$h4A8ehYsA57xUEoVc3GW zvw_#mQ+eD0Q^6d;GB~*j5q#A*eY~n2#4j~{c}wtA@J4W}eR(@k2rPvWwO+#YLY1T4 zN#W00P%hjZIOnCj{gD_En?A3TgD$z3c)eKF(Rfl^(O)X2#V8xal0JpFOu690av3l* zlmW5~WZ2z~4*raVcCb3o9#Yc&Sl-0O&m4yge`2k2ipi?FBEIYsp7cea!edh@ox#%uLuFNa@qI((h2e zJED%eKRRomsgclVimf*@KqjTLka?DVR*$gt(+v={dQC(hqp(E!ZC~msEazEXR#@Wt zZSnoKYW=qAm-}rs`fauPZMFMtr_}V@>h#;{8X%Z@L{nsd%y^aJ3aB$0oKjq!YZi7M z+Vk`+rVckixuk>|&Tk0^g&Gc&a9%j>H~;VOJx{dNMY6Q|zU5?vEk~~zYB)DagLUBz zT+go3Qcf!6BriXD~9X6A)lh%aHs9%E1UTw~Cc@N$Dn z$64dKa5Yi%{Ay$2{}ivutQ7OJFl_x84o?|bqzZ4Y@N9) zHZiJ5uVm|B^4}WIfHHRQF?KWTl+9zu*1Ls*v^{#8)Gb&}Fed!6BjzwvbKnbDOE)R0 zTK2Pj)?8!n$z_L|c`4iuXpjEM`uoZZj4NTsKi@QE)MvwI*1gBXc?O+O(3!i!Lv)IL z&hKilD0I4~t3jtq&zZYoRUSL?SV#os6gxC;>vYdkNUoE@YGG$tTFMxgjvJuveM?P! z-1G|ZYP?VFdL`3jYZHAhmhN<83*b>K>I>v(dN~#vC-Z?3qN|eV?KP~L^O>&h3 zU}>e76d*Wt)P$TNic z8MAOSH3b2Sa5;yOB#=N#Ah{qZvQ%ku`BL2MoRzX=3vy1uc)_On%~H@m_^5tEC)3i; zA{5zZxygdRrwea^w@9JRE1Op)RtYfKIoFZG*dp$G*^QL3TQ4j@z-!zb+T3J>pMpH4 zElDfQY`JhXX>~^*dv4Og&bcYRN%Q&Y?t9tu*3ls{G$UlPL8$W@DCiZZ#&gYzPlV`W z7H1a6fzr&uss}mq5}ymN<+xtIor0-2W&TX~AbZ`+%&bQUI;c^a`TXXCJm)*&tGP4M z@D>{t!dcEKga9}8?&5ModJKE#GFtrJ?bF14>R`TD?ZNs>PF7cQF^Tgov{WoGl z*26p0o;t}9*QHkAYOYz{)HE^h2IWDl`~s9lhB zkhF0cJpoh4PEf03ON?A^*3x1_3M=@B!IR4KuW@h|fP4Gc1<f!SFxw8Crz zpDnD)2)#Qi*tU8Vg*A&GSLK?O0}ha8NvTz=48U!J-YFZ<<~envoj_Ru#%uNJSGr1<+={vEy5&AtQC+4HXF~ zUbL(0l`8WR*flb$3}s@oCx%gL^ZW-3DG`^LaHsLRfUG^Ax(l4EP|w^$vf2eiH&;Ao z$FBKM5#l2%sB(JF78P-y%HGy$76flTNU97TtL=xHjR{gF3PtY8N{INv!$`K*Jt*6V z5tE_*CmDpUD}|-a=Gj+1VoEB`foAguT_5Bv@~+xnU#q2rG4E>WjFC^zGVaM-hD?|I z#A8b;)K@F=gU@`A`&mLnbhatK$S33krG?UIRv3=(ypK;34w!Zq`rtPtnnXgV>hnWX1J z{#zq6&QZIdY966{t5yVgH+fKopOT>;cZ#&ym|(%w>3iP&h}{3QduY7x1+<*E?gh@n zAt~nQIO7r@(F6a?Ff$RWIZUJ5E}vR0U1tUi3?5L+)O=^a(#bZbj7KT$4Ny} zb*++$BdZn5U+<~sec+88&@ZGf8H`EO5b*nxIEOMd1^ioXF=@Jjw3sx)a}ggN$oEgL zeP2}+mUNFd#H48tpO_*_yFYM@{O$zf7SMaX!*C@a}G$9n)@xT`EF+puoEg=|N3&o*gzz1fX` zl&W_%l?MxMAER4=@|yl9E<=Ay(ebOYRTtMrVO)lSqAtHzxN>j0{iC*43|>2|=wx!< zPI<(6&FPC4-Cp5wnq-g24@D4Q!7J}Z=n25g{u0MpvDzQ?z^>HnU znPhld%<=m0`knQMpFp|BA;0lS!!D@_r_mv`x`OkaQ3)*0$XHOY83(%8p<)9C8;VM? z36vhQP71GIpdrnR4eN#tu*Mf|RsT=k)PLfp&^lh-o0rD}W1vhMVQV9Ku}SbN6E>?wG`;5og1f$F8Vft1W^<`glS-{$eWP(C6ELKW6J?HYC@;-~UGIKx8&9#-p=Q(9cvyP5 z^!uwWx)TV%bRXw}Ktgvy*NU}f-5_?|tnQ-N^{(AnKFJ&0yNwGrGP|>4H;J}%FOVHX z4kAIaExWp>+EMF8`J#MNYCS8nyBI?7SSeQOn^{nL^+Ooa?4DJl>Tm&o* zYv^8|mEYdIUkYM&HiA4ZXxI38p=xHEY3JI~Kxy$;`d50O?6O(QzBSir%B!kkJsduj zw9!AqXFj?`Nv%(;%#6jP6$zBygn*Yjf1pslIX;M(Vb7W7G037S5)Ds15)*nnHXZqQ zlv*FPuQ8L?f~2ht8e^HHum+Qs&ljrF$0h?0HbD6Xn6>yMQNWDaRVk+{iXMC7 zaWP72L-dixi!PINeaSlxI4cE3AWVJT@lW6dnUI^R$!}+XE^y1_LKTkk z*7TiXKlfODgcN)oF3>qvf2?836s+S=XPKcnS+j^s0Ye?fV{r@aFC-v=*QCXdi#5Us zJ{rRmbv(|Kpa^N$qS0pfVctr#%n%}6#aiza7wYr4>odO&Uud8aewqFtH<1Yv3u?W< ztOeJUv~+%t?Gs;u=_;nnI|Z)1zC$tR)2L4xF`+|IX4sKQ=|yMb$c@dEK(8res6sOB zq6f&R8v5Ucp_Pp20dK}gCO?P~!$@Z2Fsuo880H(j@F#D^zT84EW6fh3Zizy{d{)Mi zzRm9x6J;D$5-XbpF0gL1s#!H|Hxia{B6VFkU^*uQeY1+TMG+xFe{%U8pfVo61yh>e za5nEygkyLB<>8=H`2=G#@7+f2WA79oiFBd9De3F*`kZ353-whC`MH$zSEn0P0HQ9JNYHnbS z@}COuQK)w$-DaO)%F)uyPiYTwf7PzubV_9%7IX{AbP+SW4@(OKH=v}}A^@KSzXS?i z2!|^C!f^|^ba5<9h>DM#^Qw)w-ufiU^@FXj{Jg1`*OkfX1<-pXVO?IrXwvcSjTysd z4mgG)jb|id!ux+o+pDD$A9;7C$<;UaUIJsKI<^0Yv-bdNV(a=x_at=C5Q-HIp@|5F z&_q!uRJ)zLJ=%c6Pl=$xqIN8_j|v4 z|IdA%dwGP+8iz^3%--2+tzXH(y#JahiWIwud_~|KcJIzbYQ4Ba+%HzareMGVYvcBE z1F%t7k@5b&IC*Pjx;@YlhhgFyrp7O;EP zJMZw?ix16D!RH91Y`{K6XMLsVPH?WFb#E6i8NhY!(S?^Njg+H|;4_f}o&pejhCM*$ zvUaa`enm6l-JGKIhL*8d2XaGc2WY2hBHGV9S{04t6kwm1cMX4f1NL@q-~Rn^S5!4b z@N^$uumn%Je5o8Zaa!?d%}?73QI->a_1w(`-}j>|o^wE1!6C+j`BQin8dAc4(2hGp_#@Wz{az+=*W*Oh;L+MhoSpH*PC~;;yOfg{`c+ zJ!WrKXBNR$mJW%3Q}_UupINdib4>jV+tYr*gWtl}5bxkNc=*NaQqI5~h(=aDd^IM=Nc39?-{GqvWSsm18Wr8}=sEUV zyHd7Hkm0!{upjo}NcbRZzZnIp{#vjIFVmEwEMDTH2CQ{$)R#S4J02^v;DzU<4xmP@)q;fp`%gltg(V#gW*i}MB1Xno|20ZkAo%U zHxT_tyuW#0d80gS!3y4iCi;~)=^mbD>W$$mvp&WNNHYf~i3E9qDnYa0qrgaDh+dv9 z@)u~PuGYGOQSR?kp8UYhoH=OTv+!3rbFs2v?xI`~WR#$}*Ck%`^(*`Am*RU@F#fZ6MtAdI}Rsc0xsV@Ubj!k-@u$UCwUgtxqS zglP*bcV8^ue*eXXg@g11N|<%l^9?{X@+!i&$u?s4;IMZz);hvq?+CVzj1}AsAR;j- z=FMmA(e_FV_Ksld=zIBg2J9Vq^p@}bUOw~$BW%(^B1i=6BCnW*kn7b=p0fumjP#EMPzTeA=vTdOL?eo zedK=llDENa!@2cQT8)ulw$dx>bIh4#c10Ph+ngVq&#@R%wp#deZkdg5k-Sb0TD$c6 zh7yf%JKO{RePJ_R9*rLzO=^#ZS0Je!0n2=kGs~H3m*cL)PDo4&90L^ zj917>rv%fUYCaC(TU&F(OP*$mXg_BgQ36(gbV6Q4P z>HrA55>qYoB)6b!2 zO))R*sXz7Lx$}V<9t+BM9<=VMvr+th+Ff4GK>sGgy4T!lMGy$-E#E6+cr@)-B7}+v z#PSDIeNU~%s4wLacJ)Iw_bV7ASf=`5>ie_YwxK!Fw?61+d8l3eX5hYmtCK3!`$9*m z?B?YWWh%@0x#(G^C5;-(Eu4J5m+$v)9ICOHmp7;xIKguMT1W?U-{Q=Q{8B#sm4%Cz z^@^T#C>zvg_Ylxy_`7Zk%6plcCpJ!Q7^=B*wPRwSuI2nyTJ>8s?CQ7gU~Nyfp3s!O z?3UL+w(i=GO37*@63|5uv*e)$()046my537w8_4E5SbC3f<&bvtU(=X?>hMX5q>LK z`unL^`m0*{t6BPMz(CW|U(3>ef~Eh&Xd&wW;_Y)jtJHr`=X|DZssGh_CP+M{)Gu72 zlx$NJbIvHCH<|3g3HPirFD#_*0Yh=VndHEGS}V01LxFd1dGt-Y`Vj5Lts#G3I;G}b zTAVFWsU^QbSr=2(Er*INAEKP|D_8bJU5N!%qfw>a)tU)rD>FdNlmtO#t!V;UVGHVP60TFh zBQ(LKoec6;CM<7XP?_(&M7~ZAwn<_nr>aLA&R5*y-j-kzP`j&L(kD@tLem1NlN46l zIa0?U9&!}9v-9X_zp~ct8p>I|@yNV)=aTcA!<)lNmwMv{&yn-@x9@L1mcKY$Be`fv zPTwalx@Lu!@rAyVfm9vgZqwjDT^)i9N!wY$Vf7bWgA8NBRgwZt0~d#18n{lqu2=)B zZC*w}$YwFA_)d5nMEDIgR3ORlbb2-KV&krsbB7;hHN}wtkur{^b&Ms9eahtg1Ic;X z`~jkU6}fpfeX)WRHXJ_R-l$wRV@^lVa5yWv@H&;7 zf5<+%AebO@A4Nzz8&CW=W>6E}^{^NiYgd3v0|qq@CIVwW$R~!1R*)e*v3tM|n(a;- zlf|Om%7?7YiDUnvbMwDFmSI3p3S|n&9khS}!7G$0Y-9>s7}&`a_A-ToOfe4zjxxo3 znF7jeIl*9|OtDC&SS(YZFks0P&X7tiQ@FwavP9jX+c`9=hQU&qVwp^_T&7q7gOxJH zDw$%nOu>eMr%d4`Q+PwhGYotoJz1vkmnqi3V69BCPNslV*9|b(C{t{bDFS2)4h*1+ zdXP*JEK_WTL5NHdCsV}B6d5qclqs@gifox82L@1)>Y7Y(9daRI0RP>RDgKfvZo{oR zGKE;C$dxJbd}Yu=yg;TXlqrhflNZYrB{D^+Oi>1dyE4T+$k~=D9>CzCOo7Q1E!*;j~-r((Jwt?wAbT? zIBP7&4W5<3>ikK9Cc02&3|<|49)r}|1)u;t0)VR(Da2thM)Laz2v>Dy@VVCstR?x~ z{M<{GDRjoJoZlp9h7Ikpp*uEw8SnzrWC}x>!VCtp#t6j3rG&v8nZi=0I3iOVg~2hI z;4fgsWQcR7+jDk(#F{L7vb|l zZt`WB;>!Q=eZpt^U+;4rk<0(z-lrRW0b(uT+dB0d-Y8b24`*5AMPkUyCf?@fUTCe| zPtehK6Mw}2`cnR9n*|^H`&eWSJcBpfCcgjwe6ip!-T#9@S^}Th6K;>6M<3p(3*U`G z&h;dU`NaPn{lC2`cX)=E{VX!|GXN9+b2hl~|M81BPd|KBwEuOUf2iB0{|D-};=2I? zAg>;W#QHH_JqnrIkXQeHq_=0~e=)Z|_D3uI%iP|NKw|iRF}I_UyZ^)7CQbey=JspE z`@fjmi6s0V=Js(MGPlRBE&dO4yGQ;nb6aD2qKh%eM#p*Er#;(-6sRh7jJM4gCvK1N zwwc0zh}&bl?V01m?Q!0A38FOV9il|=woe;E0_EsGylszh;4HMNb0pouwRsP?4?va|JPK<4v1)hOw3HNBd?{7a94Ofemndk}Rv38~JaLy!1V z@CO`nei|Q?CcqhTY&yKDLLVPlx-x~{QdxgAGKmgHS0@4oSK>5C>F)eVV~&x!LD!G1 zkklOsIoyXKsXNjRyhaW~Qg@_XG<>XvkD!`<*ongAI(~#Zbm61nEG9e@ZcJw`;VmdJ z0%7y!z&)yP4~-eRg2n_~M+V%O4tF@g|M;j3A|bN+u+uEKV;#yRI{MjBW(ip)#2q^FL zNaC6$HPO9{T4NLRE4FkCehQ{rSJSCU>i+ONP>9Vi2@3G-OF3(G@zhz3Ky}wNzW#3W zq|j+eQEB|_vtN3^VaSWuWHu-BNYO~7E0o5jKnC*f6m@Z~dTYu{WoYI;-|tJ06SEC; z4?qF86(H=Mp@xL?BgOKaDj)YVGMlh;(5iZjE$=>hPtRo>ssfYdDxx~20>>q546}km z(8`!mi#PwRd{1pRsNM%F$*t$W~LxTgN1C_AxAf^NEN;f6^Y zCPmUd^=1E5v$E4KOVAzFAgL;46&%*>csH6@p*9d?tZnT%o%5+McJ+?p0>nlosUkjM z(i3|>Pehs`&AJV#h7jxK!oLCRai6j5&MN{*3}#zbNIZaY##0l4X|lmsGmH3uwdp= z_ICU#6hAXybAmWT5Ai(b2FDf$=q5j0&e@MIUYm+rbMN9{T(oXnv(AyS9ExdCwmC;o z5|p+%3n*M(0VS-4GO6im0xJgsZ)9MxLUjyyV?|T26(o)xCz@88LqKoV99)doUAJt? zRJ2|NL$XpmNX+vL}tL#$t6ZTtn8?To)MGQ`$5=L?FL>c_5^#2^-mSdNhnX#p|Ds- zaY91cz=*|C*L(MZ*;l_wz!kiwDQJMipoy3wSJ85YM9?naGr&604$&czK=k*ljCc1$ z&qZJpWT}1>{Ss-4XNVbMXEE>*J4pA6E#sL#9B7@3n4{pS_?=iT7SXI}W1`a-*nsWA zc$g5&!k){e@^%W4VJGWcM_yF7zDMmzCvEXok_Fe8lWTuO= z_9HFNaEE0d)m2A+%}KzkifcXuZfQm5u=v~TYfpK9KI>O#z3;M*aP8AyNI7|urL#;a zI$04IU;?oLr$;4FYxSpg>$)Xg*N49oH*7Y#~Xe4ZEcwlr62&uTmo_Ie&q9s zZ{@O`sbIDt4O0LLF8;`k@E+Zh6maRPBn0FLDZ;&{~= z2sp+G#4!OlmJo>JRcBP9`Aeh(;<#?rnO{qB0&z?Lj&TBUOaP9h1md{T{YZlvP9Tm6 z!0{O3_|^0)Dq@^K9206nF3Ba+GKpcNPaHanPP9Tm6z_FA-9DiGM#+f$=5yvrn0&uJf5yy+p z#07JwzKFgv30(F2ctGO1V03Xr<J@LewVe6#EjJ!arfhNSlx^Ye(#XH6PSz>~kkv(N<>?KpvMD~cFTx%ScJksQTWE__~ zwa`zn_2i}}Qi}TVlmyqAYolhqh5x$o-MtXd~40+)>Liy&~x zN2;8xaDml0E_uWzw0azuJaW(d$T%+f<85@0d$sx^h)b6IRpei6T&uoFa2ntGbR3sF zs!~OWYCv4_sQZyaQigc2&F=g}J| zxQ9MRrRYmZx8$cpLuw#pjiZ?%nEBm<1~pX^(u4OJ5^1}%PpWFJ{Y`@oMr|!RsH-&S z8m3W{b@CluJY2Iw_4MJ^m``M~mP2@J`k8Kz$OZeZ%fXnq<)?_CZ=Y}OA^kcTgd38_ zCn>{eLy`z9yn9b`#H-aqpZA^EI*?$rbWS}!J$ObBc@BAkDvSEqOEVxgAkJ9}!yA4& zk~{GJLF8*R;Pz-LAHYxJgcX!3^2QJ7QV2&&Ld6x~@hC#da293rq*so+BlQ<5wTflg zFZ;wxW~$KlBPl~*Vq?q7@H^|RQ=o}{*v+Xo#cvyknZhSu$Bf0C+#k1CYev$H08Nyt|X~PQR>me{U^eE2t-?51I2MnkVAn8E zh7p+h`xC>0(Joobk7BIzlOQK3=|Pb zz>X7vF+u7e;oP_iY%cfBr{}wl3K8>{AG5 zPk*%pAB>nn+OJf>;f3?!c`3YWVK;kz-r4_NrRwm%gZ2&Y8}I3!e+MljTAnJ7bxHx1 zf|mj!rHBx?oDd4Kj)G}FG=!jLlE_@NK(ta6AZnXWi4&a@T@?k?%2$gd-`Dz`uaV@F zxsPe%tOAMX*?oPWRuiLgpW*!UEhFIy!q;6#azJuglIA!?DdUi-c1|C= z$g*%#)T&d<#2Yd3nRtfNb#;eV+=Lfw_|1(alKa8&Wcbrb_9$occ<8-*EDQeUB_>@56Ky^b?z@|AW zI3x~?jnCmc7>|!n2_z2u5H=aEvarA*abRqG^21?Fm}nbHZKQ5e4{(^B#Li?FvTNC| z*G(|e9~l?G|(rfQ6749i(a=f@+uS)E5` z8EbQq_>N;$J*G*Ag!w&bC03^3R^iQ_;*v7nQ{FqCoQDX2uE10<@3>v<%46-vH^**n z06PSS1Oh>Zpg>R~cqRBO_&Fq)AflbmBS?t&bI8^rGEQ7e1!kNpXA0PmE2oH6+uYRV zD=Oder=X&PtltouoYMkS(+{VXwLl&83m03Ib6Ut6IW0(83%RVNSEM8!_C(Ag#Xj=I zIl;HrRk+KI$hKs1cuzzIqIE)CI|W!7-xg!y263l&K&*;Q#lReF5yr-Xut+QsJCEHc z2Ko)f*rU?2hF92UYzUhm0nnLC;wtfzY?bVnH1_1Q1Np$kYItcIZjcWN%PJ1h`FA8` zRny;8lJ8Ew)b4bg^l^ek0 zWJ;ILle*{G6-mdWjn>pRea0c5VIrNydUod~b@fLyPw!RP%(@k**RH9QUENluK0NtO zE2F8IZJ*(w4cHz7Y3SkwTMs(Sw;ios8uQ}3xgYYFIuRb>r_gi_(w*i}Xv!=7m@#}LJW3hh;^Upm_CpEb?H#hQU2 z@4a1(fSuKOzsP}W0b+=reCwyfAS2|TJ{+!7-$k!IUB4?pAKIwx#9g)hyx*O!%b`Xq z+X}h0+>2g%${%*GBlJ%OVj70I6M3#bKX6F}Iwh_=Ki*2m%R7d+XL&&K$Efn<(1npc zNl$ohc`0i^^1x+o0?$N{>Ms?#3QD!XLf#*Olr_)thIu|ow%j}cNLiEYS9X|~a|Z`= zn$;;_I>m}IuD(o|+^12%{va{s_J{pJIqmfn$6KlGtbWSwlz-U2vppr#_Sa><2=faD$8AaLVQT08 zAk9wC)Q)|@nw?_dwIMZK57%EKZ=C~zVf71Ps_mxl?m`pi4A5HFP#GseqHeYyNqpzw z3Ok!KbF24shSN!V81nPe^EIQ=RyWt9u`!XBdDHW?v^y$u+GSPmy)_eR1GWzOY}e9~ z{_xc>gPCk+BQj%I6}f$V%hKv8rU8xdmDvG}N8&C$Id(yxHGG>@7Ey9`B45Od~xYJwFKW zKTPJ!GFSL%r~5VpPIQ&SPw(9a+CstRIUs6(TZE%F*Vy_<`_0egZzl6&Il|V-{H*s2 zE0Ti&xY(VA?p1Lskg`v{=73`RX4W>X__lK&vgTcFe<|;lU-+C^ z{DEhXAKGQYOn;vXZUt(SanH)OsQ9)}jiYV5`?}|kba>*yxCwrdHTt%PL=Nrx6CRds zAK6ajdl@DjO(r-OPWkuRmprW!7QrQh_;Ikh)#=? zM54U2)+M6HqBkNH`C=$AnV~M8o*kB9Cccg?7jG1^c8mGqv*K)VvG~zng%L&1ul$i0 zW%%)Gwsu{o9Ll=A_;nRy@pVF69%^DU$}kITA+{Pj98omZ9?Dz88eQr^8T&*v1O->~Q|^<&Dvo+>J9E*B~j1Swvb(> zHC>S0l-!rFpqqX5a9z(MJt26lHQ?_~8t^BpG#%m!LNv~E;e_Mc)XofOdJ2CfqRH^- zmN@St(hJfE*5N&FMN&{FZI#NTZ+%6~Ky;eiQVtf&J>|jjDESfj1^G?+eK~j`pMdwu zmkQPk%<%>IN*tWV!|-ZBi(p({egXx2$5|q8k&eiLvXqic$)SM5lt-e!MTL}F%4^CO z$}j~?q#4p|Xl^ur+BRAY?UVs9*r)ukg!Xub(y}hvAT6v2v@`V?vl#OkD;S#?T!!`m zB}hTF;_q_b>|V)u$>?VAe=;;s19UFRLcP%trzNDDo|OEML}r}bjiL;+hzS-|=)Du& z^L*TDzo_DF#j3Mq>mMWl}dJ}oC7OKBQ0RkXbZJ_w{{0)URR_C8Vah$sMj^Y z+)6*+kom*vUj#}0en zW42}>K@k&z@`|j$tIr`-cAIUu8>_58Kul>3YqdB~93f5+{}~<~Z9Hu=4lB=Ng=bI{ zg%|&s6;d1NdB51Y9lke---DsZH^K^Y;prOX@S9=al=ETc1l5olni93$LUn!xXwLgr zr{nmu+fgD3$dgn_LeP8681%8;J?427*we-nxE_?u)@RRR56@>exkAxT`QW`@#%nz) z>&Hs%%NqXh_^w}ZkbPzudXde#&Bj(Q^>ESspk41V@}8=@*SMFp#2a|~Z1-NfF%~(- zi1A-aJ-Y7UteM^<<1iie>% zy2h^K0`)*OX81PEIESPN^5(sp?KId1ZW<E62GK6j8!ixvvP4B9Al4DfL|(kjJU|!QirvK<@Lf0$KP?uC zL7uou+${bm{&gdhefEqw1{OS6I@uT7f-Rm7o#?QenD%|_1=fc3mZ3OCmHeKLG}uT2 zDX{WONu`{U+@*kLl=l<`B{WWzrrf_J)`SMGikH&X({|Dh(+H`yLfUYx0oSaO=xrPs zG3uI9KIU|*{&a9kP{BQQ#&m`i17!?*&E}dxjh}&-2g823T(g&BBvZtQXZYGk$JQ^C zYUh{P&!8f(2>#--5$UvXwo$SrLS7*$GW*$4Wgc5+fi8ruqK?Aa{ITG3uywqOX?JS4&oMJs>^(QbQv}ihOOFu_bn+>*JQtI93>Z;8!NF zQ`xxo!>B?~z$V;DKC}O>zA~V}G2no?92Uo$6T&&cJI48w^A`s^~lyTr9AdriMy?}>c zt>6#AK>;{(!l0Wx7rG0denovF_$C0mtzJ=eMZ|SoHg;XLc_N|cGb60{DJE(Vb&B`{ zB31EJ@f`6YF7iN;qqVM`;WuJ7Vs_eA3q}0CEz#$RO&Ny=-Dm7wZ4ZLsS-1)XT*lAnkH0ibw z>zEN%>0xwV(Hiira=32z(YMOR#zBcva7G$P*Drt5OkS`lZYO z-bBxu< zM5k@%q07(>=q?o2?u9nDK3qn{Xa(AY>h*xn=sY*Q9u;<`l<;LfZFk0R1?=4XY}>me zt-%sV>b}uMv(p+PyPE`6Z0Blx+n5>r1QyQnwSb9q;|$hU_9&YWCb#2w_)YFg)voKV z?bzbeT!V;o_9i!1fCNq|=Q@Yzqd-G64v7o&xMo~O?sD!)?q#l+Yc2;(+%E1Qw}>~L zx0V+`@#2Bayl9?OoCXKV28x8&&IA85@vRaxRD?AELh!F?Mp(l*xR*i!9|gYzM?@DT z*%IfAUGkt5kj5ZZJ{A~)*Lw|_!F)M6{u~=MB2}QwP^oaO^c!C5&Ux+WE zbr?`(Pr}Tx2nL%SfQ4a+Y;c}^6)VN6*;4j*3`q1jU>3(gvQ)B(!{r>7BuR2OpiojP zc`aem{w-txH>tmLTeAdNNZ^m_v>z0sr9!>4Mv|DXe z*U7^@9u@i&(Ufyi)XRL*rMO?1?#J{qj<_M733gDowh9#GYM3x9xL-})zYpZ-<73ExKFG}yp~cYMW9 zt@_k&TpX7mtvz;7>76C(B|M8v7!|L~DSq=p&bm;%_U(0;p0r!7B+_d*cF(Soaj9YX z8PsFyg^a*-RMDF1*6%oQf#T=k|0hi+IU>I=y2Q7{ICOYR;fTQF*Ny{qr#7L&Fo+}H z%g;-~em*ayca=nW+y6As|Mp&wvyk!lo&x9xx2Z1?$Wc|D)|6a|BGJb zPO@&V%5|6@Xa}iJm~}EEW!-@G@9={I+$>hF_T#S=boXEV12~z2f zh;NQ)c06&B)SsdJ_@;8YT5B3|v7GFWkV$WQ{CbscPjc!}iY6-pr}ww5fdW16N#qYI z%HK(y%F)i&Kh=}zu<15DfavuYzPj{q(}C&lKhM%9M@^^N>lozRG}i;xlixb&w9e8; z!15V@ng(CdjdSJah&%*9U56B{fsu31K5L7O#CDlLXzwic5kDDxnBmZ)9VG7IzsSa) zYX{9x9}vUFcp+#<0vqPu?ejY0rQOY5GT`r;4^|C+CxL!4?`rb;TWGT!(a!u%LZ(e6 z=L&VI)PJ@QJ3w26(!82mnjIs31*&8Hx2+>BkM$I2b;I7=l0wQ|Ux&{pwEB^eg+FJz zp7*@6WnsSt>aV}gK>=HD7kP1xOVZ+BqcY*>W3N`f=hkPwKxLo$l7QnMO>T~ET)cRF zRmRmJxa+Ww{ZIWH_l7qv);B$wP~_mHHBz&2@xb0fy+6oM?8myZ-{plhUjHAlA76(R z&t@e%eTPy#dLzl*8>>c|Emm{c_zScN?vm&yaXtPT`P*VO=cUWE8R>ABg(7{?^bx%M zKVm<=4iCE(`?eB7KypiOMP%ei*NN3FU)R{LSORzLIV=7ix@|;uV)g!(#@@-S!xgMb z)y{M6Uso>1|6FiLR<%Hm@^)2z_ic{-zH%`T>f9)MXIj!yr2J#fw0UPxu%4Pz^t#A0 z;5&oO(Lr;HKt{?tMhXW(%Q#D4JM3@K8cNOi0roaf=ck_1TYex4+(I9q z5)?`Tp@i(gY&LLWuVQo9;p}*J3i}$nj18W$-?8Ovgrjddw?`&)z`;^{J-!n^j3?ol zceK@=AmM9r;B0J>gNRF3ptYrF{iWzTSTz|B*Az zhc?QbB>476Z0_f;AewfRmPWfpdq5j!5(9!sJe%Rf7_S}2Guk{w*BE7tr;NY-RIe?R zsb#$xyblfBcYWx(z}#AYX@~+{#qdQ#u79=pLs}nAg*6JKmZFPg(5hmbfjr!Edt} z%OY1|eNQWzBR$@&t#Z94QlbQ+b%jwL@09UB*Y!k;_o8*f=U`ZM?22Z`!;zGTJ^!}c zd>xiJUoO{}Cb@+kllw*LeH(PckDpcJ7oy>lhV5Zo<@!qa#6n(*h7apU!nT{7=)3tk zc&J%Pgoonp^=^s!w$mOzRsZGOUDQ2(xOsEbx02-v7cY%S%20LxqRkHPHvGWV3*X!< zLC;?2r@+{d`>*$&2RJ9aDlL^hk-n9Fmjbzt9GJ)*!ZuK;E;f?tn(O9cz;bV#^r)RR}A>`SV z%rM=@lfk^buv~XEkQHN_O*i^&L5H&3pA>DC!l=&jVPL(c<6J5eEaiD2loqbyjj^S{WFphCBXG{M)!UN0m6MMBk`{rK?6j6nT&+-Tp;oh{IbL~ki!^WF9lq_9vS%t;Ie#7Jj;{+J~ zz{hGLLl_Hp99u_yKu_2g$`%#9EL{Fql7kCrS9D~0VDxZ$pi-u>gM$k>S}iv)dWT|f zQSSpQjhQdmW`v>^ZaoD`^+QY-?-#^$p{#GSa?#b-Xt2)x+sMvVJ3z`vxt2}K2%HITZvq*TZx7Rpke6S z=o2Uab1&3_d>gH}e4D>2^KB_DFlm{Gklv#Tl(f=p$@HZ|;TFrlgm_x>guYuX2P@aQ z-fG#(9$sp4L1;Iv4`Y>+p2SuDcUut*v;TKn5uD;&;k@I>VOw!7{*$A@1qR$rTrSs} zdlTQoJ;wc$3mR}ax1MW8aipm5AXk(JqIpMofxJ7Ea>_o+b>3Ybc*c9rQ}9RvJ%O3P zQLtP<+9>etmwuon3vx~$r_~8O65*1pMA(yfAj5L>C|EDV(g>|%!^=T6gdZs2U1a3E zH-pweb4QyMvkX$e`V@y^UDPqhYB5@LMU*Q7m7{1P~|;U zWTwBeleaQGI!xPrU3*QY{fy=EOq-dJ86@b|>2D7#HtMX3t(|d205Tl!E zLouryYMV0MAY}ueF=P7&587JVAG)0ZUEa!Svh_|4n(kJ|IKfSUQsDw{zK6#=!2HQX z&RJL;U~XA91zT0%p2zS5HH`Q4H;n9qHcsBad0kH1sFOFs-@48-yw}P&`qvVBo}0e{ ztoHQfZP{;-62l|=Ci8N5;OE|@i;N*>R>XQJ zeJ?YN3X@`#P-}OqTX_1zUt@O{8%aZ4qEi0QZs`*!i*Em!GP*5yVvpT9Bkr6nbXz2| zPig!4KT?rnA%W*FaTklXe^5G-XyrW3jP1x?&fdu0&1Ui0XW7~8x7(r9?>xo19!$Bit0rH5`Mo(MLb1pA&#M}7VpfM4noCy#mB`L#s4;TKTVgaVt+3W z{{ma;zaAWJLcP4X`JpqzAD>bGY~0ct0Ig(C8ygVvB zEqTWMve_dReB%D*YVl_Bta&axIAD0CS41ax8z#ePVvF_!99jU4HFR)@ir_;)z&_oD zcMeba)pTV{gN6rG1+TR|Vx?0zb=wOZMC%0q4~zjA!x)U=RQ*I-Mf*kIq^MLX7FCFv zM1-bYD5kU*vl9FLd~HuD6tAPiioqeRU*ap28Q5#_7jZX%E!LpfV6!P_Dc-agOzt4+ zAgx1ZEh3XvFX0g`yNXc@bFP|l!FJB=KVHzI4L$i0YVsi7{?e4 zfwO=xfePU8|8;?i5v@jdGHKeSvD?vW!i;3K!*<)4@hHC~dD@P<$tUj>B(_GrowN)G zZom-0!W|j7n)2(I1@=v77dnI6ug<1>x^Ihuk{13_=;Xv=3Df^5!lw~MUAQa|Uxt7O zQTD5Acj}B$uMIa$yh>O26mxvygf81UK7_Z#kw7t?Ni};Ft7X)5WM(lkGjB?MtY(g1 zuY1vYpLhsqT!W~Yq!-7{Terwg&*B-8Xw6$bfSlmnuL6B!1%zUE*4FOKL_>bUFJ8VJ%Ixd1iY}d7k;3ch_Rupd@yQt!C!B>H=Ky8a)pWsC7{c-qDm<8C$ z{1ARkw7~a6_WpDmQvlqQAX&o)9S>rg!WJ zr;ODPai|(ORtp=rd^tVjOy)0)wUS53I@>(PTI^PRes5ofm6xqDoGf+u7X2QA8xFMv zD)IH5&wA;{I+LXi(_Bkpn=kJ&zpyQ;P>@N5;*;TUL2|U}a{5TIAP25VrSiQ3V-`YA zb^Q>&(}-lA>T)pupzCa@OIVh$_uTgjOJI`alnawAx3gYnUC0;MH&>U$UhBKirF+_V z0BzE zawG@fv4jrASB$j?wA-X%tcYWA!(bjvtZV}%=?(R|8!PL5zcT@bpzKNFr**bDPZg3tYbYZl-Cx}+kn)wg zrY~^2SN+s&gMn&MS@WiaFXgT0xoPdUh~jCuw1(pgz}*@Hf%~`aaSe|k^l`qN1oT7v zq8s+(g739u#|rrKW64z;-{)VBA4rs*Z#ic9;oK@Z_2%JijMS#dT%am8Yk%B$te~mk zSdsMQp~6v)#W)tC*B%YZ#>`NdQ|0MYfb;b8^ri-*YR7p?=WU$5?z7S+iBB>DGGZi- ze;bAD@2k8j@oSl|uFnv;>tmakiw&?7aZOmp^ z==bV+T-KoNlUg*BlTzT|L;Bhc{~d2+d1nL%c*PGOR=ZkL3le?eIcEx1WxEUci*vk0 zn?=#0qiN^2o;NifEhzY>w~p*64KX&moSa&mlEu2&S@>t--Mhh=9jI;dN(qkOoq2b| z7iVzuzUHN6|8*H~^F+#DCH|`QViUhF;^7-giFfrezuB*RaJL5*?>7vB^QJDNH}JK} zE_mYIkXRLa!<Dm*{0l zfvciJ0t1y?U2*#53+4k~s3FBun~9mdFXq9l`YA0$?;5?l+6ZRC_gtR28sbJz-EgB< z%P+eiS?nTq-0g_4PxQqZ&CWkOI@n~x0k&$uxu9cB=Z(nD-r7cPD|~SJwcUA){IKv+;$hvZSb#cRvL<-mS1r?_Sg=mBW9O4X^g#w zn;qQRuQl#3_Y&sT-!qh$?Bsm#xTS6q*=E@uQ1^eecjfU>RN4Aebyq?Nu@g){WJyFI zfIwIR0R_p@0ha(Fi{L1PkR7s_g@6JL7zM`#P#keYmJydhP(g6eVH6~eFM<-CAd1OE zFer#XB_y^XgnHkp>Qr?>*k<1Qz4_zyZ_XILI``aj&OP_srElMg+*W+=-xg28`}S_2 zb%hUQ`#jtK6LL9eutR9I@S36!rIBk!D+wD?dK!w}N|@cbm36a5*y8Jz#us z<-qmXU(WD*WO|t1Q-93Q?I6|_-tk+(XUpHHTE1ragAvck5neNj7j4dZ==_&6 z0?sc_@LrL;Nmx|4dPOqi3X4*_Y2NI|qkb!xur6ucLod;-PyceTa`^e08_oos8G7Q* zR~GGFocBZL8%Aw;=U})k&SP1J*&F8~VM%k3ZJ0}IGlT_yPFt6|LnsyP?=C!I-;w!r z=;xEa-tzuyN9)HP-{|wvWMr(*Cx3p#Pg*@Byt=k;aZvMLZ8s^0pA(jDC>^fM^$mZl zdzV*|701p+)WP26mHdz;A*(`O3)va+Sx;fG_JUBs(Q8=jk;7Z5&)B=huK4)Vt!qCY z^Vh>WPVWd`Jp2!~H*BLn+TXrv)XE}%#leSPn!WIK?R@PVA}08cslPAd1JG4C*45+9 z<=#;T#h8Q5|ClAD9f*w=V~_m2&DsgKKv;|4shDx15}{L z?eq@ZrCn3L?XyPvku^Ig_Dp>LyxPM%_OD#g_|00WGF)CKzU}|YL&J_uKD=XKWes&m zX`fg9LB!&HTR+`u-xIWb_pmLWZrxVyUHxNY{6~q@GvlKfXB5xIrC-t0-yYsAU0C{+ z@b0&B>muT8N1ONfw7qL5FvI+&HOEe_O6H(Y9)b@=3p;F_3Mm*jBJ_>wgUnxNuYyj$4E$;`eU7 zV=M7*3wTYjJ49Pj&;tkN?dkSz$ro$hU2$Ojfo%t9&5r$_clFz|!QVe6tT3!=m(||i zcC9J594k1!VN}J!q+@+<`*Ct)#i zXyv%AO0<1RpTtR{ZSyAKnCstSp6p#Y>BmW1@S^)0t+yso#-#B{f3c05^quXL?d>?e z=wOc9#RVe%ZoPgX0DrG}vO@gjO`_AIi2gz4KnBDFi-(U9-Gsj*343qU@Mwkj0+%fS}!=#@Y1`76%`-q7VLT^ibU7}4gy0TNT#jL_ zIp0sH?1z_j@A~1l+d7Gf;xVx=CI>$MY!L!Iq;@CjGx}J6t$R~=!+Klbrmo)QKm69C zlNdWt@Yy)Sf3LVN$A6%>eRn(CoK9jw(DAeK*bDXYEy7-Lf!{#s3IF81(udb~NzFxtH4i@LO~zaq^(R zT_InDd>baL`m=r z)48XRgI^GnL8;D$4TV+8O}75FqfN|CVeMIDG(<*`!a8L^=#heE*U4HAfYa$O1ar~>{saeJx=F*I;$geB}EJ;I#eTiD;|V1itR)z8Fkyq z!KI5Sr%E~RYwKyFw?%rV`ON%7S%bN7JH=Jl13xj>wPr8HlXD(rpOTX)jp|^lzpdu{ zQV+8J^JvpLV)qE`6-ksV9u|uQ6`oR^i|exrNh!ilkCI`pl7X`Ltso@^HOi?LDs;-p zRZ5Q|=c0lLZ88cM#dJF>pQyLA^Uw_EdhgSs1{V*|5OAv=rnB-7zkN7T61^qSI+;Yl zqFJcvsbr$QVp#c5Rx7{t$hr9}*;T+Ix)qo}NvvFkQ${>JrOIFQeY47~SS5=jS}eH4 z#;72H9O^9!1Z_sEF0@eik#lF|4|VyvLRg)_aW!d>pV}nPqdJsf$0_~kNDcDN?sg%P zASgjZeS;9}-c}>n*_Jr!p6rz85bWTJmXn1Z!faWyd%n9L#NloiF#_ROs|3;DXL2|*7u>olHflG(dXIY)Icn`5D%D>xYa z3wv#7zbx)(K{Sygs6-203(?~GqfJ6Ip?`T>F4io99Ilk%X43ajXXeswFL;i^7ltC< zH62^C#H+9fy@k7U1{76tQRR39okQz1=riYoY`7pvq!rEcOAZO>Gy)+@qQ^)UF^03W zdt)o;-cYy@7mw4#X1PR>vLLFi1iT_4~qm-EG&t~3|}5% zF8VOu_U91b%J0H0kKZQXhdVstFk<~@xMd%ZK{`?kqzOoG9kKdGNj(M#G)6}f{Gy}> zfDF@-(2h~kGeCyxNTq+2v=_*D9VzM@CAG)h#<%N8!c9?<9Y~mtY@c5zJ^~~{M-m)$ z;w~VAb;LS2-t!kALv*AFNcTbRJheb3n2>}a@tzBS+@Y5$1F{o{VSDJ%c+Ve!V2F14 zXCsh6j5Y&x#D7@4XAF>`I#LQ`ArM1y1CaNDjMRBjL*qT`fZVMk8=K0-+Xv&2j*8&_ zs1X-bVVq7t7Ivhe-{nzCmE71<1;-;;9EEC*EpW;h=*Cf=g1+LD+$^IcF0ew$ade(s zX)2c-8Hg^7KZ@*`MQK$s;umdEjP8Pwk!!=%S11!t38cg!m?v^D7a%GT;;q+I>5^Ss z1Cck=ICIWaM;f10rQXD{AnugRKMc(kC`tXAT9FG3S!^SJ8vCd<=Wr6Aqt+WiQ_%NY zS-beP_K?~v7qrOtQL;qUS>CWc zTlG1txR`Ys_~uZuKv{TwLn|LHgu@VYb&-R}XGm8z8`q9qT)|!5vBPH*$3SY?Q_v z(S<5Fj{yE$3d^8uCx_!8Rr0l%EQCT7oQ_B|vA1O~SfS1(pGM8WU1^fP!a}1xK?E(MmLN%DL9IiMnX}Q>>kmaeHweHej5kXoW)ew42#hfcu+(q zcFG(aSOlnfA)E0M?&VE#jq5ocN2<0Zg1?44*z9IGlMWS75kxiB7gBMGKqbv`7Ntwp zB=!KL!UF<302#;C{MB=uaskh}f~b0C3r4UMH3mkgRdXkI%)tb+fYO@e7G$FmH#5D#9>4QzD{J7|KlJ|DN`^AN(# ztVLcr3aCq|RLaNiOP~nKPHL2E(pipLrN$VArfSJR=53x?QZ)CTi*5)w!Q5qY<=5L*#5Skum{?HaycIiRejJ)^^U2J zPHXL>ah`};HOpz;!^9PsfXG^Q<^>kHmTPY&0Nk%qP zAGcAqa|9u^=IMgn`)nLZc^0*EXiB+1yNkTgP0wT5-l{S`ZOsKyPB{!cr-f&TnoHcv zPQX;ESwxMN83QEtMZ92RW4<31C`ezJ*Uh_3EpFv_2rlx2 zKYa?kID2WcGPAP`_Gwdi*vV*-StD(}cRAK1o`x5DQL3jXWPm%oZT;1SgO_XJ;7Qms zD{&c;-<1N`5Zo+qdGlSx;Wo_aP@JZ~AU{IU|r&%NcKJzQ%BMm8;Ri)w4j&4{e!--WJa@(o~v)=`WUX zUYs==^Sw;{qAfQXC6jpK9P33z0Tjp+6wY>S^;e^W6D1_1gL&kGo{M(9as|kBCB&EP z2c!p3A(H%MTGpsQEpflO#OO9!)+kZKbxiO=)!*2w4cvwZ32y)KlKTMru znF?J!$lMEEwdq^?%rI=6T!%R>b}iFh%eIWqu-3gcq#aX%wzi&zX?tzg*(6ZvdYZ(= zuI=UO$%i-A7Ub~idNxnf)yajHQ+1XgY?miu&lPJ`YzJpaYIZKhOdm;1UGigJm%oQB z#xxn5Qr}6Dhe#R<|UO`Q0V( zUXX;96qmV3SKePL#yTEf`JxIqfHiI1M|Ej>F^{7VJHtk0*TM~z@g*Nd5_DyE%`O&= zoR)+w=41-XrecXl*qCc9;8Y7FVR%fOi#GQW3%$8nyGII`#PIAl;olQ9EhYd>o6&`m zM`_zWm^NL>k|eeyO5piY5`W8@>?J}Sc7f4qYV&e1dSbAIiil2kk)}Apuso0AER=pJ zUo^xwVv|sfqXd#%(SbhalBPxn<^jzzhI@4$KrI-PGcXatZ{B7}Rz0GO7&XeQ<8KO! z&=^7j&Kqfwhk4H>vJJlIX5uzLRfQe8P7Aw95TFAC+0d6LpO@FmJo)P}?o=SmV4 z=GFr|fTt8nJ^Lb(%VGa4ha9>1Y7|})=uoR3>EzH#p;K0;xk@F`aIM;Zv2++3xC>9@ z?EiwwCFqe7LJT>SwV(O#iAdru%F&@TG-h?C+(ztDU?Y{AdDj1!Stv9tA`E?f}B|$q`1ylCVD{OaaUGYH@!*zpo0|6 zgHcv6&BN&jl)WflU#z=>V;sA0-c~kr;*~4XnLS!GBVpsglwj4!-jLFoxbD1GF~j2m zTqa({F9bus~EQt8Q*Yn*22t8!B%wZmz9|3=re|kCKdRWVGxB9 z&(O79AD!`%sRjCaE32`r#n-q~kb(*b6Zc61vIWLrXCYx1oX<-PzgwVm8%9AsoUjw; zF1lu!H#Hm9C;-hZkRb;@jaxn);ZyiYh@+^>EIx_W#Oz}M+zCfbY zQoXihihl>vL+44zs}O5}@Nqefr#7cTJZ?ftA^8^|x9X)f1F;Pg=oTHB4>^N@7;<(2 z2{j?vf4)N+X+q`$8ErzWUCPC9AboYo`(b+~5X1IL)V=_SVSDAKJET9Er+l+6Kh1%bJxUehP#mLMycyR@4A7toSB0SUdxylg?uu9xO`3 z1?r|FcIa~pkPsaygFdl9+^m4@j{q^+%m4F4PlpMK0nei*WHfk|ni23kX+q|s_T@l~ z+P4Gw(1e7-(o;YTD}DiwWrTaFUN@JELxC7Rj6taxKn(x<0{_efV)$nO{JacEPu+I= zTbbe}Ab~ov$~#yr2f{~FG(RVxlr_vfp6#Q{#oj>p(2I))`b-DXPe*z^ULh_8!UuIU zo(F+^2!#8rhLoaI9gzMyvI;z1M`}l9G@k9?nE-^3(`ZO4bbc6!p>rva7lGWY^OQp8 zcYzp|ZU%A?h>-!TwV7fQkQ-1+>)jh6$8(f>1_(m@^ajE!Kdts!=o1da(76(lX8|!J zk4C9yfEZDC6YAOo#Apla@5{vxfEd}vziYYp4G_bJb|Av-0tM@}r=pen0x?>-6iB!U zSp#IM3E2)L--PT3vdo0k0$FcDtg}+YohGCkkO~tL3goy6nF`zK4pUnIx!HuQ2NG&R zb^)1aLcRf#X+o?cg2g2!BnZeF6EYgednRNmkO~u03gl-KvL1+aw7bp&&^kSU^wB-P z3p{opMw=#pX9kcUou>@(vj~WhMQee)48(|4>)V;)E)!2LAm0KpB$q;RBM>7tHUqhF zj5|^ZWC##LLp$V5G4WWREEkJSJgMM$$;2}NJnsWBdP)M2?|>L~6(J6rfeg?s-3TQ3 zPE#)cG8u@GkxG#z96(0urL5h`#lHYCvR^2WAAuOTcRw^uiB*LUo=ZAX9-1){)IXmI2|zGMXGaPA}!^{DQ z!IJ=FrU|h_@+u&F*iw`H6nMS_V&vXZv}wC2_Yrn8kbyvq-0}?~JOPN2MXh1MVy+1( zgXCpE43CvT=bb=|tW^ogM}P$B`jkRL!7k7U9XSs7(wo_#>v>`F$4#5rjLu>%i5Cm`63h^ZS6f^*G3B zGRY}LsgS$eNDai8uMPmR7>F_ZDn*uf4TzDEVvy%Q1`?pRU;;`V0%GK!Ufs*Z z79d6>Bmn7ysl0&%A)+P$>7vUS4I~qYF~XKY@}oeE@w60jUI$`Wx*fj z8+O$G1rQ@2mcn+A2?E`zTUrUk4#cf4pI~t@kSLwU|6I9P2IO}-QVOKUL;?3hwdR&m zX!r;aK7_1kH~>FW`UHq!S1ovc1#+*>Q-r!Au+B4Lz6{75Aft7jRLI!|#As9d>FHt( zknuW?{|&)npIGuJ(2XE0Vb#9V3IFBqPkH{v zeSmW%eaml`CFY&0rk-{<0qY;m6+zK2S94|c1hJzwNksyu_J18174#cf-7l`{6M*+8T z3}^yqB4`qbd)Q=994H=?07?Yi2buz!3c4S}k@f)SK@i8-bWjp#1}GUc6O;l<1*L(~ zK^dS-P!?zwXf`Mtlmp5I<$>}+4}l6mg`gr(F=!5`1T+^k5A-nT5zu_l0?p>epe*(P%+6a0T^cv`O&>NsXgEoQQ1Z@Uw0lfu!8}trnD`*?& zUC?&W4$ym`zkqgv-UodE`YUJ`=x?A8K_7v3gZ6+v2JHovfj$BK9rP*azd+@n&p`V? z{F#RBs{*0oME&mLXPd|QpTr5I9LiP@W4LI5aOrglrKzvf&Oga}i-iGbojDXBPJ70^ z9KL6AmE%L0x6_={5@e^I#5v=lpSx4OjY4|kLCPjvapXzKJ~YUavQFQ|n}0rN`t(iR zY0zzibDkl7ly%OBvLetSaLlfM3XSMZX*gvOLLCJ0K|YQ!ZLNFFhQ_X%51l(26wKj};3K1fN_*(s;62?x`$7cH1|HePU zA++{FxMe&Py9a-Biuup_ghQPKCET(e8jir>}bu2eo*=WV@EhnJ6MoRxd7uTr!d!)nfp zd0ySl(#Xe!>I+-@Su>Trxzu;!kXbvGm1k~c?340>$U1TK+D?{EXXQj}pU2jToBe!m59l;aIVq=>Ry^!6 zqtez)_y*lSzCxUS=UMsL4n$u?2F?j=CcG%UDy~9|K7*9Kmwy-2>%vJnyFT2~ZWBg_ zZe2QV3B2V=zYV^98(#Ij>BfFHii5=$-aLd6^M#JVC*|5jjta3;e}NWu>9_zt*zUHt zLR@{y_x;nTIFRU@lk%Dmhx?We7O28mr+!OQIox*?rZSE<{CMR3@}%r2I~yV%n<+dc zwnUEUX`S_j*geH5(8Krkv48h~c<_b$gO+%99D7nOx-d4}@-@Qtly6tx1@^JGAc}+1 R7xu>0Eoh1V|NN^S_;2zGol5`! literal 0 HcmV?d00001 From ed14181cc1cefdbc64d54d0845e61dac44c7b14a Mon Sep 17 00:00:00 2001 From: edgimar Date: Sun, 21 May 2017 11:18:56 -0400 Subject: [PATCH 0879/1387] add test for preserved intermediate folder permissions (#2477) This tests whether the permissions metadata is preserved when a folder is excluded but still recursed into to find a matching file in a subfolder. --- src/borg/testsuite/archiver.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index bf25eb7d..30b5566c 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1002,6 +1002,31 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_not_in('input/x/a', output) self.assert_in('A input/y/foo_y', output) + def test_create_pattern_intermediate_folders_first(self): + """test that intermediate folders appear first when patterns exclude a parent folder but include a child""" + self.patterns_file_path2 = os.path.join(self.tmpdir, 'patterns2') + with open(self.patterns_file_path2, 'wb') as fd: + fd.write(b'+ input/x/a\n+ input/x/b\n- input/x*\n') + + self.cmd('init', '--encryption=repokey', self.repository_location) + + self.create_regular_file('x/a/foo_a', size=1024 * 80) + self.create_regular_file('x/b/foo_b', size=1024 * 80) + with changedir('input'): + self.cmd('create', '--patterns-from=' + self.patterns_file_path2, + self.repository_location + '::test', '.') + + # list the archive and verify that the "intermediate" folders appear before + # their contents + out = self.cmd('list', '--format', '{type} {path}{NL}', self.repository_location + '::test') + out_list = out.splitlines() + + self.assert_in('d x/a', out_list) + self.assert_in('d x/b', out_list) + + assert out_list.index('d x/a') < out_list.index('- x/a/foo_a') + assert out_list.index('d x/b') < out_list.index('- x/b/foo_b') + def test_extract_pattern_opt(self): self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file1', size=1024 * 80) From 384d7635a412462a807d050deda408c780acbad7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 21 May 2017 17:07:58 +0200 Subject: [PATCH 0880/1387] docs: add systemd warning regarding placeholders --- docs/usage/help.rst.inc | 10 ++++++++++ src/borg/archiver.py | 12 +++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index 8b3f8ded..9082e507 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -206,6 +206,16 @@ Examples:: borg create /path/to/repo::{hostname}-{now:%Y-%m-%d_%H:%M:%S} ... borg prune --prefix '{hostname}-' ... +.. note:: + systemd uses a difficult, non-standard syntax for command lines in unit files (refer to + the `systemd.unit(5)` manual page). + + When invoking borg from unit files, pay particular attention to escaping, + especially when using the now/utcnow placeholders, since systemd performs its own + %-based variable replacement even in quoted text. To avoid interference from systemd, + double all percent signs (``{hostname}-{now:%Y-%m-%d_%H:%M:%S}`` + becomes ``{hostname}-{now:%%Y-%%m-%%d_%%H:%%M:%%S}``). + .. _borg_compression: borg help compression diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 79f89e97..7a364201 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2009,7 +2009,17 @@ class Archiver: borg create /path/to/repo::{hostname}-{user}-{utcnow} ... borg create /path/to/repo::{hostname}-{now:%Y-%m-%d_%H:%M:%S} ... - borg prune --prefix '{hostname}-' ...\n\n''') + borg prune --prefix '{hostname}-' ... + + .. note:: + systemd uses a difficult, non-standard syntax for command lines in unit files (refer to + the `systemd.unit(5)` manual page). + + When invoking borg from unit files, pay particular attention to escaping, + especially when using the now/utcnow placeholders, since systemd performs its own + %-based variable replacement even in quoted text. To avoid interference from systemd, + double all percent signs (``{hostname}-{now:%Y-%m-%d_%H:%M:%S}`` + becomes ``{hostname}-{now:%%Y-%%m-%%d_%%H:%%M:%%S}``).\n\n''') helptext['compression'] = textwrap.dedent(''' Compression is lz4 by default. If you want something else, you have to specify what you want. From fe2d648465fde8de108783fef7f163d8a7b74220 Mon Sep 17 00:00:00 2001 From: schuft69 Date: Mon, 22 May 2017 08:23:08 +0200 Subject: [PATCH 0881/1387] Update README.rst --- README.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/README.rst b/README.rst index ba7c735f..d5578567 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,5 @@ |screencast| -.. highlight:: bash What is BorgBackup? ------------------- From 0bcb8c2a3908029d255701a647b523ba29d4e155 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 19 May 2017 18:40:59 +0200 Subject: [PATCH 0882/1387] faq: rewrote IntegrityError answer --- docs/changes.rst | 3 ++ docs/faq.rst | 91 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index d8d9a351..e52b23e5 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,3 +1,6 @@ + +.. _important_notes: + Important notes =============== diff --git a/docs/faq.rst b/docs/faq.rst index ae93039a..8424d91f 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -194,41 +194,80 @@ repo. It will then be able to check using CRCs and HMACs. I get an IntegrityError or similar - what now? ---------------------------------------------- -The first step should be to check whether it's a problem with the disk drive, -IntegrityErrors can be a sign of drive failure or other hardware issues. +A single error does not necessarily indicate bad hardware or a Borg +bug. All hardware exhibits a bit error rate (BER). Hard drives are typically +specified as exhibiting less than one error every 12 to 120 TB +(one bit error in 10e14 to 10e15 bits). The specification is often called +*unrecoverable read error rate* (URE rate). -Using the smartmontools one can retrieve self-diagnostics of the drive in question -(where the repository is located, use *findmnt*, *mount* or *lsblk* to find the -*/dev/...* path of the drive):: +Apart from these very rare errors there are two main causes of errors: - # smartctl -a /dev/sdSomething +(i) Defective hardware: described below. +(ii) Bugs in software (Borg, operating system, libraries): + Ensure software is up to date. + Check whether the issue is caused by any fixed bugs described in :ref:`important_notes`. -Attributes that are a typical cause of data corruption are *Offline_Uncorrectable*, -*Current_Pending_Sector*, *Reported_Uncorrect*. A high *UDMA_CRC_Error_Count* usually -indicates a bad cable. If the *entire drive* is failing, then all data should be copied -off it as soon as possible. -Some drives log IO errors, which are also logged by the system (refer to the journal/dmesg). -IO errors that impact only the filesystem can go unnoticed, since they are not reported -to applications (e.g. Borg), but can still corrupt data. +.. rubric:: Finding defective hardware -If any of these are suspicious, a self-test is recommended:: +.. note:: - # smartctl -t long /dev/sdSomething + Hardware diagnostics are operating system dependent and do not + apply universally. The commands shown apply for popular Unix-like + systems. Refer to your operating system's manual. -Running ``fsck`` if not done already might yield further insights. +Checking hard drives + Find the drive containing the repository and use *findmnt*, *mount* or *lsblk* + to learn the device path (typically */dev/...*) of the drive. + Then, smartmontools can retrieve self-diagnostics of the drive in question:: + + # smartctl -a /dev/sdSomething + + The *Offline_Uncorrectable*, *Current_Pending_Sector* and *Reported_Uncorrect* + attributes indicate data corruption. A high *UDMA_CRC_Error_Count* usually + indicates a bad cable. + + I/O errors logged by the system (refer to the system journal or + dmesg) can point to issues as well. I/O errors only affecting the + file system easily go unnoticed, since they are not reported to + applications (e.g. Borg), while these errors can still corrupt data. + + Drives can corrupt some sectors in one event, while remaining + reliable otherwise. Conversely, drives can fail completely with no + advance warning. If in doubt, copy all data from the drive in + question to another drive -- just in case it fails completely. + + If any of these are suspicious, a self-test is recommended:: + + # smartctl -t long /dev/sdSomething + + Running ``fsck`` if not done already might yield further insights. + +Checking memory + Intermittent issues, such as ``borg check`` finding errors + inconsistently between runs, are frequently caused by bad memory. + + Run memtest86+ (or an equivalent memory tester) to verify that + the memory subsystem is operating correctly. + +Checking processors + Processors rarely cause errors. If they do, they are usually overclocked + or otherwise operated outside their specifications. We do not recommend to + operate hardware outside its specifications for productive use. + + Tools to verify correct processor operation include Prime95 (mprime), linpack, + and the `Intel Processor Diagnostic Tool + `_ + (applies only to Intel processors). + +.. rubric:: Repairing a damaged repository + +With any defective hardware found and replaced, the damage done to the repository +needs to be ascertained and fixed. :ref:`borg_check` provides diagnostics and ``--repair`` options for repositories with -issues. We recommend to first run without ``--repair`` to assess the situation and -if the found issues / proposed repairs sound right re-run it with ``--repair`` enabled. - -When errors are intermittent the cause might be bad memory, running memtest86+ or a similar -test is recommended. - -A single error does not indicate bad hardware or a Borg bug -- all hardware has a certain -bit error rate (BER), for hard drives this is typically specified as less than one error -every 12 to 120 TB (one bit error in 10e14 to 10e15 bits) and often called -*unrecoverable read error rate* (URE rate). +issues. We recommend to first run without ``--repair`` to assess the situation. +If the found issues and proposed repairs seem right, re-run "check" with ``--repair`` enabled. Security ######## From 8e24ddae062df0cc014db3a8de25259c5fb4cb2d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 1 Mar 2017 19:26:20 +0100 Subject: [PATCH 0883/1387] increase DEFAULT_SEGMENTS_PER_DIR to 2000 --- src/borg/constants.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/borg/constants.py b/src/borg/constants.py index f7cb11c9..b60627f5 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -31,8 +31,7 @@ DEFAULT_MAX_SEGMENT_SIZE = 500 * 1024 * 1024 # the header, and the total size was set to 20 MiB). MAX_DATA_SIZE = 20971479 -# A few hundred files per directory to go easy on filesystems which don't like too many files per dir (NTFS) -DEFAULT_SEGMENTS_PER_DIR = 500 +DEFAULT_SEGMENTS_PER_DIR = 2000 CHUNK_MIN_EXP = 19 # 2**19 == 512kiB CHUNK_MAX_EXP = 23 # 2**23 == 8MiB From 691c6adc045c94ae42783c344e49f70914269fe5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 20 May 2017 21:28:50 +0200 Subject: [PATCH 0884/1387] docs/data structures: detail explanation of compaction --- docs/internals/data-structures.rst | 50 ++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index d15858de..169b78f5 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -122,11 +122,49 @@ such obsolete entries is called sparse, while a segment containing no such entri Since writing a ``DELETE`` tag does not actually delete any data and thus does not free disk space any log-based data store will need a -compaction strategy. +compaction strategy (somewhat analogous to a garbage collector). +Borg uses a simple forward compacting algorithm, +which avoids modifying existing segments. +Compaction runs when a commit is issued (unless the :ref:`append_only_mode` is active). +One client transaction can manifest as multiple physical transactions, +since compaction is transacted, too, and Borg does not distinguish between the two:: -Borg tracks which segments are sparse and does a forward compaction -when a commit is issued (unless the :ref:`append_only_mode` is -active). + Perspective| Time --> + -----------+-------------- + Client | Begin transaction - Modify Data - Commit | (done) + Repository | Begin transaction - Modify Data - Commit | Compact segments - Commit | (done) + +The compaction algorithm requires two inputs in addition to the segments themselves: + +(i) Which segments are sparse, to avoid scanning all segments (impractical). + Further, Borg uses a conditional compaction strategy: Only those + segments that exceed a threshold sparsity are compacted. + + To implement the threshold condition efficiently, the sparsity has + to be stored as well. Therefore, Borg stores a mapping ``(segment + id,) -> (number of sparse bytes,)``. + + The 1.0.x series used a simpler non-conditional algorithm, + which only required the list of sparse segments. Thus, + it only stored a list, not the mapping described above. +(ii) Each segment's reference count, which indicates how many live objects are in a segment. + This is not strictly required to perform the algorithm. Rather, it is used to validate + that a segment is unused before deleting it. If the algorithm is incorrect, or the reference + count was not accounted correctly, then an assertion failure occurs. + +These two pieces of information are stored in the hints file (`hints.N`) +next to the index (`index.N`). + +When loading a hints file, Borg checks the version contained in the file. +The 1.0.x series writes version 1 of the format (with the segments list instead +of the mapping, mentioned above). Since Borg 1.0.4, version 2 is read as well. +The 1.1.x series writes version 2 of the format and reads either version. +When reading a version 1 hints file, Borg 1.1.x will +read all sparse segments to determine their sparsity. + +This process may take some time if a repository is kept in the append-only mode, +which causes the number of sparse segments to grow. Repositories not in append-only +mode have no sparse segments in 1.0.x, since compaction is unconditional. Compaction processes sparse segments from oldest to newest; sparse segments which don't contain enough deleted data to justify compaction are skipped. This @@ -135,8 +173,8 @@ a couple kB were deleted in a segment. Segments that are compacted are read in entirety. Current entries are written to a new segment, while superseded entries are omitted. After each segment an intermediary -commit is written to the new segment, data is synced and the old segment is deleted -- -freeing disk space. +commit is written to the new segment. Then, the old segment is deleted +(asserting that the reference count diminished to zero), freeing disk space. (The actual algorithm is more complex to avoid various consistency issues, refer to the ``borg.repository`` module for more comments and documentation on these issues.) From f8c63f9a6643faa6b83bcf066a93941ff8c9e226 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 22 May 2017 13:11:00 +0200 Subject: [PATCH 0885/1387] docs/data structures: add simple example compaction run --- docs/internals/compaction.png | Bin 0 -> 774962 bytes docs/internals/compaction.vsd | Bin 0 -> 190464 bytes docs/internals/data-structures.rst | 6 ++++++ 3 files changed, 6 insertions(+) create mode 100644 docs/internals/compaction.png create mode 100644 docs/internals/compaction.vsd diff --git a/docs/internals/compaction.png b/docs/internals/compaction.png new file mode 100644 index 0000000000000000000000000000000000000000..927ae0b21507e3cf274d221c636fdc103d9641eb GIT binary patch literal 774962 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 z�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®*=q!q_l;EbO<~k?@Y(#m? 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Ǚ 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

e-d6Ek9CzEs>C6 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

1m3&l^DcS@|M9z@Ds4Pq18WVk~VTI*` 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 DNoWHe literal 0 HcmV?d00001 diff --git a/docs/internals/compaction.vsd b/docs/internals/compaction.vsd new file mode 100644 index 0000000000000000000000000000000000000000..73cc0b061a2164f630f674589fe90db84352d066 GIT binary patch literal 190464 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

*p{zY*36V0!ONICjBZL5y!( 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

>b3q3Yi0wmaKUXW%aZ!+3P_b|S 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 Date: Mon, 22 May 2017 13:29:38 +0200 Subject: [PATCH 0886/1387] mount: do pre-mount checks before opening repository --- src/borg/archiver.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 7a364201..bc03a7f3 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1231,22 +1231,24 @@ class Archiver: def do_mount(self, args): """Mount archive or an entire repository as a FUSE filesystem""" + # Perform these checks before opening the repository and asking for a passphrase. + try: import borg.fuse except ImportError as e: self.print_error('borg mount not available: loading fuse support failed [ImportError: %s]' % str(e)) return self.exit_code + if not os.path.isdir(args.mountpoint) or not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK): + self.print_error('%s: Mountpoint must be a writable directory' % args.mountpoint) + return self.exit_code + return self._do_mount(args) @with_repository() def _do_mount(self, args, repository, manifest, key): from .fuse import FuseOperations - if not os.path.isdir(args.mountpoint) or not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK): - self.print_error('%s: Mountpoint must be a writable directory' % args.mountpoint) - return self.exit_code - with cache_if_remote(repository) as cached_repo: operations = FuseOperations(key, repository, manifest, args, cached_repo) logger.info("Mounting filesystem") From b484c79bc26c7267d81495f65e519b6f70e7c311 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 18 May 2017 02:02:51 +0200 Subject: [PATCH 0887/1387] document follow_symlinks requirements, check libc, fixes #2507 --- docs/installation.rst | 15 +++++++++++++++ src/borg/archiver.py | 3 ++- src/borg/helpers.py | 10 ++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 3ae5bc8c..8897fc11 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -54,6 +54,21 @@ manages data. .. _locking: https://en.wikipedia.org/wiki/File_locking#Lock_files +(G)LIBC requirements +-------------------- + +Borg uses some filesytem functions from Python's `os` standard library module +with `follow_symlinks=False`. These are implemented since quite a while with +the non-symlink-following (g)libc functions like e.g. `lstat` or `lutimes` +(not: `stat` or `utimes`). + +Some stoneage systems (like RHEL/CentOS 5) and also Python interpreter binaries +compiled to be able to run on such systems (like Python installed via Anaconda) +might miss these functions and Borg won't be able to work correctly. +This issue will be detected early and Borg will abort with a fatal error. + +For the Borg binaries, there are additional (g)libc requirements, see below. + .. _distribution-package: Distribution Package diff --git a/src/borg/archiver.py b/src/borg/archiver.py index bc03a7f3..97f4425a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -54,7 +54,7 @@ from .helpers import get_cache_dir from .helpers import Manifest from .helpers import hardlinkable from .helpers import StableDict -from .helpers import check_extension_modules +from .helpers import check_python, check_extension_modules from .helpers import dir_is_tagged, is_slow_msgpack, yes, sysinfo from .helpers import log_multi from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm @@ -3829,6 +3829,7 @@ class Archiver: return args def prerun_checks(self, logger): + check_python() check_extension_modules() selftest(logger) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index a93ba710..cd481dc0 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -114,6 +114,16 @@ class InvalidPlaceholder(PlaceholderError): """Invalid placeholder "{}" in string: {}""" +class PythonLibcTooOld(Error): + """FATAL: this Python was compiled for a too old (g)libc and misses required functionality.""" + + +def check_python(): + required_funcs = {os.stat, os.utime} + if not os.supports_follow_symlinks.issuperset(required_funcs): + raise PythonLibcTooOld + + def check_extension_modules(): from . import platform, compress, item if hashindex.API_VERSION != '1.1_01': From 094376a8ad23c218defcc69d601be0b014965243 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 18 May 2017 02:37:54 +0200 Subject: [PATCH 0888/1387] require and use chown with follow_symlinks=False should be equivalent to using os.lchown() before. --- src/borg/archive.py | 2 +- src/borg/helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 96a9070c..9954eb70 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -674,7 +674,7 @@ Utilization of max. archive size: {csize_max:.0%} if fd: os.fchown(fd, uid, gid) else: - os.lchown(path, uid, gid) + os.chown(path, uid, gid, follow_symlinks=False) except OSError: pass if fd: diff --git a/src/borg/helpers.py b/src/borg/helpers.py index cd481dc0..b4090da9 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -119,7 +119,7 @@ class PythonLibcTooOld(Error): def check_python(): - required_funcs = {os.stat, os.utime} + required_funcs = {os.stat, os.utime, os.chown} if not os.supports_follow_symlinks.issuperset(required_funcs): raise PythonLibcTooOld From efec00b39cb9be6193c368f9c3c09cdc3a5811a9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 18 May 2017 02:44:00 +0200 Subject: [PATCH 0889/1387] use stat with follow_symlinks=False should be equivalent to using os.lstat() before. --- src/borg/archive.py | 2 +- src/borg/archiver.py | 4 ++-- src/borg/helpers.py | 2 +- src/borg/testsuite/__init__.py | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 9954eb70..8337a974 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -569,7 +569,7 @@ Utilization of max. archive size: {csize_max:.0%} path = os.path.join(dest, item.path) # Attempt to remove existing files, ignore errors on failure try: - st = os.lstat(path) + st = os.stat(path, follow_symlinks=False) if stat.S_ISDIR(st.st_mode): os.rmdir(path) else: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 97f4425a..8496e1d0 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -436,7 +436,7 @@ class Archiver: continue path = os.path.normpath(path) try: - st = os.lstat(path) + st = os.stat(path, follow_symlinks=False) except OSError as e: self.print_warning('%s: %s', path, e) continue @@ -498,7 +498,7 @@ class Archiver: """ if st is None: with backup_io('stat'): - st = os.lstat(path) + st = os.stat(path, follow_symlinks=False) recurse_excluded_dir = False if not matcher.match(path): diff --git a/src/borg/helpers.py b/src/borg/helpers.py index b4090da9..3367feaf 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1758,7 +1758,7 @@ class GenericDirEntry: def stat(self, follow_symlinks=True): assert not follow_symlinks - return os.lstat(self.path) + return os.stat(self.path, follow_symlinks=follow_symlinks) def _check_type(self, type): st = self.stat(False) diff --git a/src/borg/testsuite/__init__.py b/src/borg/testsuite/__init__.py index 38f5d4ab..e6af9a29 100644 --- a/src/borg/testsuite/__init__.py +++ b/src/borg/testsuite/__init__.py @@ -67,7 +67,7 @@ def are_symlinks_supported(): with unopened_tempfile() as filepath: try: os.symlink('somewhere', filepath) - if os.lstat(filepath) and os.readlink(filepath) == 'somewhere': + if os.stat(filepath, follow_symlinks=False) and os.readlink(filepath) == 'somewhere': return True except OSError: pass @@ -109,7 +109,7 @@ def is_utime_fully_supported(): open(filepath, 'w').close() try: os.utime(filepath, (1000, 2000), follow_symlinks=False) - new_stats = os.lstat(filepath) + new_stats = os.stat(filepath, follow_symlinks=False) if new_stats.st_atime == 1000 and new_stats.st_mtime == 2000: return True except OSError as err: @@ -158,8 +158,8 @@ class BaseTestCase(unittest.TestCase): for filename in diff.common: path1 = os.path.join(diff.left, filename) path2 = os.path.join(diff.right, filename) - s1 = os.lstat(path1) - s2 = os.lstat(path2) + s1 = os.stat(path1, follow_symlinks=False) + s2 = os.stat(path2, follow_symlinks=False) # Assume path2 is on FUSE if st_dev is different fuse = s1.st_dev != s2.st_dev attrs = ['st_uid', 'st_gid', 'st_rdev'] From 6f81d24324a4300351e7560608d6b8225c44dc28 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 18 May 2017 03:05:19 +0200 Subject: [PATCH 0890/1387] remove unused no_lchflags_because --- src/borg/testsuite/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/borg/testsuite/__init__.py b/src/borg/testsuite/__init__.py index e6af9a29..08e6db25 100644 --- a/src/borg/testsuite/__init__.py +++ b/src/borg/testsuite/__init__.py @@ -30,13 +30,11 @@ except ImportError: raises = None has_lchflags = hasattr(os, 'lchflags') or sys.platform.startswith('linux') -no_lchlfags_because = '' if has_lchflags else '(not supported on this platform)' try: with tempfile.NamedTemporaryFile() as file: platform.set_flags(file.name, stat.UF_NODUMP) except OSError: has_lchflags = False - no_lchlfags_because = '(the file system at %s does not support flags)' % tempfile.gettempdir() try: import llfuse From 60df56bd11f238cf7a534740ecaf50288a3244a7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 22 May 2017 18:22:09 +0200 Subject: [PATCH 0891/1387] README: how to help the project --- README.rst | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index d5578567..9bfdd40d 100644 --- a/README.rst +++ b/README.rst @@ -115,6 +115,16 @@ Now doing another backup, just to show off the great deduplication:: For a graphical frontend refer to our complementary project `BorgWeb `_. +Helping, Donations and Bounties +------------------------------- + +Your help is always welcome! +Spread the word, give feedback, help with documentation, testing or development. + +You can also give monetary support to the project, see there for details: + +https://borgbackup.readthedocs.io/en/stable/support.html#bounties-and-fundraisers + Links ----- @@ -122,9 +132,8 @@ Links * `Releases `_, `PyPI packages `_ and `ChangeLog `_ -* `GitHub `_, - `Issue Tracker `_ and - `Bounties & Fundraisers `_ +* `GitHub `_ and + `Issue Tracker `_. * `Web-Chat (IRC) `_ and `Mailing List `_ * `License `_ From 7284413e3f25f9616fdba0b7e83db26068b4f76a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 22 May 2017 17:30:45 +0200 Subject: [PATCH 0892/1387] docs: borg_domain for custom priority index entries --- docs/borg_domain.py | 102 ++++++++++++++++++++++ docs/conf.py | 14 ++- docs/usage/benchmark_crud.rst.inc | 2 + docs/usage/break-lock.rst.inc | 2 + docs/usage/change-passphrase.rst.inc | 2 + docs/usage/check.rst.inc | 2 + docs/usage/create.rst.inc | 2 + docs/usage/delete.rst.inc | 2 + docs/usage/diff.rst.inc | 2 + docs/usage/export-tar.rst.inc | 2 + docs/usage/extract.rst.inc | 2 + docs/usage/info.rst.inc | 2 + docs/usage/init.rst.inc | 2 + docs/usage/key_change-passphrase.rst.inc | 2 + docs/usage/key_export.rst.inc | 2 + docs/usage/key_import.rst.inc | 2 + docs/usage/key_migrate-to-repokey.rst.inc | 2 + docs/usage/list.rst.inc | 2 + docs/usage/mount.rst.inc | 2 + docs/usage/prune.rst.inc | 2 + docs/usage/recreate.rst.inc | 2 + docs/usage/rename.rst.inc | 2 + docs/usage/serve.rst.inc | 2 + docs/usage/umount.rst.inc | 2 + docs/usage/upgrade.rst.inc | 2 + docs/usage/with-lock.rst.inc | 2 + setup.py | 1 + 27 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 docs/borg_domain.py diff --git a/docs/borg_domain.py b/docs/borg_domain.py new file mode 100644 index 00000000..625e8140 --- /dev/null +++ b/docs/borg_domain.py @@ -0,0 +1,102 @@ +from sphinx import addnodes +from sphinx.domains import Domain, ObjType +from sphinx.locale import l_, _ +from sphinx.directives import ObjectDescription +from sphinx.domains.std import ws_re + + +class BorgObject(ObjectDescription): + indextemplate = l_('%s') + parse_node = None # type: Callable[[GenericObject, BuildEnvironment, unicode, addnodes.desc_signature], unicode] # NOQA + + def handle_signature(self, sig, signode): + # type: (unicode, addnodes.desc_signature) -> unicode + pass + + def add_target_and_index(self, name, sig, signode): + # type: (str, str, addnodes.desc_signature) -> None + # ^ ignore this one, don't insert any markup. + # v- the human text v- the target name + # "borg key change-passphrase" -> "borg-key-change-passphrase" + del name # ignored + targetname = sig.replace(' ', '-') + if self.indextemplate: + colon = self.indextemplate.find(':') + if colon != -1: + indextype = self.indextemplate[:colon].strip() + indexentry = self.indextemplate[colon + 1:].strip() % (sig,) + else: + indextype = 'single' + indexentry = self.indextemplate % (sig,) + self.indexnode['entries'].append((indextype, indexentry, targetname, '', None)) + self.env.domaindata['borg']['objects'][targetname] = self.env.docname, self.objtype, sig + + def run(self): + super().run() + return [self.indexnode] + + +class BorgCommand(BorgObject): + """ + Inserts an index entry and an anchor for a borg command. + + For example, the following snippet creates an index entry about the "borg foo-and-bar" + command as well as a "borg-foo-and-bar" anchor (id). + + .. borg:command:: borg foo-and-bar + """ + + indextemplate = l_('%s (command)') + + +class BorgEnvVar(BorgObject): + """ + Inserts an index entry and an anchor for an environment variable. + (Currently not used) + """ + + indextemplate = l_('%s (environment variable)') + + +class BorgDomain(Domain): + """Land of the Borg.""" + name = 'borg' + label = 'Borg' + object_types = { + 'command': ObjType(l_('command')), + 'env_var': ObjType(l_('env_var')), + } + directives = { + 'command': BorgCommand, + 'env_var': BorgEnvVar, + } + roles = {} + initial_data = { + 'objects': {}, # fullname -> docname, objtype + } + + def clear_doc(self, docname): + # required for incremental builds + try: + del self.data['objects'][docname] + except KeyError: + pass + + def merge_domaindata(self, docnames, otherdata): + # needed due to parallel_read_safe + for fullname, (docname, objtype, sig) in otherdata['objects'].items(): + if docname in docnames: + self.data['objects'][fullname] = (docname, objtype, sig) + + def get_objects(self): + for refname, (docname, objtype, sig) in list(self.data['objects'].items()): + yield sig, sig, objtype, docname, refname, 1 + + +def setup(app): + app.add_domain(BorgDomain) + return { + 'version': 1, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/docs/conf.py b/docs/conf.py index dc40f2a0..9a541ac1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,6 +16,7 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. import sys, os sys.path.insert(0, os.path.abspath('../src')) +sys.path.insert(0, os.path.abspath('.')) from borg import __version__ as sw_version @@ -26,9 +27,7 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] +# Extensions are defined at the end of this file. # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -241,7 +240,14 @@ man_pages = [ 1), ] -extensions = ['sphinx.ext.extlinks', 'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] +extensions = [ + 'sphinx.ext.extlinks', + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', + 'borg_domain', +] extlinks = { 'issue': ('https://github.com/borgbackup/borg/issues/%s', '#'), diff --git a/docs/usage/benchmark_crud.rst.inc b/docs/usage/benchmark_crud.rst.inc index d47e8d62..604c8795 100644 --- a/docs/usage/benchmark_crud.rst.inc +++ b/docs/usage/benchmark_crud.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg benchmark crud + .. _borg_benchmark_crud: borg benchmark crud diff --git a/docs/usage/break-lock.rst.inc b/docs/usage/break-lock.rst.inc index 1b8e5915..24a97303 100644 --- a/docs/usage/break-lock.rst.inc +++ b/docs/usage/break-lock.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg break-lock + .. _borg_break-lock: borg break-lock diff --git a/docs/usage/change-passphrase.rst.inc b/docs/usage/change-passphrase.rst.inc index b0a6c2bb..7763745c 100644 --- a/docs/usage/change-passphrase.rst.inc +++ b/docs/usage/change-passphrase.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg change-passphrase + .. _borg_change-passphrase: borg change-passphrase diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index 56bc42c8..73b3ab8e 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg check + .. _borg_check: borg check diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 6b7006cb..d02072f9 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg create + .. _borg_create: borg create diff --git a/docs/usage/delete.rst.inc b/docs/usage/delete.rst.inc index 0977f022..fe750682 100644 --- a/docs/usage/delete.rst.inc +++ b/docs/usage/delete.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg delete + .. _borg_delete: borg delete diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index 0163c5dc..12f6e9e3 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg diff + .. _borg_diff: borg diff diff --git a/docs/usage/export-tar.rst.inc b/docs/usage/export-tar.rst.inc index f2c4e03a..4b801570 100644 --- a/docs/usage/export-tar.rst.inc +++ b/docs/usage/export-tar.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg export-tar + .. _borg_export-tar: borg export-tar diff --git a/docs/usage/extract.rst.inc b/docs/usage/extract.rst.inc index f5b2d494..95b7f3bb 100644 --- a/docs/usage/extract.rst.inc +++ b/docs/usage/extract.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg extract + .. _borg_extract: borg extract diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index 0376329a..d6de9349 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg info + .. _borg_info: borg info diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index a5a6dbfd..e23e9cb9 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg init + .. _borg_init: borg init diff --git a/docs/usage/key_change-passphrase.rst.inc b/docs/usage/key_change-passphrase.rst.inc index 7666afc2..ffc02209 100644 --- a/docs/usage/key_change-passphrase.rst.inc +++ b/docs/usage/key_change-passphrase.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg key change-passphrase + .. _borg_key_change-passphrase: borg key change-passphrase diff --git a/docs/usage/key_export.rst.inc b/docs/usage/key_export.rst.inc index e976ae2d..6f27448c 100644 --- a/docs/usage/key_export.rst.inc +++ b/docs/usage/key_export.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg key export + .. _borg_key_export: borg key export diff --git a/docs/usage/key_import.rst.inc b/docs/usage/key_import.rst.inc index ceb89e3f..92c68671 100644 --- a/docs/usage/key_import.rst.inc +++ b/docs/usage/key_import.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg key import + .. _borg_key_import: borg key import diff --git a/docs/usage/key_migrate-to-repokey.rst.inc b/docs/usage/key_migrate-to-repokey.rst.inc index df242566..5faabf31 100644 --- a/docs/usage/key_migrate-to-repokey.rst.inc +++ b/docs/usage/key_migrate-to-repokey.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg key migrate-to-repokey + .. _borg_key_migrate-to-repokey: borg key migrate-to-repokey diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index cd8db74c..147de2ef 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg list + .. _borg_list: borg list diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index 026cc680..d17f1921 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg mount + .. _borg_mount: borg mount diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index 40e0c26c..ff288feb 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg prune + .. _borg_prune: borg prune diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index fd3ef446..3b6fc04c 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg recreate + .. _borg_recreate: borg recreate diff --git a/docs/usage/rename.rst.inc b/docs/usage/rename.rst.inc index 13baa7e4..c5e98f31 100644 --- a/docs/usage/rename.rst.inc +++ b/docs/usage/rename.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg rename + .. _borg_rename: borg rename diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index f3f1aa65..945ff174 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg serve + .. _borg_serve: borg serve diff --git a/docs/usage/umount.rst.inc b/docs/usage/umount.rst.inc index ab02038b..8be61e6b 100644 --- a/docs/usage/umount.rst.inc +++ b/docs/usage/umount.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg umount + .. _borg_umount: borg umount diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index bdf76ccd..13fd247e 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg upgrade + .. _borg_upgrade: borg upgrade diff --git a/docs/usage/with-lock.rst.inc b/docs/usage/with-lock.rst.inc index 47b5abcc..34a2b69d 100644 --- a/docs/usage/with-lock.rst.inc +++ b/docs/usage/with-lock.rst.inc @@ -1,5 +1,7 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! +.. borg:command:: borg with-lock + .. _borg_with-lock: borg with-lock diff --git a/setup.py b/setup.py index 5e2d309d..c676d8c6 100644 --- a/setup.py +++ b/setup.py @@ -258,6 +258,7 @@ class build_usage(Command): params = {"command": command, "command_": command.replace(' ', '_'), "underline": '-' * len('borg ' + command)} + doc.write(".. borg:command:: borg {command}\n\n".format(**params)) doc.write(".. _borg_{command_}:\n\n".format(**params)) doc.write("borg {command}\n{underline}\n::\n\n borg [common options] {command}".format(**params)) self.write_usage(parser, doc) From 226d1f60947bdaf4cb110205fd7e541c8ad15c1c Mon Sep 17 00:00:00 2001 From: enkore Date: Mon, 22 May 2017 20:45:29 +0200 Subject: [PATCH 0893/1387] Revert "docs: borg_domain for custom priority index entries" --- docs/borg_domain.py | 102 ---------------------- docs/conf.py | 14 +-- docs/usage/benchmark_crud.rst.inc | 2 - docs/usage/break-lock.rst.inc | 2 - docs/usage/change-passphrase.rst.inc | 2 - docs/usage/check.rst.inc | 2 - docs/usage/create.rst.inc | 2 - docs/usage/delete.rst.inc | 2 - docs/usage/diff.rst.inc | 2 - docs/usage/export-tar.rst.inc | 2 - docs/usage/extract.rst.inc | 2 - docs/usage/info.rst.inc | 2 - docs/usage/init.rst.inc | 2 - docs/usage/key_change-passphrase.rst.inc | 2 - docs/usage/key_export.rst.inc | 2 - docs/usage/key_import.rst.inc | 2 - docs/usage/key_migrate-to-repokey.rst.inc | 2 - docs/usage/list.rst.inc | 2 - docs/usage/mount.rst.inc | 2 - docs/usage/prune.rst.inc | 2 - docs/usage/recreate.rst.inc | 2 - docs/usage/rename.rst.inc | 2 - docs/usage/serve.rst.inc | 2 - docs/usage/umount.rst.inc | 2 - docs/usage/upgrade.rst.inc | 2 - docs/usage/with-lock.rst.inc | 2 - setup.py | 1 - 27 files changed, 4 insertions(+), 161 deletions(-) delete mode 100644 docs/borg_domain.py diff --git a/docs/borg_domain.py b/docs/borg_domain.py deleted file mode 100644 index 625e8140..00000000 --- a/docs/borg_domain.py +++ /dev/null @@ -1,102 +0,0 @@ -from sphinx import addnodes -from sphinx.domains import Domain, ObjType -from sphinx.locale import l_, _ -from sphinx.directives import ObjectDescription -from sphinx.domains.std import ws_re - - -class BorgObject(ObjectDescription): - indextemplate = l_('%s') - parse_node = None # type: Callable[[GenericObject, BuildEnvironment, unicode, addnodes.desc_signature], unicode] # NOQA - - def handle_signature(self, sig, signode): - # type: (unicode, addnodes.desc_signature) -> unicode - pass - - def add_target_and_index(self, name, sig, signode): - # type: (str, str, addnodes.desc_signature) -> None - # ^ ignore this one, don't insert any markup. - # v- the human text v- the target name - # "borg key change-passphrase" -> "borg-key-change-passphrase" - del name # ignored - targetname = sig.replace(' ', '-') - if self.indextemplate: - colon = self.indextemplate.find(':') - if colon != -1: - indextype = self.indextemplate[:colon].strip() - indexentry = self.indextemplate[colon + 1:].strip() % (sig,) - else: - indextype = 'single' - indexentry = self.indextemplate % (sig,) - self.indexnode['entries'].append((indextype, indexentry, targetname, '', None)) - self.env.domaindata['borg']['objects'][targetname] = self.env.docname, self.objtype, sig - - def run(self): - super().run() - return [self.indexnode] - - -class BorgCommand(BorgObject): - """ - Inserts an index entry and an anchor for a borg command. - - For example, the following snippet creates an index entry about the "borg foo-and-bar" - command as well as a "borg-foo-and-bar" anchor (id). - - .. borg:command:: borg foo-and-bar - """ - - indextemplate = l_('%s (command)') - - -class BorgEnvVar(BorgObject): - """ - Inserts an index entry and an anchor for an environment variable. - (Currently not used) - """ - - indextemplate = l_('%s (environment variable)') - - -class BorgDomain(Domain): - """Land of the Borg.""" - name = 'borg' - label = 'Borg' - object_types = { - 'command': ObjType(l_('command')), - 'env_var': ObjType(l_('env_var')), - } - directives = { - 'command': BorgCommand, - 'env_var': BorgEnvVar, - } - roles = {} - initial_data = { - 'objects': {}, # fullname -> docname, objtype - } - - def clear_doc(self, docname): - # required for incremental builds - try: - del self.data['objects'][docname] - except KeyError: - pass - - def merge_domaindata(self, docnames, otherdata): - # needed due to parallel_read_safe - for fullname, (docname, objtype, sig) in otherdata['objects'].items(): - if docname in docnames: - self.data['objects'][fullname] = (docname, objtype, sig) - - def get_objects(self): - for refname, (docname, objtype, sig) in list(self.data['objects'].items()): - yield sig, sig, objtype, docname, refname, 1 - - -def setup(app): - app.add_domain(BorgDomain) - return { - 'version': 1, - 'parallel_read_safe': True, - 'parallel_write_safe': True, - } diff --git a/docs/conf.py b/docs/conf.py index 9a541ac1..dc40f2a0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,6 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. import sys, os sys.path.insert(0, os.path.abspath('../src')) -sys.path.insert(0, os.path.abspath('.')) from borg import __version__ as sw_version @@ -27,7 +26,9 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' -# Extensions are defined at the end of this file. +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -240,14 +241,7 @@ man_pages = [ 1), ] -extensions = [ - 'sphinx.ext.extlinks', - 'sphinx.ext.autodoc', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', - 'borg_domain', -] +extensions = ['sphinx.ext.extlinks', 'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] extlinks = { 'issue': ('https://github.com/borgbackup/borg/issues/%s', '#'), diff --git a/docs/usage/benchmark_crud.rst.inc b/docs/usage/benchmark_crud.rst.inc index 604c8795..d47e8d62 100644 --- a/docs/usage/benchmark_crud.rst.inc +++ b/docs/usage/benchmark_crud.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg benchmark crud - .. _borg_benchmark_crud: borg benchmark crud diff --git a/docs/usage/break-lock.rst.inc b/docs/usage/break-lock.rst.inc index 24a97303..1b8e5915 100644 --- a/docs/usage/break-lock.rst.inc +++ b/docs/usage/break-lock.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg break-lock - .. _borg_break-lock: borg break-lock diff --git a/docs/usage/change-passphrase.rst.inc b/docs/usage/change-passphrase.rst.inc index 7763745c..b0a6c2bb 100644 --- a/docs/usage/change-passphrase.rst.inc +++ b/docs/usage/change-passphrase.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg change-passphrase - .. _borg_change-passphrase: borg change-passphrase diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index 73b3ab8e..56bc42c8 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg check - .. _borg_check: borg check diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index d02072f9..6b7006cb 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg create - .. _borg_create: borg create diff --git a/docs/usage/delete.rst.inc b/docs/usage/delete.rst.inc index fe750682..0977f022 100644 --- a/docs/usage/delete.rst.inc +++ b/docs/usage/delete.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg delete - .. _borg_delete: borg delete diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index 12f6e9e3..0163c5dc 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg diff - .. _borg_diff: borg diff diff --git a/docs/usage/export-tar.rst.inc b/docs/usage/export-tar.rst.inc index 4b801570..f2c4e03a 100644 --- a/docs/usage/export-tar.rst.inc +++ b/docs/usage/export-tar.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg export-tar - .. _borg_export-tar: borg export-tar diff --git a/docs/usage/extract.rst.inc b/docs/usage/extract.rst.inc index 95b7f3bb..f5b2d494 100644 --- a/docs/usage/extract.rst.inc +++ b/docs/usage/extract.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg extract - .. _borg_extract: borg extract diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index d6de9349..0376329a 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg info - .. _borg_info: borg info diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index e23e9cb9..a5a6dbfd 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg init - .. _borg_init: borg init diff --git a/docs/usage/key_change-passphrase.rst.inc b/docs/usage/key_change-passphrase.rst.inc index ffc02209..7666afc2 100644 --- a/docs/usage/key_change-passphrase.rst.inc +++ b/docs/usage/key_change-passphrase.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg key change-passphrase - .. _borg_key_change-passphrase: borg key change-passphrase diff --git a/docs/usage/key_export.rst.inc b/docs/usage/key_export.rst.inc index 6f27448c..e976ae2d 100644 --- a/docs/usage/key_export.rst.inc +++ b/docs/usage/key_export.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg key export - .. _borg_key_export: borg key export diff --git a/docs/usage/key_import.rst.inc b/docs/usage/key_import.rst.inc index 92c68671..ceb89e3f 100644 --- a/docs/usage/key_import.rst.inc +++ b/docs/usage/key_import.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg key import - .. _borg_key_import: borg key import diff --git a/docs/usage/key_migrate-to-repokey.rst.inc b/docs/usage/key_migrate-to-repokey.rst.inc index 5faabf31..df242566 100644 --- a/docs/usage/key_migrate-to-repokey.rst.inc +++ b/docs/usage/key_migrate-to-repokey.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg key migrate-to-repokey - .. _borg_key_migrate-to-repokey: borg key migrate-to-repokey diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index 147de2ef..cd8db74c 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg list - .. _borg_list: borg list diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index d17f1921..026cc680 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg mount - .. _borg_mount: borg mount diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index ff288feb..40e0c26c 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg prune - .. _borg_prune: borg prune diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index 3b6fc04c..fd3ef446 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg recreate - .. _borg_recreate: borg recreate diff --git a/docs/usage/rename.rst.inc b/docs/usage/rename.rst.inc index c5e98f31..13baa7e4 100644 --- a/docs/usage/rename.rst.inc +++ b/docs/usage/rename.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg rename - .. _borg_rename: borg rename diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index 945ff174..f3f1aa65 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg serve - .. _borg_serve: borg serve diff --git a/docs/usage/umount.rst.inc b/docs/usage/umount.rst.inc index 8be61e6b..ab02038b 100644 --- a/docs/usage/umount.rst.inc +++ b/docs/usage/umount.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg umount - .. _borg_umount: borg umount diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index 13fd247e..bdf76ccd 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg upgrade - .. _borg_upgrade: borg upgrade diff --git a/docs/usage/with-lock.rst.inc b/docs/usage/with-lock.rst.inc index 34a2b69d..47b5abcc 100644 --- a/docs/usage/with-lock.rst.inc +++ b/docs/usage/with-lock.rst.inc @@ -1,7 +1,5 @@ .. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! -.. borg:command:: borg with-lock - .. _borg_with-lock: borg with-lock diff --git a/setup.py b/setup.py index c676d8c6..5e2d309d 100644 --- a/setup.py +++ b/setup.py @@ -258,7 +258,6 @@ class build_usage(Command): params = {"command": command, "command_": command.replace(' ', '_'), "underline": '-' * len('borg ' + command)} - doc.write(".. borg:command:: borg {command}\n\n".format(**params)) doc.write(".. _borg_{command_}:\n\n".format(**params)) doc.write("borg {command}\n{underline}\n::\n\n borg [common options] {command}".format(**params)) self.write_usage(parser, doc) From 65d1deae1a182fe92e9f8b7381f5a2e3df4fb6a5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 18 May 2017 22:26:09 +0200 Subject: [PATCH 0894/1387] faq: separate section for attic-stuff --- docs/faq.rst | 70 +++++++++++++++++++++++++++++++++++--------------- docs/usage.rst | 2 ++ 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 44cff07f..c251417c 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -111,24 +111,6 @@ Are there other known limitations? :ref:`borg_info` shows how large (relative to the maximum size) existing archives are. -Why is my backup bigger than with attic? ----------------------------------------- - -Attic was rather unflexible when it comes to compression, it always -compressed using zlib level 6 (no way to switch compression off or -adjust the level or algorithm). - -The default in Borg is lz4, which is fast enough to not use significant CPU time -in most cases, but can only achieve modest compression. It still compresses -easily compressed data fairly well. - -zlib compression with all levels (1-9) as well as LZMA (1-6) are available -as well, for cases where they are worth it. - -Which choice is the best option depends on a number of factors, like -bandwidth to the repository, how well the data compresses, available CPU -power and so on. - If a backup stops mid-way, does the already-backed-up data stay there? ---------------------------------------------------------------------- @@ -649,6 +631,9 @@ Borg intends to be: or without warning. allow compatibility breaking for other cases. * if major version number changes, it may have incompatible changes +Migrating from Attic +#################### + What are the differences between Attic and Borg? ------------------------------------------------ @@ -659,8 +644,9 @@ Borg is a fork of `Attic`_ and maintained by "`The Borg collective`_". Here's a (incomplete) list of some major changes: +* lots of attic issues fixed (see `issue #5 `_), + including critical data corruption bugs and security issues. * more open, faster paced development (see `issue #1 `_) -* lots of attic issues fixed (see `issue #5 `_) * less chunk management overhead (less memory and disk usage for chunks index) * faster remote cache resync (useful when backing up multiple machines into same repo) * compression: no, lz4, zlib or lzma compression, adjustable compression levels @@ -676,4 +662,48 @@ Here's a (incomplete) list of some major changes: Please read the :ref:`changelog` (or ``docs/changes.rst`` in the source distribution) for more information. -Borg is not compatible with original attic (but there is a one-way conversion). +Borg is not compatible with original Attic (but there is a one-way conversion). + +How do I migrate from Attic to Borg? +------------------------------------ + +Use :ref:`borg_upgrade`. This is a one-way process that cannot be reversed. + +There are some caveats: + +- The upgrade can only be performed on local repositories. + It cannot be performed on remote repositories. + +- If the repository is in "keyfile" encryption mode, the keyfile must + exist locally or it must be manually moved after performing the upgrade: + + 1. Locate the repository ID, contained in the ``config`` file in the repository. + 2. Locate the attic key file at ``~/.attic/keys/``. The correct key for the + repository starts with the line ``ATTIC_KEY ``. + 3. Copy the attic key file to ``~/.config/borg/keys/`` + 4. Change the first line from ``ATTIC_KEY ...`` to ``BORG_KEY ...``. + 5. Verify that the repository is now accessible (e.g. ``borg list ``). +- Attic and Borg use different :ref:`"chunker params" `. + This means that data added by Borg won't deduplicate with the existing data + stored by Attic. The effect is lessened if the files cache is used with Borg. +- Repositories in "passphrase" mode *must* be migrated to "repokey" mode using + :ref:`borg_key_migrate-to-repokey`. Borg does not support the "passphrase" mode + any other way. + +Why is my backup bigger than with attic? +---------------------------------------- + +Attic was rather unflexible when it comes to compression, it always +compressed using zlib level 6 (no way to switch compression off or +adjust the level or algorithm). + +The default in Borg is lz4, which is fast enough to not use significant CPU time +in most cases, but can only achieve modest compression. It still compresses +easily compressed data fairly well. + +zlib compression with all levels (1-9) as well as LZMA (1-6) are available +as well, for cases where they are worth it. + +Which choice is the best option depends on a number of factors, like +bandwidth to the repository, how well the data compresses, available CPU +power and so on. diff --git a/docs/usage.rst b/docs/usage.rst index 924559b3..7997bcb0 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -563,6 +563,8 @@ Additional Notes Here are misc. notes about topics that are maybe not covered in enough detail in the usage section. +.. _chunker-params: + --chunker-params ~~~~~~~~~~~~~~~~ From acf3842e0250a878a8ac70fce8c63d6502f21e2e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 18 May 2017 22:39:43 +0200 Subject: [PATCH 0895/1387] docs: create appendices in the book --- docs/book.rst | 5 +---- docs/conf.py | 7 ++++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/book.rst b/docs/book.rst index 679a0522..a1a1b9fe 100644 --- a/docs/book.rst +++ b/docs/book.rst @@ -4,6 +4,7 @@ Borg documentation ================== .. when you add an element here, do not forget to add it to index.rst +.. Note: Some things are in appendices (see latex_appendices in conf.py) .. toctree:: :maxdepth: 2 @@ -14,9 +15,5 @@ Borg documentation usage deployment faq - support - resources - changes internals development - authors diff --git a/docs/conf.py b/docs/conf.py index dc40f2a0..4f935a26 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -224,7 +224,12 @@ latex_show_urls = 'footnote' #latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +latex_appendices = [ + 'support', + 'resources', + 'changes', + 'authors', +] # If false, no module index is generated. #latex_domain_indices = True From d99aff1276de3e4e3c230cba74a72e4896f98d73 Mon Sep 17 00:00:00 2001 From: Benedikt Neuffer Date: Mon, 6 Mar 2017 20:46:03 +0100 Subject: [PATCH 0896/1387] ipv6 address support also: Location: more informative exception when parsing fails --- src/borg/helpers.py | 10 ++++++--- src/borg/testsuite/helpers.py | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index a93ba710..2f4a3379 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -814,7 +814,7 @@ class Location: ssh_re = re.compile(r""" (?Pssh):// # ssh:// """ + optional_user_re + r""" # user@ (optional) - (?P[^:/]+)(?::(?P\d+))? # host or host:port + (?P([^:/]+|\[[0-9a-fA-F:.]+\]))(?::(?P\d+))? # host or host:port or [ipv6] or [ipv6]:port """ + abs_path_re + optional_archive_re, re.VERBOSE) # path or path::archive file_re = re.compile(r""" @@ -825,7 +825,7 @@ class Location: scp_re = re.compile(r""" ( """ + optional_user_re + r""" # user@ (optional) - (?P[^:/]+): # host: (don't match / in host to disambiguate from file:) + (?P([^:/]+|\[[0-9a-fA-F:.]+\])): # host: (don't match / or [ipv6] in host to disambiguate from file:) )? # user@host: part is optional """ + scp_path_re + optional_archive_re, re.VERBOSE) # path with optional archive @@ -841,7 +841,7 @@ class Location: def __init__(self, text=''): self.orig = text if not self.parse(self.orig): - raise ValueError + raise ValueError('Location: parse failed: %s' % self.orig) def parse(self, text): text = replace_placeholders(text) @@ -872,6 +872,8 @@ class Location: self.proto = m.group('proto') self.user = m.group('user') self.host = m.group('host') + if self.host is not None: + self.host = self.host.lstrip('[').rstrip(']') self.port = m.group('port') and int(m.group('port')) or None self.path = normpath_special(m.group('path')) self.archive = m.group('archive') @@ -886,6 +888,8 @@ class Location: if m: self.user = m.group('user') self.host = m.group('host') + if isinstance(self.host, str): + self.host = self.host.lstrip('[').rstrip(']') self.path = normpath_special(m.group('path')) self.archive = m.group('archive') self.proto = self.host and 'ssh' or 'file' diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index ff6b5efe..7ce22dc2 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -58,6 +58,30 @@ class TestLocationWithoutEnv: "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)" assert repr(Location('ssh://user@host/some/path')) == \ "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)" + assert repr(Location('ssh://user@[::]:1234/some/path::archive')) == \ + "Location(proto='ssh', user='user', host='::', port=1234, path='/some/path', archive='archive')" + assert repr(Location('ssh://user@[::]:1234/some/path')) == \ + "Location(proto='ssh', user='user', host='::', port=1234, path='/some/path', archive=None)" + assert repr(Location('ssh://user@[::]/some/path')) == \ + "Location(proto='ssh', user='user', host='::', port=None, path='/some/path', archive=None)" + assert repr(Location('ssh://user@[2001:db8::]:1234/some/path::archive')) == \ + "Location(proto='ssh', user='user', host='2001:db8::', port=1234, path='/some/path', archive='archive')" + assert repr(Location('ssh://user@[2001:db8::]:1234/some/path')) == \ + "Location(proto='ssh', user='user', host='2001:db8::', port=1234, path='/some/path', archive=None)" + assert repr(Location('ssh://user@[2001:db8::]/some/path')) == \ + "Location(proto='ssh', user='user', host='2001:db8::', port=None, path='/some/path', archive=None)" + assert repr(Location('ssh://user@[2001:db8::c0:ffee]:1234/some/path::archive')) == \ + "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=1234, path='/some/path', archive='archive')" + assert repr(Location('ssh://user@[2001:db8::c0:ffee]:1234/some/path')) == \ + "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=1234, path='/some/path', archive=None)" + assert repr(Location('ssh://user@[2001:db8::c0:ffee]/some/path')) == \ + "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=None, path='/some/path', archive=None)" + assert repr(Location('ssh://user@[2001:db8::192.0.2.1]:1234/some/path::archive')) == \ + "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=1234, path='/some/path', archive='archive')" + assert repr(Location('ssh://user@[2001:db8::192.0.2.1]:1234/some/path')) == \ + "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=1234, path='/some/path', archive=None)" + assert repr(Location('ssh://user@[2001:db8::192.0.2.1]/some/path')) == \ + "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path', archive=None)" def test_file(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) @@ -72,6 +96,22 @@ class TestLocationWithoutEnv: "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive='archive')" assert repr(Location('user@host:/some/path')) == \ "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)" + assert repr(Location('user@[::]:/some/path::archive')) == \ + "Location(proto='ssh', user='user', host='::', port=None, path='/some/path', archive='archive')" + assert repr(Location('user@[::]:/some/path')) == \ + "Location(proto='ssh', user='user', host='::', port=None, path='/some/path', archive=None)" + assert repr(Location('user@[2001:db8::]:/some/path::archive')) == \ + "Location(proto='ssh', user='user', host='2001:db8::', port=None, path='/some/path', archive='archive')" + assert repr(Location('user@[2001:db8::]:/some/path')) == \ + "Location(proto='ssh', user='user', host='2001:db8::', port=None, path='/some/path', archive=None)" + assert repr(Location('user@[2001:db8::c0:ffee]:/some/path::archive')) == \ + "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=None, path='/some/path', archive='archive')" + assert repr(Location('user@[2001:db8::c0:ffee]:/some/path')) == \ + "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=None, path='/some/path', archive=None)" + assert repr(Location('user@[2001:db8::192.0.2.1]:/some/path::archive')) == \ + "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path', archive='archive')" + assert repr(Location('user@[2001:db8::192.0.2.1]:/some/path')) == \ + "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path', archive=None)" def test_smb(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) From 0d406e7bafa8064edba92841235bfee79e191df4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 23 May 2017 03:09:57 +0200 Subject: [PATCH 0897/1387] use _host to store host/ipv4/ipv6 for ipv6, it includes the outer square brackets. the host property strips any outer square brackets. --- src/borg/helpers.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 2f4a3379..3fc22e7c 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -769,7 +769,7 @@ def bin_to_hex(binary): class Location: """Object representing a repository / archive location """ - proto = user = host = port = path = archive = None + proto = user = _host = port = path = archive = None # user must not contain "@", ":" or "/". # Quoting adduser error message: @@ -871,9 +871,7 @@ class Location: if m: self.proto = m.group('proto') self.user = m.group('user') - self.host = m.group('host') - if self.host is not None: - self.host = self.host.lstrip('[').rstrip(']') + self._host = m.group('host') self.port = m.group('port') and int(m.group('port')) or None self.path = normpath_special(m.group('path')) self.archive = m.group('archive') @@ -887,12 +885,10 @@ class Location: m = self.scp_re.match(text) if m: self.user = m.group('user') - self.host = m.group('host') - if isinstance(self.host, str): - self.host = self.host.lstrip('[').rstrip(']') + self._host = m.group('host') self.path = normpath_special(m.group('path')) self.archive = m.group('archive') - self.proto = self.host and 'ssh' or 'file' + self.proto = self._host and 'ssh' or 'file' return True return False @@ -916,6 +912,12 @@ class Location: def __repr__(self): return "Location(%s)" % self + @property + def host(self): + # strip square brackets used for IPv6 addrs + if self._host is not None: + return self._host.lstrip('[').rstrip(']') + def canonical_path(self): if self.proto == 'file': return self.path @@ -927,7 +929,7 @@ class Location: else: path = self.path return 'ssh://{}{}{}{}'.format('{}@'.format(self.user) if self.user else '', - self.host, + self._host, # needed for ipv6 addrs ':{}'.format(self.port) if self.port else '', path) From 83fdc39251c6a6f69b7951ddc8513afb2d24bfb2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 18 May 2017 23:14:36 +0200 Subject: [PATCH 0898/1387] docs book: use A4 format, new builder option format. --- docs/conf.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4f935a26..1baf39a7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -193,12 +193,6 @@ htmlhelp_basename = 'borgdoc' # -- Options for LaTeX output -------------------------------------------------- -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '12pt' - # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ @@ -210,6 +204,11 @@ latex_documents = [ # the title page. latex_logo = '_static/logo.pdf' +latex_elements = { + 'papersize': 'a4paper', + 'pointsize': '10pt', +} + # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False From 53d0f1fd022f2d09c3ddff86a50c11cfa3b16578 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 21 May 2017 10:25:42 +0200 Subject: [PATCH 0899/1387] fail in borg package if version metadata is completely broken this helps to fail early when people do badly done scm based repackaging --- src/borg/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/borg/__init__.py b/src/borg/__init__.py index 9ac0e0f9..33b9616d 100644 --- a/src/borg/__init__.py +++ b/src/borg/__init__.py @@ -4,3 +4,15 @@ from ._version import version as __version__ __version_tuple__ = tuple(LooseVersion(__version__).version[:3]) + +# assert that all semver components are integers +# this is mainly to show errors when people repackage poorly +# and setuptools_scm determines a 0.1.dev... version +assert all(isinstance(v, int) for v in __version_tuple__), \ + """\ +broken borgbackup version metadata: %r + +version metadata is obtained dynamically on installation via setuptools_scm, +please ensure your git repo has the correct tags or you provide the version +using SETUPTOOLS_SCM_PRETEND_VERSION in your build script. +""" % __version__ From 573d728f7a82184ac742217e801cc0648257c97e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 00:36:50 +0200 Subject: [PATCH 0900/1387] init: don't allow creating nested repositories --- src/borg/repository.py | 43 ++++++++++++++++++++++++++++++---- src/borg/testsuite/archiver.py | 8 +++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index b253d3f6..723cf8db 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -88,7 +88,7 @@ class Repository: """Repository {} does not exist.""" class AlreadyExists(Error): - """Repository {} already exists.""" + """A repository already exists at {}.""" class InvalidRepository(Error): """{} is not a valid repository. Check repo config.""" @@ -158,11 +158,46 @@ class Repository: def id_str(self): return bin_to_hex(self.id) - def create(self, path): - """Create a new empty repository at `path` + def check_can_create_repository(self, path): + """ + Raise self.AlreadyExists if a repository already exists at *path* or any parent directory. + + Checking parent directories is done for two reasons: + (1) It's just a weird thing to do, and usually not intended. A Borg using the "parent" repository + may be confused, or we may accidentally put stuff into the "data/" or "data//" directories. + (2) When implementing repository quotas (which we currently don't), it's important to prohibit + folks from creating quota-free repositories. Since no one can create a repository within another + repository, user's can only use the quota'd repository, when their --restrict-to-path points + at the user's repository. """ if os.path.exists(path) and (not os.path.isdir(path) or os.listdir(path)): raise self.AlreadyExists(path) + + while True: + # Check all parent directories for Borg's repository README + previous_path = path + # Thus, path = previous_path/.. + path = os.path.abspath(os.path.join(previous_path, os.pardir)) + if path == previous_path: + # We reached the root of the directory hierarchy (/.. = / and C:\.. = C:\). + break + try: + # Use binary mode to avoid troubles if a README contains some stuff not in our locale + with open(os.path.join(path, 'README'), 'rb') as fd: + # Read only the first ~100 bytes (if any), in case some README file we stumble upon is large. + readme_head = fd.read(100) + # The first comparison captures our current variant (REPOSITORY_README), the second comparison + # is an older variant of the README file (used by 1.0.x). + if b'Borg Backup repository' in readme_head or b'Borg repository' in readme_head: + raise self.AlreadyExists(path) + except OSError: + # Ignore FileNotFound, PermissionError, ... + pass + + def create(self, path): + """Create a new empty repository at `path` + """ + self.check_can_create_repository(path) if not os.path.exists(path): os.mkdir(path) with open(os.path.join(path, 'README'), 'w') as fd: @@ -434,7 +469,7 @@ class Repository: # At this point the index may only be updated by compaction, which won't resize it. # We still apply a factor of four so that a later, separate invocation can free space # (journaling all deletes for all chunks is one index size) or still make minor additions - # (which may grow the index up to twice it's current size). + # (which may grow the index up to twice its current size). # Note that in a subsequent operation the committed index is still on-disk, therefore we # arrive at index_size * (1 + 2 + 1). # In that order: journaled deletes (1), hashtable growth (2), persisted index (1). diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 30b5566c..508b1586 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2074,6 +2074,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_init_requires_encryption_option(self): self.cmd('init', self.repository_location, exit_code=2) + def test_init_nested_repositories(self): + self.cmd('init', '--encryption=repokey', self.repository_location) + if self.FORK_DEFAULT: + self.cmd('init', '--encryption=repokey', self.repository_location + '/nested', exit_code=2) + else: + with pytest.raises(Repository.AlreadyExists): + self.cmd('init', '--encryption=repokey', self.repository_location + '/nested') + def check_cache(self): # First run a regular borg check self.cmd('check', self.repository_location) From 4b8a04b5e7e0240cba4deaa063268a2109ac7e85 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 12:32:42 +0200 Subject: [PATCH 0901/1387] key file names: remove colons from host name --- src/borg/helpers.py | 2 +- src/borg/testsuite/helpers.py | 39 ++++++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 15f56437..f083ea1a 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -916,7 +916,7 @@ class Location: def to_key_filename(self): name = re.sub('[^\w]', '_', self.path).strip('_') if self.proto != 'file': - name = self.host + '__' + name + name = re.sub('[^\w]', '_', self.host) + '__' + name return os.path.join(get_keys_dir(), name) def __repr__(self): diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 7ce22dc2..52df044f 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -50,10 +50,19 @@ def test_bin_to_hex(): class TestLocationWithoutEnv: - def test_ssh(self, monkeypatch): + @pytest.fixture + def keys_dir(self, tmpdir, monkeypatch): + tmpdir = str(tmpdir) + monkeypatch.setenv('BORG_KEYS_DIR', tmpdir) + if not tmpdir.endswith(os.path.sep): + tmpdir += os.path.sep + return tmpdir + + def test_ssh(self, monkeypatch, keys_dir): monkeypatch.delenv('BORG_REPO', raising=False) assert repr(Location('ssh://user@host:1234/some/path::archive')) == \ "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')" + assert Location('ssh://user@host:1234/some/path::archive').to_key_filename() == keys_dir + 'host__some_path' assert repr(Location('ssh://user@host:1234/some/path')) == \ "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)" assert repr(Location('ssh://user@host/some/path')) == \ @@ -62,12 +71,14 @@ class TestLocationWithoutEnv: "Location(proto='ssh', user='user', host='::', port=1234, path='/some/path', archive='archive')" assert repr(Location('ssh://user@[::]:1234/some/path')) == \ "Location(proto='ssh', user='user', host='::', port=1234, path='/some/path', archive=None)" + assert Location('ssh://user@[::]:1234/some/path').to_key_filename() == keys_dir + '____some_path' assert repr(Location('ssh://user@[::]/some/path')) == \ "Location(proto='ssh', user='user', host='::', port=None, path='/some/path', archive=None)" assert repr(Location('ssh://user@[2001:db8::]:1234/some/path::archive')) == \ "Location(proto='ssh', user='user', host='2001:db8::', port=1234, path='/some/path', archive='archive')" assert repr(Location('ssh://user@[2001:db8::]:1234/some/path')) == \ "Location(proto='ssh', user='user', host='2001:db8::', port=1234, path='/some/path', archive=None)" + assert Location('ssh://user@[2001:db8::]:1234/some/path').to_key_filename() == keys_dir + '2001_db8____some_path' assert repr(Location('ssh://user@[2001:db8::]/some/path')) == \ "Location(proto='ssh', user='user', host='2001:db8::', port=None, path='/some/path', archive=None)" assert repr(Location('ssh://user@[2001:db8::c0:ffee]:1234/some/path::archive')) == \ @@ -82,15 +93,17 @@ class TestLocationWithoutEnv: "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=1234, path='/some/path', archive=None)" assert repr(Location('ssh://user@[2001:db8::192.0.2.1]/some/path')) == \ "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path', archive=None)" + assert Location('ssh://user@[2001:db8::192.0.2.1]/some/path').to_key_filename() == keys_dir + '2001_db8__192_0_2_1__some_path' - def test_file(self, monkeypatch): + def test_file(self, monkeypatch, keys_dir): monkeypatch.delenv('BORG_REPO', raising=False) assert repr(Location('file:///some/path::archive')) == \ "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive')" assert repr(Location('file:///some/path')) == \ "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive=None)" + assert Location('file:///some/path').to_key_filename() == keys_dir + 'some_path' - def test_scp(self, monkeypatch): + def test_scp(self, monkeypatch, keys_dir): monkeypatch.delenv('BORG_REPO', raising=False) assert repr(Location('user@host:/some/path::archive')) == \ "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive='archive')" @@ -112,42 +125,51 @@ class TestLocationWithoutEnv: "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path', archive='archive')" assert repr(Location('user@[2001:db8::192.0.2.1]:/some/path')) == \ "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path', archive=None)" + assert Location('user@[2001:db8::192.0.2.1]:/some/path').to_key_filename() == keys_dir + '2001_db8__192_0_2_1__some_path' - def test_smb(self, monkeypatch): + def test_smb(self, monkeypatch, keys_dir): monkeypatch.delenv('BORG_REPO', raising=False) assert repr(Location('file:////server/share/path::archive')) == \ "Location(proto='file', user=None, host=None, port=None, path='//server/share/path', archive='archive')" + assert Location('file:////server/share/path::archive').to_key_filename() == keys_dir + 'server_share_path' - def test_folder(self, monkeypatch): + def test_folder(self, monkeypatch, keys_dir): monkeypatch.delenv('BORG_REPO', raising=False) assert repr(Location('path::archive')) == \ "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive')" assert repr(Location('path')) == \ "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)" + assert Location('path').to_key_filename() == keys_dir + 'path' - def test_abspath(self, monkeypatch): + def test_abspath(self, monkeypatch, keys_dir): monkeypatch.delenv('BORG_REPO', raising=False) assert repr(Location('/some/absolute/path::archive')) == \ "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive='archive')" assert repr(Location('/some/absolute/path')) == \ "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)" + assert Location('/some/absolute/path').to_key_filename() == keys_dir + 'some_absolute_path' assert repr(Location('ssh://user@host/some/path')) == \ "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)" + assert Location('ssh://user@host/some/path').to_key_filename() == keys_dir + 'host__some_path' - def test_relpath(self, monkeypatch): + def test_relpath(self, monkeypatch, keys_dir): monkeypatch.delenv('BORG_REPO', raising=False) assert repr(Location('some/relative/path::archive')) == \ "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive='archive')" assert repr(Location('some/relative/path')) == \ "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)" + assert Location('some/relative/path').to_key_filename() == keys_dir + 'some_relative_path' assert repr(Location('ssh://user@host/./some/path')) == \ "Location(proto='ssh', user='user', host='host', port=None, path='/./some/path', archive=None)" + assert Location('ssh://user@host/./some/path').to_key_filename() == keys_dir + 'host__some_path' assert repr(Location('ssh://user@host/~/some/path')) == \ "Location(proto='ssh', user='user', host='host', port=None, path='/~/some/path', archive=None)" + assert Location('ssh://user@host/~/some/path').to_key_filename() == keys_dir + 'host__some_path' assert repr(Location('ssh://user@host/~user/some/path')) == \ "Location(proto='ssh', user='user', host='host', port=None, path='/~user/some/path', archive=None)" + assert Location('ssh://user@host/~user/some/path').to_key_filename() == keys_dir + 'host__user_some_path' - def test_with_colons(self, monkeypatch): + def test_with_colons(self, monkeypatch, keys_dir): monkeypatch.delenv('BORG_REPO', raising=False) assert repr(Location('/abs/path:w:cols::arch:col')) == \ "Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols', archive='arch:col')" @@ -155,6 +177,7 @@ class TestLocationWithoutEnv: "Location(proto='file', user=None, host=None, port=None, path='/abs/path:with:colons', archive='archive')" assert repr(Location('/abs/path:with:colons')) == \ "Location(proto='file', user=None, host=None, port=None, path='/abs/path:with:colons', archive=None)" + assert Location('/abs/path:with:colons').to_key_filename() == keys_dir + 'abs_path_with_colons' def test_user_parsing(self): # see issue #1930 From 38ed9a20afe70b0becc473710a89d914ad5a2223 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 12:36:45 +0200 Subject: [PATCH 0902/1387] key file names: limit to 100 characters (not bytes) --- src/borg/helpers.py | 5 +++++ src/borg/testsuite/helpers.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index f083ea1a..db66b822 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -917,6 +917,11 @@ class Location: name = re.sub('[^\w]', '_', self.path).strip('_') if self.proto != 'file': name = re.sub('[^\w]', '_', self.host) + '__' + name + if len(name) > 100: + # Limit file names to some reasonable length. Most file systems + # limit them to 255 [unit of choice]; due to variations in unicode + # handling we truncate to 100 *characters*. + name = name[:100] return os.path.join(get_keys_dir(), name) def __repr__(self): diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 52df044f..b23e277b 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -141,6 +141,10 @@ class TestLocationWithoutEnv: "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)" assert Location('path').to_key_filename() == keys_dir + 'path' + def test_long_path(self, monkeypatch, keys_dir): + monkeypatch.delenv('BORG_REPO', raising=False) + assert Location(os.path.join(*(40 * ['path']))).to_key_filename() == keys_dir + '_'.join(20 * ['path']) + '_' + def test_abspath(self, monkeypatch, keys_dir): monkeypatch.delenv('BORG_REPO', raising=False) assert repr(Location('/some/absolute/path::archive')) == \ From 39051ac5f148eb2792ddfb4b69cb79dbd8ab7928 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 13:22:24 +0200 Subject: [PATCH 0903/1387] file_integrity: split in IntegrityCheckedFile + Detached variant --- src/borg/crypto/file_integrity.py | 79 ++++++++++++++++++---------- src/borg/testsuite/file_integrity.py | 38 ++++++------- 2 files changed, 70 insertions(+), 47 deletions(-) diff --git a/src/borg/crypto/file_integrity.py b/src/borg/crypto/file_integrity.py index 5c1fa4e1..53cbcea3 100644 --- a/src/borg/crypto/file_integrity.py +++ b/src/borg/crypto/file_integrity.py @@ -104,7 +104,7 @@ class FileIntegrityError(IntegrityError): class IntegrityCheckedFile(FileLikeWrapper): - def __init__(self, path, write, filename=None, override_fd=None): + def __init__(self, path, write, filename=None, override_fd=None, integrity_data=None): self.path = path self.writing = write mode = 'wb' if write else 'rb' @@ -114,10 +114,10 @@ class IntegrityCheckedFile(FileLikeWrapper): self.hash_filename(filename) - if write: + if write or not integrity_data: self.digests = {} else: - self.digests = self.read_integrity_file(path, self.hasher) + self.digests = self.parse_integrity_data(path, integrity_data, self.hasher) # TODO: When we're reading but don't have any digests, i.e. no integrity file existed, # TODO: then we could just short-circuit. @@ -126,32 +126,27 @@ class IntegrityCheckedFile(FileLikeWrapper): # In Borg the name itself encodes the context (eg. index.N, cache, files), # while the path doesn't matter, and moving e.g. a repository or cache directory is supported. # Changing the name however imbues a change of context that is not permissible. + # While Borg does not use anything except ASCII in these file names, it's important to use + # the same encoding everywhere for portability. Using os.fsencode() would be wrong. filename = os.path.basename(filename or self.path) self.hasher.update(('%10d' % len(filename)).encode()) self.hasher.update(filename.encode()) - @staticmethod - def integrity_file_path(path): - return path + '.integrity' - @classmethod - def read_integrity_file(cls, path, hasher): + def parse_integrity_data(cls, path: str, data: str, hasher: SHA512FileHashingWrapper): try: - with open(cls.integrity_file_path(path), 'r') as fd: - integrity_file = json.load(fd) - # Provisions for agility now, implementation later, but make sure the on-disk joint is oiled. - algorithm = integrity_file['algorithm'] - if algorithm != hasher.ALGORITHM: - logger.warning('Cannot verify integrity of %s: Unknown algorithm %r', path, algorithm) - return - digests = integrity_file['digests'] - # Require at least presence of the final digest - digests['final'] - return digests - except FileNotFoundError: - logger.info('No integrity file found for %s', path) - except (OSError, ValueError, TypeError, KeyError) as e: - logger.warning('Could not read integrity file for %s: %s', path, e) + integrity_file = json.loads(data) + # Provisions for agility now, implementation later, but make sure the on-disk joint is oiled. + algorithm = integrity_file['algorithm'] + if algorithm != hasher.ALGORITHM: + logger.warning('Cannot verify integrity of %s: Unknown algorithm %r', path, algorithm) + return + digests = integrity_file['digests'] + # Require at least presence of the final digest + digests['final'] + return digests + except (ValueError, TypeError, KeyError) as e: + logger.warning('Could not parse integrity data for %s: %s', path, e) raise FileIntegrityError(path) def hash_part(self, partname, is_final=False): @@ -173,10 +168,38 @@ class IntegrityCheckedFile(FileLikeWrapper): if exception: return if self.writing: - with open(self.integrity_file_path(self.path), 'w') as fd: - json.dump({ - 'algorithm': self.hasher.ALGORITHM, - 'digests': self.digests, - }, fd) + self.store_integrity_data(json.dumps({ + 'algorithm': self.hasher.ALGORITHM, + 'digests': self.digests, + })) elif self.digests: logger.debug('Verified integrity of %s', self.path) + + def store_integrity_data(self, data: str): + self.integrity_data = data + + +class DetachedIntegrityCheckedFile(IntegrityCheckedFile): + def __init__(self, path, write, filename=None, override_fd=None): + super().__init__(path, write, filename, override_fd) + if not write: + self.digests = self.read_integrity_file(self.path, self.hasher) + + @staticmethod + def integrity_file_path(path): + return path + '.integrity' + + @classmethod + def read_integrity_file(cls, path, hasher): + try: + with open(cls.integrity_file_path(path), 'r') as fd: + return cls.parse_integrity_data(path, fd.read(), hasher) + except FileNotFoundError: + logger.info('No integrity file found for %s', path) + except OSError as e: + logger.warning('Could not read integrity file for %s: %s', path, e) + raise FileIntegrityError(path) + + def store_integrity_data(self, data: str): + with open(self.integrity_file_path(self.path), 'w') as fd: + fd.write(data) diff --git a/src/borg/testsuite/file_integrity.py b/src/borg/testsuite/file_integrity.py index a8ef95f7..0dd323d6 100644 --- a/src/borg/testsuite/file_integrity.py +++ b/src/borg/testsuite/file_integrity.py @@ -1,21 +1,21 @@ import pytest -from ..crypto.file_integrity import IntegrityCheckedFile, FileIntegrityError +from ..crypto.file_integrity import IntegrityCheckedFile, DetachedIntegrityCheckedFile, FileIntegrityError class TestReadIntegrityFile: def test_no_integrity(self, tmpdir): protected_file = tmpdir.join('file') protected_file.write('1234') - assert IntegrityCheckedFile.read_integrity_file(str(protected_file), None) is None + assert DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file), None) is None def test_truncated_integrity(self, tmpdir): protected_file = tmpdir.join('file') protected_file.write('1234') tmpdir.join('file.integrity').write('') with pytest.raises(FileIntegrityError): - IntegrityCheckedFile.read_integrity_file(str(protected_file), None) + DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file), None) def test_unknown_algorithm(self, tmpdir): class SomeHasher: @@ -24,7 +24,7 @@ class TestReadIntegrityFile: protected_file = tmpdir.join('file') protected_file.write('1234') tmpdir.join('file.integrity').write('{"algorithm": "HMAC_SERIOUSHASH", "digests": "1234"}') - assert IntegrityCheckedFile.read_integrity_file(str(protected_file), SomeHasher()) is None + assert DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file), SomeHasher()) is None @pytest.mark.parametrize('json', ( '{"ALGORITHM": "HMAC_SERIOUSHASH", "digests": "1234"}', @@ -38,7 +38,7 @@ class TestReadIntegrityFile: protected_file.write('1234') tmpdir.join('file.integrity').write(json) with pytest.raises(FileIntegrityError): - IntegrityCheckedFile.read_integrity_file(str(protected_file), None) + DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file), None) def test_valid(self, tmpdir): class SomeHasher: @@ -47,35 +47,35 @@ class TestReadIntegrityFile: protected_file = tmpdir.join('file') protected_file.write('1234') tmpdir.join('file.integrity').write('{"algorithm": "HMAC_FOO1", "digests": {"final": "1234"}}') - assert IntegrityCheckedFile.read_integrity_file(str(protected_file), SomeHasher()) == {'final': '1234'} + assert DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file), SomeHasher()) == {'final': '1234'} -class TestIntegrityCheckedFile: +class TestDetachedIntegrityCheckedFile: @pytest.fixture def integrity_protected_file(self, tmpdir): path = str(tmpdir.join('file')) - with IntegrityCheckedFile(path, write=True) as fd: + with DetachedIntegrityCheckedFile(path, write=True) as fd: fd.write(b'foo and bar') return path def test_simple(self, tmpdir, integrity_protected_file): assert tmpdir.join('file').check(file=True) assert tmpdir.join('file.integrity').check(file=True) - with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd: assert fd.read() == b'foo and bar' def test_corrupted_file(self, integrity_protected_file): with open(integrity_protected_file, 'ab') as fd: fd.write(b' extra data') with pytest.raises(FileIntegrityError): - with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd: assert fd.read() == b'foo and bar extra data' def test_corrupted_file_partial_read(self, integrity_protected_file): with open(integrity_protected_file, 'ab') as fd: fd.write(b' extra data') with pytest.raises(FileIntegrityError): - with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd: data = b'foo and bar' assert fd.read(len(data)) == data @@ -88,7 +88,7 @@ class TestIntegrityCheckedFile: tmpdir.join('file').move(new_path) tmpdir.join('file.integrity').move(new_path + '.integrity') with pytest.raises(FileIntegrityError): - with IntegrityCheckedFile(str(new_path), write=False) as fd: + with DetachedIntegrityCheckedFile(str(new_path), write=False) as fd: assert fd.read() == b'foo and bar' def test_moved_file(self, tmpdir, integrity_protected_file): @@ -96,27 +96,27 @@ class TestIntegrityCheckedFile: tmpdir.join('file').move(new_dir.join('file')) tmpdir.join('file.integrity').move(new_dir.join('file.integrity')) new_path = str(new_dir.join('file')) - with IntegrityCheckedFile(new_path, write=False) as fd: + with DetachedIntegrityCheckedFile(new_path, write=False) as fd: assert fd.read() == b'foo and bar' def test_no_integrity(self, tmpdir, integrity_protected_file): tmpdir.join('file.integrity').remove() - with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd: assert fd.read() == b'foo and bar' -class TestIntegrityCheckedFileParts: +class TestDetachedIntegrityCheckedFileParts: @pytest.fixture def integrity_protected_file(self, tmpdir): path = str(tmpdir.join('file')) - with IntegrityCheckedFile(path, write=True) as fd: + with DetachedIntegrityCheckedFile(path, write=True) as fd: fd.write(b'foo and bar') fd.hash_part('foopart') fd.write(b' other data') return path def test_simple(self, integrity_protected_file): - with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd: data1 = b'foo and bar' assert fd.read(len(data1)) == data1 fd.hash_part('foopart') @@ -127,7 +127,7 @@ class TestIntegrityCheckedFileParts: # Because some hash_part failed, the final digest will fail as well - again - even if we catch # the failing hash_part. This is intentional: (1) it makes the code simpler (2) it's a good fail-safe # against overly broad exception handling. - with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd: data1 = b'foo and bar' assert fd.read(len(data1)) == data1 with pytest.raises(FileIntegrityError): @@ -140,7 +140,7 @@ class TestIntegrityCheckedFileParts: with open(integrity_protected_file, 'ab') as fd: fd.write(b'some extra stuff that does not belong') with pytest.raises(FileIntegrityError): - with IntegrityCheckedFile(integrity_protected_file, write=False) as fd: + with DetachedIntegrityCheckedFile(integrity_protected_file, write=False) as fd: data1 = b'foo and bar' try: assert fd.read(len(data1)) == data1 From 06cf15cc6dcee0e490755e5d17a8eccd9bc0ea42 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 13:43:04 +0200 Subject: [PATCH 0904/1387] hashindex: read/write: accept file-like objects for path --- src/borg/hashindex.pyx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index fba8c7a3..2409836f 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -67,8 +67,11 @@ cdef class IndexBase: def __cinit__(self, capacity=0, path=None, key_size=32): self.key_size = key_size if path: - with open(path, 'rb') as fd: - self.index = hashindex_read(fd) + if isinstance(path, (str, bytes)): + with open(path, 'rb') as fd: + self.index = hashindex_read(fd) + else: + self.index = hashindex_read(path) assert self.index, 'hashindex_read() returned NULL with no exception set' else: self.index = hashindex_init(capacity, self.key_size, self.value_size) @@ -84,8 +87,11 @@ cdef class IndexBase: return cls(path=path) def write(self, path): - with open(path, 'wb') as fd: - hashindex_write(self.index, fd) + if isinstance(path, (str, bytes)): + with open(path, 'wb') as fd: + hashindex_write(self.index, fd) + else: + hashindex_write(self.index, path) def clear(self): hashindex_free(self.index) From 2b518b71884c612a00f7951c5516f412db9ca5c0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 13:43:15 +0200 Subject: [PATCH 0905/1387] cache: add integrity checking of chunks and files caches --- src/borg/cache.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 882d9863..fa2e6fe4 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -24,6 +24,7 @@ from .helpers import remove_surrogates from .helpers import ProgressIndicatorPercent, ProgressIndicatorMessage from .item import ArchiveItem, ChunkListEntry from .crypto.key import PlaintextKey +from .crypto.file_integrity import IntegrityCheckedFile, DetachedIntegrityCheckedFile from .locking import Lock from .platform import SaveFile from .remote import cache_if_remote @@ -237,6 +238,7 @@ class CacheConfig: config.set('cache', 'version', '1') config.set('cache', 'repository', self.repository.id_str) config.set('cache', 'manifest', '') + config.add_section('integrity') with SaveFile(self.config_path) as fd: config.write(fd) @@ -253,6 +255,12 @@ class CacheConfig: self.manifest_id = unhexlify(self._config.get('cache', 'manifest')) self.timestamp = self._config.get('cache', 'timestamp', fallback=None) self.key_type = self._config.get('cache', 'key_type', fallback=None) + if not self._config.has_section('integrity'): + self._config.add_section('integrity') + try: + self.integrity = dict(self._config.items('integrity')) + except configparser.NoSectionError: + self.integrity = {} previous_location = self._config.get('cache', 'previous_location', fallback=None) if previous_location: self.previous_location = recanonicalize_relative_location(previous_location, self.repository) @@ -266,6 +274,8 @@ class CacheConfig: if key: self._config.set('cache', 'key_type', str(key.TYPE)) self._config.set('cache', 'previous_location', self.repository._location.canonical_path()) + for file, integrity_data in self.integrity.items(): + self._config.set('integrity', file, integrity_data) with SaveFile(self.config_path) as fd: self._config.write(fd) @@ -392,14 +402,16 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" with open(os.path.join(self.path, 'README'), 'w') as fd: fd.write(CACHE_README) self.cache_config.create() - ChunkIndex().write(os.path.join(self.path, 'chunks').encode('utf-8')) + ChunkIndex().write(os.path.join(self.path, 'chunks')) os.makedirs(os.path.join(self.path, 'chunks.archive.d')) with SaveFile(os.path.join(self.path, 'files'), binary=True) as fd: pass # empty file def _do_open(self): self.cache_config.load() - self.chunks = ChunkIndex.read(os.path.join(self.path, 'chunks').encode('utf-8')) + with IntegrityCheckedFile(path=os.path.join(self.path, 'chunks'), write=False, + integrity_data=self.cache_config.integrity.get('chunks')) as fd: + self.chunks = ChunkIndex.read(fd) self.files = None def open(self): @@ -417,7 +429,9 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" self.files = {} self._newest_mtime = None logger.debug('Reading files cache ...') - with open(os.path.join(self.path, 'files'), 'rb') as fd: + + with IntegrityCheckedFile(path=os.path.join(self.path, 'files'), write=False, + integrity_data=self.cache_config.integrity.get('files')) as fd: u = msgpack.Unpacker(use_list=True) while True: data = fd.read(64 * 1024) @@ -458,7 +472,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" self._newest_mtime = 2 ** 63 - 1 # nanoseconds, good until y2262 ttl = int(os.environ.get('BORG_FILES_CACHE_TTL', 20)) pi.output('Saving files cache') - with SaveFile(os.path.join(self.path, 'files'), binary=True) as fd: + with IntegrityCheckedFile(path=os.path.join(self.path, 'files'), write=True) as fd: for path_hash, item in self.files.items(): # Only keep files seen in this backup that are older than newest mtime seen in this backup - # this is to avoid issues with filesystem snapshots and mtime granularity. @@ -467,10 +481,13 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if entry.age == 0 and bigint_to_int(entry.mtime) < self._newest_mtime or \ entry.age > 0 and entry.age < ttl: msgpack.pack((path_hash, entry), fd) + self.cache_config.integrity['files'] = fd.integrity_data + pi.output('Saving chunks cache') + with IntegrityCheckedFile(path=os.path.join(self.path, 'chunks'), write=True) as fd: + self.chunks.write(fd) + self.cache_config.integrity['chunks'] = fd.integrity_data pi.output('Saving cache config') self.cache_config.save(self.manifest, self.key) - pi.output('Saving chunks cache') - self.chunks.write(os.path.join(self.path, 'chunks').encode('utf-8')) os.rename(os.path.join(self.path, 'txn.active'), os.path.join(self.path, 'txn.tmp')) shutil.rmtree(os.path.join(self.path, 'txn.tmp')) From 1dfe6930032540e03e57160c4ce80d5822e4cd24 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 13:49:03 +0200 Subject: [PATCH 0906/1387] cache: integrity checking in archive.chunks.d --- src/borg/cache.py | 22 ++++++++++++++-------- src/borg/crypto/file_integrity.py | 5 ++++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index fa2e6fe4..9a920a70 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -527,7 +527,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def mkpath(id, suffix=''): id_hex = bin_to_hex(id) path = os.path.join(archive_path, id_hex + suffix) - return path.encode('utf-8') + return path def cached_archives(): if self.do_cache: @@ -543,6 +543,10 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def cleanup_outdated(ids): for id in ids: os.unlink(mkpath(id)) + try: + os.unlink(mkpath(id) + '.integrity') + except FileNotFoundError: + pass def fetch_and_build_idx(archive_id, repository, key, chunk_idx): cdata = repository.get(archive_id) @@ -565,12 +569,13 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if self.do_cache: fn = mkpath(archive_id) fn_tmp = mkpath(archive_id, suffix='.tmp') - try: - chunk_idx.write(fn_tmp) - except Exception: - os.unlink(fn_tmp) - else: - os.rename(fn_tmp, fn) + with DetachedIntegrityCheckedFile(path=fn_tmp, write=True, filename=bin_to_hex(archive_id)) as fd: + try: + chunk_idx.write(fd) + except Exception: + os.unlink(fn_tmp) + else: + os.rename(fn_tmp, fn) def lookup_name(archive_id): for info in self.manifest.archives.list(): @@ -601,7 +606,8 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if archive_id in cached_ids: archive_chunk_idx_path = mkpath(archive_id) logger.info("Reading cached archive chunk index for %s ..." % archive_name) - archive_chunk_idx = ChunkIndex.read(archive_chunk_idx_path) + with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path, write=False) as fd: + archive_chunk_idx = ChunkIndex.read(fd) else: logger.info('Fetching and building archive index for %s ...' % archive_name) archive_chunk_idx = ChunkIndex() diff --git a/src/borg/crypto/file_integrity.py b/src/borg/crypto/file_integrity.py index 53cbcea3..de4dcba3 100644 --- a/src/borg/crypto/file_integrity.py +++ b/src/borg/crypto/file_integrity.py @@ -182,6 +182,9 @@ class IntegrityCheckedFile(FileLikeWrapper): class DetachedIntegrityCheckedFile(IntegrityCheckedFile): def __init__(self, path, write, filename=None, override_fd=None): super().__init__(path, write, filename, override_fd) + filename = filename or os.path.basename(path) + output_dir = os.path.dirname(path) + self.output_integrity_file = self.integrity_file_path(os.path.join(output_dir, filename)) if not write: self.digests = self.read_integrity_file(self.path, self.hasher) @@ -201,5 +204,5 @@ class DetachedIntegrityCheckedFile(IntegrityCheckedFile): raise FileIntegrityError(path) def store_integrity_data(self, data: str): - with open(self.integrity_file_path(self.path), 'w') as fd: + with open(self.output_integrity_file, 'w') as fd: fd.write(data) From addd7addfec2fbc8690210796494bd5e608068d5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 14:00:03 +0200 Subject: [PATCH 0907/1387] cache: chunks.archive.d: autofix corruption --- src/borg/cache.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 9a920a70..369735f2 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -24,7 +24,7 @@ from .helpers import remove_surrogates from .helpers import ProgressIndicatorPercent, ProgressIndicatorMessage from .item import ArchiveItem, ChunkListEntry from .crypto.key import PlaintextKey -from .crypto.file_integrity import IntegrityCheckedFile, DetachedIntegrityCheckedFile +from .crypto.file_integrity import IntegrityCheckedFile, DetachedIntegrityCheckedFile, FileIntegrityError from .locking import Lock from .platform import SaveFile from .remote import cache_if_remote @@ -542,11 +542,14 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def cleanup_outdated(ids): for id in ids: - os.unlink(mkpath(id)) - try: - os.unlink(mkpath(id) + '.integrity') - except FileNotFoundError: - pass + cleanup_cached_archive(id) + + def cleanup_cached_archive(id): + os.unlink(mkpath(id)) + try: + os.unlink(mkpath(id) + '.integrity') + except FileNotFoundError: + pass def fetch_and_build_idx(archive_id, repository, key, chunk_idx): cdata = repository.get(archive_id) @@ -606,9 +609,17 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if archive_id in cached_ids: archive_chunk_idx_path = mkpath(archive_id) logger.info("Reading cached archive chunk index for %s ..." % archive_name) - with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path, write=False) as fd: - archive_chunk_idx = ChunkIndex.read(fd) - else: + try: + with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path, write=False) as fd: + archive_chunk_idx = ChunkIndex.read(fd) + except FileIntegrityError as fie: + logger.error('Cached archive chunk index of %s is corrupted: %s', archive_name, fie) + # Delete it and fetch a new index + cleanup_cached_archive(archive_id) + cached_ids.remove(archive_id) + if archive_id not in cached_ids: + # Do not make this an else branch; the FileIntegrityError exception handler + # above can remove *archive_id* from *cached_ids*. logger.info('Fetching and building archive index for %s ...' % archive_name) archive_chunk_idx = ChunkIndex() fetch_and_build_idx(archive_id, repository, self.key, archive_chunk_idx) From f59affe5858d127680c06b108c5c3454bc29ac49 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 14:01:21 +0200 Subject: [PATCH 0908/1387] cache: fix possible printf issue with archive names in sync --- src/borg/cache.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 369735f2..ce8cef25 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -566,7 +566,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" for item in unpacker: if not isinstance(item, dict): logger.error('Error: Did not get expected metadata dict - archive corrupted!') - continue + continue # XXX: continue?! for chunk_id, size, csize in item.get(b'chunks', []): chunk_idx.add(chunk_id, 1, size, csize) if self.do_cache: @@ -589,9 +589,9 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" logger.info('Synchronizing chunks cache...') cached_ids = cached_archives() archive_ids = repo_archives() - logger.info('Archives: %d, w/ cached Idx: %d, w/ outdated Idx: %d, w/o cached Idx: %d.' % ( + logger.info('Archives: %d, w/ cached Idx: %d, w/ outdated Idx: %d, w/o cached Idx: %d.', len(archive_ids), len(cached_ids), - len(cached_ids - archive_ids), len(archive_ids - cached_ids), )) + len(cached_ids - archive_ids), len(archive_ids - cached_ids)) # deallocates old hashindex, creates empty hashindex: chunk_idx.clear() cleanup_outdated(cached_ids - archive_ids) @@ -608,7 +608,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if self.do_cache: if archive_id in cached_ids: archive_chunk_idx_path = mkpath(archive_id) - logger.info("Reading cached archive chunk index for %s ..." % archive_name) + logger.info("Reading cached archive chunk index for %s ...", archive_name) try: with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path, write=False) as fd: archive_chunk_idx = ChunkIndex.read(fd) @@ -620,7 +620,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if archive_id not in cached_ids: # Do not make this an else branch; the FileIntegrityError exception handler # above can remove *archive_id* from *cached_ids*. - logger.info('Fetching and building archive index for %s ...' % archive_name) + logger.info('Fetching and building archive index for %s ...', archive_name) archive_chunk_idx = ChunkIndex() fetch_and_build_idx(archive_id, repository, self.key, archive_chunk_idx) logger.info("Merging into master chunks index ...") @@ -633,7 +633,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" chunk_idx.merge(archive_chunk_idx) else: chunk_idx = chunk_idx or ChunkIndex() - logger.info('Fetching archive index for %s ...' % archive_name) + logger.info('Fetching archive index for %s ...', archive_name) fetch_and_build_idx(archive_id, repository, self.key, chunk_idx) if self.progress: pi.finish() From d463dd89aa8c93ed5dee3a51456ef9a5a398d0ca Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 15:18:45 +0200 Subject: [PATCH 0909/1387] hashindex: read/write: use hash_part for HashHeader --- src/borg/_hashindex.c | 27 +++++++++++++++++++++++++++ src/borg/testsuite/hashindex.py | 22 ++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/borg/_hashindex.c b/src/borg/_hashindex.c index 289457fe..b41d57b7 100644 --- a/src/borg/_hashindex.c +++ b/src/borg/_hashindex.c @@ -291,6 +291,20 @@ hashindex_read(PyObject *file_py) goto fail_decref_header; } + /* + * Hash the header + * If the header is corrupted this bails before doing something stupid (like allocating 3.8 TB of memory) + */ + Py_XDECREF(PyObject_CallMethod(file_py, "hash_part", "s", "HashHeader")); + if(PyErr_Occurred()) { + if(PyErr_ExceptionMatches(PyExc_AttributeError)) { + /* Be able to work with regular file objects which do not have a hash_part method. */ + PyErr_Clear(); + } else { + goto fail_decref_header; + } + } + /* Find length of file */ length_object = PyObject_CallMethod(file_py, "seek", "ni", (Py_ssize_t)0, SEEK_END); if(PyErr_Occurred()) { @@ -473,6 +487,19 @@ hashindex_write(HashIndex *index, PyObject *file_py) return; } + /* + * Hash the header + */ + Py_XDECREF(PyObject_CallMethod(file_py, "hash_part", "s", "HashHeader")); + if(PyErr_Occurred()) { + if(PyErr_ExceptionMatches(PyExc_AttributeError)) { + /* Be able to work with regular file objects which do not have a hash_part method. */ + PyErr_Clear(); + } else { + return; + } + } + /* Note: explicitly construct view; BuildValue can convert (pointer, length) to Python objects, but copies them for doing so */ buckets_view = PyMemoryView_FromMemory((char*)index->buckets, buckets_length, PyBUF_READ); if(!buckets_view) { diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 11639907..120c01b4 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -6,6 +6,7 @@ import zlib from ..hashindex import NSIndex, ChunkIndex from .. import hashindex +from ..crypto.file_integrity import IntegrityCheckedFile, FileIntegrityError from . import BaseTestCase # Note: these tests are part of the self test, do not use or import py.test functionality here. @@ -319,6 +320,27 @@ class HashIndexDataTestCase(BaseTestCase): assert idx1[H(3)] == (ChunkIndex.MAX_VALUE, 6, 7) +class HashIndexIntegrityTestCase(HashIndexDataTestCase): + def write_integrity_checked_index(self, tempdir): + idx = self._deserialize_hashindex(self.HASHINDEX) + file = os.path.join(tempdir, 'idx') + with IntegrityCheckedFile(path=file, write=True) as fd: + idx.write(fd) + integrity_data = fd.integrity_data + assert 'final' in integrity_data + assert 'HashHeader' in integrity_data + return file, integrity_data + + def test_integrity_checked_file(self): + with tempfile.TemporaryDirectory() as tempdir: + file, integrity_data = self.write_integrity_checked_index(tempdir) + with open(file, 'r+b') as fd: + fd.write(b'Foo') + with self.assert_raises(FileIntegrityError): + with IntegrityCheckedFile(path=file, write=False, integrity_data=integrity_data) as fd: + ChunkIndex.read(fd) + + class NSIndexTestCase(BaseTestCase): def test_nsindex_segment_limit(self): idx = NSIndex() From 83bca02a4e283afedd7e04dbe47064d1890d6497 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 15:31:30 +0200 Subject: [PATCH 0910/1387] file_integrity: hash_part: mix length into state --- src/borg/crypto/file_integrity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/borg/crypto/file_integrity.py b/src/borg/crypto/file_integrity.py index de4dcba3..eb796c67 100644 --- a/src/borg/crypto/file_integrity.py +++ b/src/borg/crypto/file_integrity.py @@ -152,6 +152,7 @@ class IntegrityCheckedFile(FileLikeWrapper): def hash_part(self, partname, is_final=False): if not self.writing and not self.digests: return + self.hasher.update(('%10d' % len(partname)).encode()) self.hasher.update(partname.encode()) self.hasher.hash_length(seek_to_end=is_final) digest = self.hasher.hexdigest() From 50ac9d914dd54cde5a9c69d8be0f5abcfc8a59c4 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 15:54:38 +0200 Subject: [PATCH 0911/1387] testsuite: add ArchiverCorruptionTestCase --- src/borg/cache.py | 2 ++ src/borg/testsuite/archiver.py | 64 ++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/borg/cache.py b/src/borg/cache.py index ce8cef25..aad48ca5 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -22,6 +22,7 @@ from .helpers import safe_ns from .helpers import yes, hostname_is_unique from .helpers import remove_surrogates from .helpers import ProgressIndicatorPercent, ProgressIndicatorMessage +from .helpers import set_ec, EXIT_WARNING from .item import ArchiveItem, ChunkListEntry from .crypto.key import PlaintextKey from .crypto.file_integrity import IntegrityCheckedFile, DetachedIntegrityCheckedFile, FileIntegrityError @@ -617,6 +618,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" # Delete it and fetch a new index cleanup_cached_archive(archive_id) cached_ids.remove(archive_id) + set_ec(EXIT_WARNING) if archive_id not in cached_ids: # Do not make this an else branch; the FileIntegrityError exception handler # above can remove *archive_id* from *cached_ids*. diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 508b1586..13c24106 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1,5 +1,6 @@ import argparse import errno +import io import json import logging import os @@ -37,6 +38,7 @@ from ..constants import * # NOQA from ..crypto.low_level import bytes_to_long, num_aes_blocks from ..crypto.key import KeyfileKeyBase, RepoKey, KeyfileKey, Passphrase, TAMRequiredError from ..crypto.keymanager import RepoIdMismatch, NotABorgKeyFile +from ..crypto.file_integrity import FileIntegrityError from ..helpers import Location, get_security_dir from ..helpers import Manifest from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR @@ -2886,6 +2888,68 @@ class RemoteArchiverTestCase(ArchiverTestCase): self.assert_true(marker not in res) +class ArchiverCorruptionTestCase(ArchiverTestCaseBase): + def corrupt(self, file): + with open(file, 'r+b') as fd: + fd.seek(-1, io.SEEK_END) + fd.write(b'1') + + def test_cache_chunks(self): + self.cmd('init', '--encryption=repokey', self.repository_location) + cache_path = json.loads(self.cmd('info', self.repository_location, '--json'))['cache']['path'] + self.corrupt(os.path.join(cache_path, 'chunks')) + + if self.FORK_DEFAULT: + out = self.cmd('info', self.repository_location, exit_code=2) + assert 'failed integrity check' in out + else: + with pytest.raises(FileIntegrityError): + self.cmd('info', self.repository_location) + + def test_cache_files(self): + self.create_test_files() + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + cache_path = json.loads(self.cmd('info', self.repository_location, '--json'))['cache']['path'] + self.corrupt(os.path.join(cache_path, 'files')) + + if self.FORK_DEFAULT: + out = self.cmd('create', self.repository_location + '::test1', 'input', exit_code=2) + assert 'failed integrity check' in out + else: + with pytest.raises(FileIntegrityError): + self.cmd('create', self.repository_location + '::test1', 'input') + + def test_chunks_archive(self): + self.create_test_files() + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test1', 'input') + # Find ID of test1 so we can corrupt it later :) + target_id = self.cmd('list', self.repository_location, '--format={id}{LF}').strip() + self.cmd('create', self.repository_location + '::test2', 'input') + self.cmd('delete', '--cache-only', self.repository_location) + + cache_path = json.loads(self.cmd('info', self.repository_location, '--json'))['cache']['path'] + chunks_archive = os.path.join(cache_path, 'chunks.archive.d') + assert len(os.listdir(chunks_archive)) == 4 # two archives, one chunks cache and one .integrity file each + + self.corrupt(os.path.join(chunks_archive, target_id)) + + # Trigger cache sync by changing the manifest ID in the cache config + config_path = os.path.join(cache_path, 'config') + config = ConfigParser(interpolation=None) + config.read(config_path) + config.set('cache', 'manifest', bin_to_hex(bytes(32))) + with open(config_path, 'w') as fd: + config.write(fd) + + # Cache sync will notice corrupted archive chunks, but automatically recover. + out = self.cmd('create', '-v', self.repository_location + '::test3', 'input', exit_code=1) + assert 'Reading cached archive chunk index for test1' in out + assert 'Cached archive chunk index of test1 is corrupted' in out + assert 'Fetching and building archive index for test1' in out + + class DiffArchiverTestCase(ArchiverTestCaseBase): def test_basic_functionality(self): # Initialize test folder From d35d388d9ce9cd183da3ee8551228c70bbf88d36 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 16:13:40 +0200 Subject: [PATCH 0912/1387] cache integrity: handle interference from old versions --- src/borg/cache.py | 20 ++++++++++++++++---- src/borg/testsuite/archiver.py | 17 +++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index aad48ca5..9af9dbf9 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -240,6 +240,7 @@ class CacheConfig: config.set('cache', 'repository', self.repository.id_str) config.set('cache', 'manifest', '') config.add_section('integrity') + config.set('integrity', 'manifest', '') with SaveFile(self.config_path) as fd: config.write(fd) @@ -256,11 +257,19 @@ class CacheConfig: self.manifest_id = unhexlify(self._config.get('cache', 'manifest')) self.timestamp = self._config.get('cache', 'timestamp', fallback=None) self.key_type = self._config.get('cache', 'key_type', fallback=None) - if not self._config.has_section('integrity'): - self._config.add_section('integrity') try: self.integrity = dict(self._config.items('integrity')) + if self._config.get('cache', 'manifest') != self.integrity.pop('manifest'): + # The cache config file is updated (parsed with ConfigParser, the state of the ConfigParser + # is modified and then written out.), not re-created. + # Thus, older versions will leave our [integrity] section alone, making the section's data invalid. + # Therefore, we also add the manifest ID to this section and + # can discern whether an older version interfere by comparing the manifest IDs of this section + # and the main [cache] section. + self.integrity = {} + logger.warning('Cache integrity data lost: old Borg version modified the cache.') except configparser.NoSectionError: + logger.debug('Cache integrity: No integrity data found (files, chunks). Cache is from old version.') self.integrity = {} previous_location = self._config.get('cache', 'previous_location', fallback=None) if previous_location: @@ -272,11 +281,14 @@ class CacheConfig: if manifest: self._config.set('cache', 'manifest', manifest.id_str) self._config.set('cache', 'timestamp', manifest.timestamp) + if not self._config.has_section('integrity'): + self._config.add_section('integrity') + for file, integrity_data in self.integrity.items(): + self._config.set('integrity', file, integrity_data) + self._config.set('integrity', 'manifest', manifest.id_str) if key: self._config.set('cache', 'key_type', str(key.TYPE)) self._config.set('cache', 'previous_location', self.repository._location.canonical_path()) - for file, integrity_data in self.integrity.items(): - self._config.set('integrity', file, integrity_data) with SaveFile(self.config_path) as fd: self._config.write(fd) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 13c24106..a2d0ff80 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2949,6 +2949,23 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): assert 'Cached archive chunk index of test1 is corrupted' in out assert 'Fetching and building archive index for test1' in out + def test_old_version_intefered(self): + self.create_test_files() + self.cmd('init', '--encryption=repokey', self.repository_location) + cache_path = json.loads(self.cmd('info', self.repository_location, '--json'))['cache']['path'] + + # Modify the main manifest ID without touching the manifest ID in the integrity section. + # This happens if a version without integrity checking modifies the cache. + config_path = os.path.join(cache_path, 'config') + config = ConfigParser(interpolation=None) + config.read(config_path) + config.set('cache', 'manifest', bin_to_hex(bytes(32))) + with open(config_path, 'w') as fd: + config.write(fd) + + out = self.cmd('info', self.repository_location) + assert 'Cache integrity data lost: old Borg version modified the cache.' in out + class DiffArchiverTestCase(ArchiverTestCaseBase): def test_basic_functionality(self): From 0a295dd753ad42e925cf4771dc3b87b248456dc7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 17:55:42 +0200 Subject: [PATCH 0913/1387] setup.py clean to remove compiled files --- setup.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5e2d309d..5b6ab972 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ if sys.platform.startswith('freebsd'): from setuptools import setup, find_packages, Extension from setuptools.command.sdist import sdist +from distutils.command.clean import clean compress_source = 'src/borg/compress.pyx' crypto_ll_source = 'src/borg/crypto/low_level.pyx' @@ -567,11 +568,30 @@ class build_man(Command): write(option.ljust(padding), desc) +class Clean(clean): + def run(self): + super().run() + for source in cython_sources: + genc = source.replace('.pyx', '.c') + try: + os.unlink(genc) + print('rm', genc) + except FileNotFoundError: + pass + compiled_glob = source.replace('.pyx', '.cpython*') + for compiled in glob(compiled_glob): + try: + os.unlink(compiled) + print('rm', compiled) + except FileNotFoundError: + pass + cmdclass = { 'build_ext': build_ext, 'build_usage': build_usage, 'build_man': build_man, - 'sdist': Sdist + 'sdist': Sdist, + 'clean': Clean, } ext_modules = [] From b57fb1a5e5d54d8ecd4984c2246d47f83f4bc9e0 Mon Sep 17 00:00:00 2001 From: edgimar Date: Sun, 28 May 2017 00:52:56 -0400 Subject: [PATCH 0914/1387] Update changes.rst fix denglish --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index e67e3ab3..ffc7603b 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,7 +4,7 @@ Important notes =============== -This section is used for infos about security and corruption issues. +This section provides information about security and corruption issues. .. _tam_vuln: From 0a5d9b6f7cae7d75f183d9b3a959558393dee4f5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 31 May 2017 18:06:28 +0200 Subject: [PATCH 0915/1387] cache sync: close archive chunks file before renaming --- src/borg/cache.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 9af9dbf9..4b59dfbd 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -585,13 +585,14 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" if self.do_cache: fn = mkpath(archive_id) fn_tmp = mkpath(archive_id, suffix='.tmp') - with DetachedIntegrityCheckedFile(path=fn_tmp, write=True, filename=bin_to_hex(archive_id)) as fd: - try: + try: + with DetachedIntegrityCheckedFile(path=fn_tmp, write=True, + filename=bin_to_hex(archive_id)) as fd: chunk_idx.write(fd) - except Exception: - os.unlink(fn_tmp) - else: - os.rename(fn_tmp, fn) + except Exception: + os.unlink(fn_tmp) + else: + os.rename(fn_tmp, fn) def lookup_name(archive_id): for info in self.manifest.archives.list(): From 9032aa062b9487f8737cfb6ab5efb67924a72127 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 31 May 2017 18:08:02 +0200 Subject: [PATCH 0916/1387] testsuite: simplify ArchiverCorruptionTestCase --- src/borg/cache.py | 4 ++-- src/borg/crypto/file_integrity.py | 6 ++--- src/borg/testsuite/archiver.py | 39 ++++++++++++++----------------- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 4b59dfbd..13045f0e 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -264,10 +264,10 @@ class CacheConfig: # is modified and then written out.), not re-created. # Thus, older versions will leave our [integrity] section alone, making the section's data invalid. # Therefore, we also add the manifest ID to this section and - # can discern whether an older version interfere by comparing the manifest IDs of this section + # can discern whether an older version interfered by comparing the manifest IDs of this section # and the main [cache] section. self.integrity = {} - logger.warning('Cache integrity data lost: old Borg version modified the cache.') + logger.warning('Cache integrity data not available: old Borg version modified the cache.') except configparser.NoSectionError: logger.debug('Cache integrity: No integrity data found (files, chunks). Cache is from old version.') self.integrity = {} diff --git a/src/borg/crypto/file_integrity.py b/src/borg/crypto/file_integrity.py index eb796c67..032b8672 100644 --- a/src/borg/crypto/file_integrity.py +++ b/src/borg/crypto/file_integrity.py @@ -135,13 +135,13 @@ class IntegrityCheckedFile(FileLikeWrapper): @classmethod def parse_integrity_data(cls, path: str, data: str, hasher: SHA512FileHashingWrapper): try: - integrity_file = json.loads(data) + integrity_data = json.loads(data) # Provisions for agility now, implementation later, but make sure the on-disk joint is oiled. - algorithm = integrity_file['algorithm'] + algorithm = integrity_data['algorithm'] if algorithm != hasher.ALGORITHM: logger.warning('Cannot verify integrity of %s: Unknown algorithm %r', path, algorithm) return - digests = integrity_file['digests'] + digests = integrity_data['digests'] # Require at least presence of the final digest digests['final'] return digests diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index a2d0ff80..36d32ba8 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2889,15 +2889,19 @@ class RemoteArchiverTestCase(ArchiverTestCase): class ArchiverCorruptionTestCase(ArchiverTestCaseBase): + def setUp(self): + super().setUp() + self.create_test_files() + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cache_path = json.loads(self.cmd('info', self.repository_location, '--json'))['cache']['path'] + def corrupt(self, file): with open(file, 'r+b') as fd: fd.seek(-1, io.SEEK_END) fd.write(b'1') def test_cache_chunks(self): - self.cmd('init', '--encryption=repokey', self.repository_location) - cache_path = json.loads(self.cmd('info', self.repository_location, '--json'))['cache']['path'] - self.corrupt(os.path.join(cache_path, 'chunks')) + self.corrupt(os.path.join(self.cache_path, 'chunks')) if self.FORK_DEFAULT: out = self.cmd('info', self.repository_location, exit_code=2) @@ -2907,11 +2911,8 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): self.cmd('info', self.repository_location) def test_cache_files(self): - self.create_test_files() - self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') - cache_path = json.loads(self.cmd('info', self.repository_location, '--json'))['cache']['path'] - self.corrupt(os.path.join(cache_path, 'files')) + self.corrupt(os.path.join(self.cache_path, 'files')) if self.FORK_DEFAULT: out = self.cmd('create', self.repository_location + '::test1', 'input', exit_code=2) @@ -2921,42 +2922,38 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): self.cmd('create', self.repository_location + '::test1', 'input') def test_chunks_archive(self): - self.create_test_files() - self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test1', 'input') # Find ID of test1 so we can corrupt it later :) target_id = self.cmd('list', self.repository_location, '--format={id}{LF}').strip() self.cmd('create', self.repository_location + '::test2', 'input') - self.cmd('delete', '--cache-only', self.repository_location) - cache_path = json.loads(self.cmd('info', self.repository_location, '--json'))['cache']['path'] - chunks_archive = os.path.join(cache_path, 'chunks.archive.d') + # Force cache sync, creating archive chunks of test1 and test2 in chunks.archive.d + self.cmd('delete', '--cache-only', self.repository_location) + self.cmd('info', self.repository_location, '--json') + + chunks_archive = os.path.join(self.cache_path, 'chunks.archive.d') assert len(os.listdir(chunks_archive)) == 4 # two archives, one chunks cache and one .integrity file each self.corrupt(os.path.join(chunks_archive, target_id)) # Trigger cache sync by changing the manifest ID in the cache config - config_path = os.path.join(cache_path, 'config') + config_path = os.path.join(self.cache_path, 'config') config = ConfigParser(interpolation=None) config.read(config_path) config.set('cache', 'manifest', bin_to_hex(bytes(32))) with open(config_path, 'w') as fd: config.write(fd) - # Cache sync will notice corrupted archive chunks, but automatically recover. + # Cache sync notices corrupted archive chunks, but automatically recovers. out = self.cmd('create', '-v', self.repository_location + '::test3', 'input', exit_code=1) assert 'Reading cached archive chunk index for test1' in out assert 'Cached archive chunk index of test1 is corrupted' in out assert 'Fetching and building archive index for test1' in out - def test_old_version_intefered(self): - self.create_test_files() - self.cmd('init', '--encryption=repokey', self.repository_location) - cache_path = json.loads(self.cmd('info', self.repository_location, '--json'))['cache']['path'] - + def test_old_version_interfered(self): # Modify the main manifest ID without touching the manifest ID in the integrity section. # This happens if a version without integrity checking modifies the cache. - config_path = os.path.join(cache_path, 'config') + config_path = os.path.join(self.cache_path, 'config') config = ConfigParser(interpolation=None) config.read(config_path) config.set('cache', 'manifest', bin_to_hex(bytes(32))) @@ -2964,7 +2961,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): config.write(fd) out = self.cmd('info', self.repository_location) - assert 'Cache integrity data lost: old Borg version modified the cache.' in out + assert 'Cache integrity data not available: old Borg version modified the cache.' in out class DiffArchiverTestCase(ArchiverTestCaseBase): From 4edf77788d7826b09a9541889d5d6daa3a80cf6c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 00:59:39 +0200 Subject: [PATCH 0917/1387] Implement storage quotas --- docs/internals/data-structures.rst | 61 ++++++++++++++++++++++++++++++ docs/usage/init.rst.inc | 2 + docs/usage/serve.rst.inc | 2 + src/borg/archiver.py | 23 ++++++++++- src/borg/remote.py | 7 +++- src/borg/repository.py | 45 ++++++++++++++++++++-- src/borg/testsuite/repository.py | 48 +++++++++++++++++++++++ 7 files changed, 182 insertions(+), 6 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 400a2d6b..e7fbb496 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -185,6 +185,67 @@ commit logic) showing the principal operation of compaction: (The actual algorithm is more complex to avoid various consistency issues, refer to the ``borg.repository`` module for more comments and documentation on these issues.) +.. _internals_storage_quota: + +Storage quotas +~~~~~~~~~~~~~~ + +Quotas are implemented at the Repository level. The active quota of a repository +is determined by the ``storage_quota`` `config` entry or a run-time override (via :ref:`borg_serve`). +The currently used quota is stored in the hints file. Operations (PUT and DELETE) during +a transaction modify the currently used quota: + +- A PUT adds the size of the *log entry* to the quota, + i.e. the length of the data plus the 41 byte header. +- A DELETE subtracts the size of the deleted log entry from the quota, + which includes the header. + +Thus, PUT and DELETE are symmetric and cancel each other out precisely. + +The quota does not track on-disk size overheads (due to conditional compaction +or append-only mode). In normal operation the inclusion of the log entry headers +in the quota act as a faithful proxy for index and hints overheads. + +By tracking effective content size, the client can *always* recover from a full quota +by deleting archives. This would not be possible if the quota tracked on-disk size, +since journaling DELETEs requires extra disk space before space is freed. +Tracking effective size on the other hand accounts DELETEs immediately as freeing quota. + +.. rubric:: Enforcing the quota + +The storage quota is meant as a robust mechanism for service providers, therefore +:ref:`borg_serve` has to enforce it without loopholes (e.g. modified clients). + +The quota is enforcible only if *all* :ref:`borg_serve` versions +accessible to clients support quotas (see next section). Further, quota is +per repository. Therefore, ensure clients can only access a defined set of repositories +with their quotas set, using ``--restrict-to-path``. + +If the client exceeds the storage quota the ``StorageQuotaExceeded`` exception is +raised. Normally a client could ignore such an exception and just send a ``commit()`` +command anyway, circumventing the quota. However, when ``StorageQuotaExceeded`` is raised, +it is stored in the ``transaction_doomed`` attribute of the repository. +If the transaction is doomed, then commit will re-raise this exception, aborting the commit. + +The transaction_doomed indicator is reset on a rollback (which erases the quota-exceeding +state). + +.. rubric:: Compatibility with older servers and enabling quota after-the-fact + +If no quota data is stored in the hints file, Borg assumes zero quota is used. +Thus, if a repository with an enabled quota is written to with an older version +that does not understand quotas, then the quota usage will be erased. + +A similar situation arises when upgrading from a Borg release that did not have quotas. +Borg will start tracking quota use from the time of the upgrade, starting at zero. + +If the quota shall be enforced accurately in these cases, either + +- delete the ``index.N`` and ``hints.N`` files, forcing Borg to rebuild both, + re-acquiring quota data in the process, or +- edit the msgpacked ``hints.N`` file (not recommended and thus not + documented further). + .. _manifest: The manifest diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index a5a6dbfd..36262431 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -17,6 +17,8 @@ optional arguments | select encryption key mode **(required)** ``-a``, ``--append-only`` | create an append-only mode repository + ``--storage-quota`` + | Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota. `Common options`_ | diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index f3f1aa65..dab3c32f 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -13,6 +13,8 @@ optional arguments | restrict repository access to PATH. Can be specified multiple times to allow the client access to several directories. Access to all sub-directories is granted implicitly; PATH doesn't need to directly point to a repository. ``--append-only`` | only allow appending to repository segment files + ``--storage-quota`` + | Override storage quota of the repository (e.g. 5G, 1.5T). When a new repository is initialized, sets the storage quota on the new repository as well. Default: no quota. `Common options`_ | diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 8496e1d0..419e46e1 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -46,7 +46,7 @@ from .helpers import Error, NoManifestError, set_ec from .helpers import location_validator, archivename_validator, ChunkerParams from .helpers import PrefixSpec, SortBySpec, HUMAN_SORT_KEYS from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter -from .helpers import format_timedelta, format_file_size, format_archive +from .helpers import format_timedelta, format_file_size, parse_file_size, format_archive from .helpers import safe_encode, remove_surrogates, bin_to_hex, prepare_dump_dict from .helpers import prune_within, prune_split from .helpers import timestamp @@ -142,6 +142,13 @@ def with_archive(method): return wrapper +def parse_storage_quota(storage_quota): + parsed = parse_file_size(storage_quota) + if parsed < parse_file_size('10M'): + raise argparse.ArgumentTypeError('quota is too small (%s). At least 10M are required.' % storage_quota) + return parsed + + class Archiver: def __init__(self, lock_wait=None, prog=None): @@ -206,7 +213,11 @@ class Archiver: def do_serve(self, args): """Start in server mode. This command is usually not used manually.""" - return RepositoryServer(restrict_to_paths=args.restrict_to_paths, append_only=args.append_only).serve() + return RepositoryServer( + restrict_to_paths=args.restrict_to_paths, + append_only=args.append_only, + storage_quota=args.storage_quota, + ).serve() @with_repository(create=True, exclusive=True, manifest=False) def do_init(self, args, repository): @@ -2330,6 +2341,11 @@ class Archiver: 'Access to all sub-directories is granted implicitly; PATH doesn\'t need to directly point to a repository.') subparser.add_argument('--append-only', dest='append_only', action='store_true', help='only allow appending to repository segment files') + subparser.add_argument('--storage-quota', dest='storage_quota', default=None, + type=parse_storage_quota, + help='Override storage quota of the repository (e.g. 5G, 1.5T). ' + 'When a new repository is initialized, sets the storage quota on the new ' + 'repository as well. Default: no quota.') init_epilog = process_epilog(""" This command initializes an empty repository. A repository is a filesystem @@ -2420,6 +2436,9 @@ class Archiver: help='select encryption key mode **(required)**') subparser.add_argument('-a', '--append-only', dest='append_only', action='store_true', help='create an append-only mode repository') + subparser.add_argument('--storage-quota', dest='storage_quota', default=None, + type=parse_storage_quota, + help='Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota.') check_epilog = process_epilog(""" The check command verifies the consistency of a repository and the corresponding archives. diff --git a/src/borg/remote.py b/src/borg/remote.py index 1a67930e..f2c1e48a 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -178,7 +178,7 @@ class RepositoryServer: # pragma: no cover 'inject_exception', ) - def __init__(self, restrict_to_paths, append_only): + def __init__(self, restrict_to_paths, append_only, storage_quota): self.repository = None self.restrict_to_paths = restrict_to_paths # This flag is parsed from the serve command line via Archiver.do_serve, @@ -186,6 +186,7 @@ class RepositoryServer: # pragma: no cover # whatever the client wants, except when initializing a new repository # (see RepositoryServer.open below). self.append_only = append_only + self.storage_quota = storage_quota self.client_version = parse_version('1.0.8') # fallback version if client is too old to send version information def positional_to_named(self, method, argv): @@ -360,6 +361,7 @@ class RepositoryServer: # pragma: no cover append_only = (not create and self.append_only) or append_only self.repository = Repository(path, create, lock_wait=lock_wait, lock=lock, append_only=append_only, + storage_quota=self.storage_quota, exclusive=exclusive) self.repository.__enter__() # clean exit handled by serve() method return self.repository.id @@ -671,6 +673,9 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. topic = 'borg.debug.' + topic if 'repository' in topic: opts.append('--debug-topic=%s' % topic) + + if 'storage_quota' in args and args.storage_quota: + opts.append('--storage-quota=%s' % args.storage_quota) env_vars = [] if not hostname_is_unique(): env_vars.append('BORG_HOSTNAME_IS_UNIQUE=no') diff --git a/src/borg/repository.py b/src/borg/repository.py index 723cf8db..1caf4277 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -107,10 +107,14 @@ class Repository: class InsufficientFreeSpaceError(Error): """Insufficient free space to complete transaction (required: {}, available: {}).""" - def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True, append_only=False): + class StorageQuotaExceeded(Error): + """The storage quota ({}) has been exceeded ({}). Try deleting some archives.""" + + def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True, + append_only=False, storage_quota=None): self.path = os.path.abspath(path) self._location = Location('file://%s' % self.path) - self.io = None + self.io = None # type: LoggedIO self.lock = None self.index = None # This is an index of shadowed log entries during this transaction. Consider the following sequence: @@ -124,6 +128,9 @@ class Repository: self.created = False self.exclusive = exclusive self.append_only = append_only + self.storage_quota = storage_quota + self.storage_quota_use = 0 + self.transaction_doomed = None def __del__(self): if self.lock: @@ -209,6 +216,10 @@ class Repository: config.set('repository', 'segments_per_dir', str(DEFAULT_SEGMENTS_PER_DIR)) config.set('repository', 'max_segment_size', str(DEFAULT_MAX_SEGMENT_SIZE)) config.set('repository', 'append_only', str(int(self.append_only))) + if self.storage_quota: + config.set('repository', 'storage_quota', str(self.storage_quota)) + else: + config.set('repository', 'storage_quota', '0') config.set('repository', 'additional_free_space', '0') config.set('repository', 'id', bin_to_hex(os.urandom(32))) self.save_config(path, config) @@ -331,6 +342,9 @@ class Repository: # append_only can be set in the constructor # it shouldn't be overridden (True -> False) here self.append_only = self.append_only or self.config.getboolean('repository', 'append_only', fallback=False) + if self.storage_quota is None: + # self.storage_quota is None => no explicit storage_quota was specified, use repository setting. + self.storage_quota = self.config.getint('repository', 'storage_quota', fallback=0) self.id = unhexlify(self.config.get('repository', 'id').strip()) self.io = LoggedIO(self.path, self.max_segment_size, self.segments_per_dir) @@ -346,7 +360,12 @@ class Repository: """Commit transaction """ # save_space is not used anymore, but stays for RPC/API compatibility. + if self.transaction_doomed: + exception = self.transaction_doomed + self.rollback() + raise exception self.check_free_space() + self.log_storage_quota() self.io.write_commit() if not self.append_only: self.compact_segments() @@ -398,6 +417,7 @@ class Repository: if transaction_id is None: self.segments = {} # XXX bad name: usage_count_of_segment_x = self.segments[x] self.compact = FreeSpace() # XXX bad name: freeable_space_of_segment_x = self.compact[x] + self.storage_quota_use = 0 self.shadow_index.clear() else: if do_cleanup: @@ -420,6 +440,7 @@ class Repository: logger.debug('Upgrading from v1 hints.%d', transaction_id) self.segments = hints[b'segments'] self.compact = FreeSpace() + self.storage_quota_use = 0 for segment in sorted(hints[b'compact']): logger.debug('Rebuilding sparse info for segment %d', segment) self._rebuild_sparse(segment) @@ -429,6 +450,8 @@ class Repository: else: self.segments = hints[b'segments'] self.compact = FreeSpace(hints[b'compact']) + self.storage_quota_use = hints.get(b'storage_quota_use', 0) + self.log_storage_quota() # Drop uncommitted segments in the shadow index for key, shadowed_segments in self.shadow_index.items(): for segment in list(shadowed_segments): @@ -438,7 +461,8 @@ class Repository: def write_index(self): hints = {b'version': 2, b'segments': self.segments, - b'compact': self.compact} + b'compact': self.compact, + b'storage_quota_use': self.storage_quota_use, } transaction_id = self.io.get_segments_transaction_id() assert transaction_id is not None hints_file = os.path.join(self.path, 'hints.%d' % transaction_id) @@ -515,6 +539,11 @@ class Repository: formatted_free = format_file_size(free_space) raise self.InsufficientFreeSpaceError(formatted_required, formatted_free) + def log_storage_quota(self): + if self.storage_quota: + logger.info('Storage quota: %s out of %s used.', + format_file_size(self.storage_quota_use), format_file_size(self.storage_quota)) + def compact_segments(self): """Compact sparse segments by copying data into new segments """ @@ -672,6 +701,7 @@ class Repository: pass self.index[key] = segment, offset self.segments[segment] += 1 + self.storage_quota_use += size elif tag == TAG_DELETE: try: # if the deleted PUT is not in the index, there is nothing to clean up @@ -684,6 +714,7 @@ class Repository: # is already gone, then it was already compacted. self.segments[s] -= 1 size = self.io.read(s, offset, key, read_data=False) + self.storage_quota_use -= size self.compact[s] += size elif tag == TAG_COMMIT: continue @@ -821,6 +852,7 @@ class Repository: self.io.cleanup(self.io.get_segments_transaction_id()) self.index = None self._active_txn = False + self.transaction_doomed = None def rollback(self): # note: when used in remote mode, this is time limited, see RemoteRepository.shutdown_time. @@ -915,14 +947,20 @@ class Repository: else: self.segments[segment] -= 1 size = self.io.read(segment, offset, id, read_data=False) + self.storage_quota_use -= size self.compact[segment] += size segment, size = self.io.write_delete(id) self.compact[segment] += size self.segments.setdefault(segment, 0) segment, offset = self.io.write_put(id, data) + self.storage_quota_use += len(data) + self.io.put_header_fmt.size self.segments.setdefault(segment, 0) self.segments[segment] += 1 self.index[id] = segment, offset + if self.storage_quota and self.storage_quota_use > self.storage_quota: + self.transaction_doomed = self.StorageQuotaExceeded( + format_file_size(self.storage_quota), format_file_size(self.storage_quota_use)) + raise self.transaction_doomed def delete(self, id, wait=True): """delete a repo object @@ -939,6 +977,7 @@ class Repository: self.shadow_index.setdefault(id, []).append(segment) self.segments[segment] -= 1 size = self.io.read(segment, offset, id, read_data=False) + self.storage_quota_use -= size self.compact[segment] += size segment, size = self.io.write_delete(id) self.compact[segment] += size diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 16f47b91..f9c270ec 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -415,6 +415,43 @@ class RepositoryFreeSpaceTestCase(RepositoryTestCaseBase): assert not os.path.exists(self.repository.path) +class QuotaTestCase(RepositoryTestCaseBase): + def test_tracking(self): + assert self.repository.storage_quota_use == 0 + self.repository.put(H(1), bytes(1234)) + assert self.repository.storage_quota_use == 1234 + 41 + self.repository.put(H(2), bytes(5678)) + assert self.repository.storage_quota_use == 1234 + 5678 + 2 * 41 + self.repository.delete(H(1)) + assert self.repository.storage_quota_use == 5678 + 41 + self.repository.commit() + self.reopen() + with self.repository: + # Open new transaction; hints and thus quota data is not loaded unless needed. + self.repository.put(H(3), b'') + self.repository.delete(H(3)) + assert self.repository.storage_quota_use == 5678 + 41 + + def test_exceed_quota(self): + assert self.repository.storage_quota_use == 0 + self.repository.storage_quota = 50 + self.repository.put(H(1), b'') + assert self.repository.storage_quota_use == 41 + self.repository.commit() + with pytest.raises(Repository.StorageQuotaExceeded): + self.repository.put(H(2), b'') + assert self.repository.storage_quota_use == 82 + with pytest.raises(Repository.StorageQuotaExceeded): + self.repository.commit() + assert self.repository.storage_quota_use == 82 + self.reopen() + with self.repository: + self.repository.storage_quota = 50 + # Open new transaction; hints and thus quota data is not loaded unless needed. + self.repository.put(H(1), b'') + assert self.repository.storage_quota_use == 41 + + class NonceReservation(RepositoryTestCaseBase): def test_get_free_nonce_asserts(self): self.reopen(exclusive=False) @@ -641,6 +678,7 @@ class RepositoryCheckTestCase(RepositoryTestCaseBase): @pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteRepositoryTestCase(RepositoryTestCase): + repository = None # type: RemoteRepository def open(self, create=False): return RemoteRepository(Location('__testsuite__:' + os.path.join(self.tmppath, 'repository')), @@ -716,6 +754,10 @@ class RemoteRepositoryTestCase(RepositoryTestCase): umask = 0o077 debug_topics = [] + def __contains__(self, item): + # To behave like argparse.Namespace + return hasattr(self, item) + assert self.repository.borg_cmd(None, testing=True) == [sys.executable, '-m', 'borg.archiver', 'serve'] args = MockArgs() # XXX without next line we get spurious test fails when using pytest-xdist, root cause unknown: @@ -727,6 +769,12 @@ class RemoteRepositoryTestCase(RepositoryTestCase): args.debug_topics = ['something_client_side', 'repository_compaction'] assert self.repository.borg_cmd(args, testing=False) == ['borg-0.28.2', 'serve', '--umask=077', '--info', '--debug-topic=borg.debug.repository_compaction'] + args = MockArgs() + args.storage_quota = 0 + assert self.repository.borg_cmd(args, testing=False) == ['borg', 'serve', '--umask=077', '--info'] + args.storage_quota = 314159265 + assert self.repository.borg_cmd(args, testing=False) == ['borg', 'serve', '--umask=077', '--info', + '--storage-quota=314159265'] class RemoteLegacyFree(RepositoryTestCaseBase): From f8b48dc8d7c37288336fe024a40352b1cacbee02 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 31 May 2017 18:48:48 +0200 Subject: [PATCH 0918/1387] remote: propagate Error.traceback correctly --- src/borg/archiver.py | 2 +- src/borg/remote.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 419e46e1..f6bb69e6 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -4000,7 +4000,7 @@ def main(): # pragma: no cover tb = "%s\n%s" % (traceback.format_exc(), sysinfo()) exit_code = e.exit_code except RemoteRepository.RPCError as e: - important = e.exception_class not in ('LockTimeout', ) + important = e.exception_class not in ('LockTimeout', ) and e.traceback msgid = e.exception_class tb_log_level = logging.ERROR if important else logging.DEBUG if important: diff --git a/src/borg/remote.py b/src/borg/remote.py index f2c1e48a..47c59741 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -253,8 +253,10 @@ class RepositoryServer: # pragma: no cover if dictFormat: ex_short = traceback.format_exception_only(e.__class__, e) ex_full = traceback.format_exception(*sys.exc_info()) + ex_trace = True if isinstance(e, Error): ex_short = [e.get_message()] + ex_trace = e.traceback if isinstance(e, (Repository.DoesNotExist, Repository.AlreadyExists, PathNotAllowed)): # These exceptions are reconstructed on the client end in RemoteRepository.call_many(), # and will be handled just like locally raised exceptions. Suppress the remote traceback @@ -269,6 +271,7 @@ class RepositoryServer: # pragma: no cover b'exception_args': e.args, b'exception_full': ex_full, b'exception_short': ex_short, + b'exception_trace': ex_trace, b'sysinfo': sysinfo()}) except TypeError: msg = msgpack.packb({MSGID: msgid, @@ -277,6 +280,7 @@ class RepositoryServer: # pragma: no cover for x in e.args], b'exception_full': ex_full, b'exception_short': ex_short, + b'exception_trace': ex_trace, b'sysinfo': sysinfo()}) os_write(stdout_fd, msg) @@ -485,6 +489,10 @@ class RemoteRepository: else: return self.exception_class + @property + def traceback(self): + return self.unpacked.get(b'exception_trace', True) + @property def exception_class(self): return self.unpacked[b'exception_class'].decode() From 578b76af3ab5ed09dfb8bfc4bd581b2be849a129 Mon Sep 17 00:00:00 2001 From: TuXicc Date: Wed, 31 May 2017 19:25:21 +0200 Subject: [PATCH 0919/1387] Added BORG_PASSCOMMAND environment variable (#2573) --- docs/faq.rst | 10 +++++----- docs/quickstart.rst | 2 ++ docs/usage_general.rst.inc | 12 ++++++++++-- src/borg/crypto/key.py | 24 ++++++++++++++++++++++-- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index c251417c..cb9c3a2a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -257,11 +257,11 @@ Security How can I specify the encryption passphrase programmatically? ------------------------------------------------------------- -The encryption passphrase can be specified programmatically using the -`BORG_PASSPHRASE` environment variable. This is convenient when setting up -automated encrypted backups. Another option is to use -key file based encryption with a blank passphrase. See -:ref:`encrypted_repos` for more details. +The encryption passphrase or a command to retrieve the passphrase can be +specified programmatically using the `BORG_PASSPHRASE` or `BORG_PASSCOMMAND` +environment variables. This is convenient when setting up automated encrypted +backups. Another option is to use key file based encryption with a blank passphrase. +See :ref:`encrypted_repos` for more details. .. _password_env: .. note:: Be careful how you set the environment; using the ``env`` diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 466b8306..ffc42e35 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -86,6 +86,8 @@ backed up and that the ``prune`` command is keeping and deleting the correct bac # Setting this, so you won't be asked for your repository passphrase: export BORG_PASSPHRASE='XYZl0ngandsecurepa_55_phrasea&&123' + # or this to ask an external program to supply the passphrase: + export BORG_PASSCOMMAND='pass show backup' # some helpers and error handling: function info () { echo -e "\n"`date` $@"\n" >&2; } diff --git a/docs/usage_general.rst.inc b/docs/usage_general.rst.inc index 2b8c630e..726ac624 100644 --- a/docs/usage_general.rst.inc +++ b/docs/usage_general.rst.inc @@ -131,12 +131,20 @@ General: can either leave it away or abbreviate as `::`, if a positional parameter is required. BORG_PASSPHRASE When set, use the value to answer the passphrase question for encrypted repositories. - It is used when a passphrase is needed to access a encrypted repo as well as when a new + It is used when a passphrase is needed to access an encrypted repo as well as when a new passphrase should be initially set when initializing an encrypted repo. See also BORG_NEW_PASSPHRASE. + BORG_PASSCOMMAND + When set, use the standard output of the command (trailing newlines are stripped) to answer the + passphrase question for encrypted repositories. + It is used when a passphrase is needed to access an encrypted repo as well as when a new + passphrase should be initially set when initializing an encrypted repo. + If BORG_PASSPHRASE is also set, it takes precedence. + See also BORG_NEW_PASSPHRASE. BORG_NEW_PASSPHRASE When set, use the value to answer the passphrase question when a **new** passphrase is asked for. - This variable is checked first. If it is not set, BORG_PASSPHRASE will be checked also. + This variable is checked first. If it is not set, BORG_PASSPHRASE and BORG_PASSCOMMAND will also + be checked. Main usecase for this is to fully automate ``borg change-passphrase``. BORG_DISPLAY_PASSPHRASE When set, use the value to answer the "display the passphrase for verification" question when defining a new passphrase for encrypted repositories. diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 37cf3f55..72c260a1 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -3,6 +3,7 @@ import getpass import os import sys import textwrap +import subprocess from binascii import a2b_base64, b2a_base64, hexlify from hashlib import sha256, sha512, pbkdf2_hmac from hmac import HMAC, compare_digest @@ -29,7 +30,11 @@ PREFIX = b'\0' * 8 class PassphraseWrong(Error): - """passphrase supplied in BORG_PASSPHRASE is incorrect""" + """passphrase supplied in BORG_PASSPHRASE or by BORG_PASSCOMMAND is incorrect.""" + + +class PasscommandFailure(Error): + """passcommand supplied in BORG_PASSCOMMAND failed: {}""" class PasswordRetriesExceeded(Error): @@ -413,7 +418,22 @@ class Passphrase(str): @classmethod def env_passphrase(cls, default=None): - return cls._env_passphrase('BORG_PASSPHRASE', default) + passphrase = cls._env_passphrase('BORG_PASSPHRASE', default) + if passphrase is not None: + return passphrase + passphrase = cls.env_passcommand() + if passphrase is not None: + return passphrase + + @classmethod + def env_passcommand(cls, default=None): + passcommand = os.environ.get('BORG_PASSCOMMAND', None) + if passcommand is not None: + try: + passphrase = subprocess.check_output(passcommand.split(), universal_newlines=True) + except (subprocess.CalledProcessError, FileNotFoundError) as e: + raise PasscommandFailure(e) + return cls(passphrase.rstrip('\n')) @classmethod def env_new_passphrase(cls, default=None): From 4e6a771ee7cb4fb98b839d7c85454fc12d05ecd3 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 31 May 2017 19:41:17 +0200 Subject: [PATCH 0920/1387] BORG_PASSCOMMAND: use same cmd-string splitting as BORG_RSH --- src/borg/crypto/key.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 72c260a1..491d0ab7 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -1,6 +1,7 @@ import configparser import getpass import os +import shlex import sys import textwrap import subprocess @@ -430,7 +431,7 @@ class Passphrase(str): passcommand = os.environ.get('BORG_PASSCOMMAND', None) if passcommand is not None: try: - passphrase = subprocess.check_output(passcommand.split(), universal_newlines=True) + passphrase = subprocess.check_output(shlex.split(passcommand), universal_newlines=True) except (subprocess.CalledProcessError, FileNotFoundError) as e: raise PasscommandFailure(e) return cls(passphrase.rstrip('\n')) From bcf4b4492b32afcc1f6b9fe9f91b5511e5d01907 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 31 May 2017 20:28:17 +0200 Subject: [PATCH 0921/1387] testsuite: add test for parse_storage_quota --- src/borg/testsuite/archiver.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 508b1586..cace8e7c 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -31,7 +31,7 @@ except ImportError: from .. import xattr, helpers, platform from ..archive import Archive, ChunkBuffer, flags_noatime, flags_normal -from ..archiver import Archiver +from ..archiver import Archiver, parse_storage_quota from ..cache import Cache from ..constants import * # NOQA from ..crypto.low_level import bytes_to_long, num_aes_blocks @@ -3242,3 +3242,9 @@ class TestCommonOptions: result[args_key] = args_value assert parse_vars_from_line(*line) == result + + +def test_parse_storage_quota(): + assert parse_storage_quota('50M') == 50 * 1000**2 + with pytest.raises(argparse.ArgumentTypeError): + parse_storage_quota('5M') From 0221e310588df20f6d876e92c1cfb4cb4e2d02cc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 31 May 2017 21:47:07 +0200 Subject: [PATCH 0922/1387] file_integrity: use xxh64 --- AUTHORS | 6 + src/borg/algorithms/crc32.pyx | 65 +++ src/borg/algorithms/xxh64/xxhash.c | 615 +++++++++++++++++++++++++++ src/borg/algorithms/xxh64/xxhash.h | 245 +++++++++++ src/borg/crypto/file_integrity.py | 60 ++- src/borg/testsuite/crc32.py | 18 + src/borg/testsuite/file_integrity.py | 20 +- 7 files changed, 997 insertions(+), 32 deletions(-) create mode 100644 src/borg/algorithms/xxh64/xxhash.c create mode 100644 src/borg/algorithms/xxh64/xxhash.h diff --git a/AUTHORS b/AUTHORS index c03eb716..bfb56cfe 100644 --- a/AUTHORS +++ b/AUTHORS @@ -51,3 +51,9 @@ Folding CRC32 Borg includes an extremely fast folding implementation of CRC32, Copyright 2013 Intel Corporation, licensed under the terms of the zlib license. + +xxHash +------ + +XXH64, a fast non-cryptographic hash algorithm. Copyright 2012-2016 Yann Collet, +licensed under a BSD 2-clause license. diff --git a/src/borg/algorithms/crc32.pyx b/src/borg/algorithms/crc32.pyx index 07c8560f..b725cc5a 100644 --- a/src/borg/algorithms/crc32.pyx +++ b/src/borg/algorithms/crc32.pyx @@ -1,6 +1,8 @@ +from ..helpers import bin_to_hex from libc.stdint cimport uint32_t from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release +from cpython.bytes cimport PyBytes_FromStringAndSize cdef extern from "crc32_dispatch.c": @@ -10,6 +12,29 @@ cdef extern from "crc32_dispatch.c": int _have_clmul "have_clmul"() +cdef extern from "xxh64/xxhash.c": + ctypedef struct XXH64_canonical_t: + char digest[8] + + ctypedef struct XXH64_state_t: + pass # opaque + + ctypedef unsigned long long XXH64_hash_t + + ctypedef enum XXH_errorcode: + XXH_OK, + XXH_ERROR + + XXH64_hash_t XXH64 (const void* input, size_t length, unsigned long long seed); + + XXH_errorcode XXH64_reset (XXH64_state_t* statePtr, unsigned long long seed); + XXH_errorcode XXH64_update (XXH64_state_t* statePtr, const void* input, size_t length); + XXH64_hash_t XXH64_digest (const XXH64_state_t* statePtr); + + void XXH64_canonicalFromHash(XXH64_canonical_t* dst, XXH64_hash_t hash); + XXH64_hash_t XXH64_hashFromCanonical(const XXH64_canonical_t* src); + + cdef Py_buffer ro_buffer(object data) except *: cdef Py_buffer view PyObject_GetBuffer(data, &view, PyBUF_SIMPLE) @@ -39,3 +64,43 @@ if have_clmul: crc32 = crc32_clmul else: crc32 = crc32_slice_by_8 + + +def xxh64(data, seed=0): + cdef unsigned long long _seed = seed + cdef XXH64_hash_t hash + cdef XXH64_canonical_t digest + cdef Py_buffer data_buf = ro_buffer(data) + try: + hash = XXH64(data_buf.buf, data_buf.len, _seed) + finally: + PyBuffer_Release(&data_buf) + XXH64_canonicalFromHash(&digest, hash) + return PyBytes_FromStringAndSize( digest.digest, 8) + + +cdef class StreamingXXH64: + cdef XXH64_state_t state + + def __cinit__(self, seed=0): + cdef unsigned long long _seed = seed + if XXH64_reset(&self.state, _seed) != XXH_OK: + raise Exception('XXH64_reset failed') + + def update(self, data): + cdef Py_buffer data_buf = ro_buffer(data) + try: + if XXH64_update(&self.state, data_buf.buf, data_buf.len) != XXH_OK: + raise Exception('XXH64_update failed') + finally: + PyBuffer_Release(&data_buf) + + def digest(self): + cdef XXH64_hash_t hash + cdef XXH64_canonical_t digest + hash = XXH64_digest(&self.state) + XXH64_canonicalFromHash(&digest, hash) + return PyBytes_FromStringAndSize( digest.digest, 8) + + def hexdigest(self): + return bin_to_hex(self.digest()) diff --git a/src/borg/algorithms/xxh64/xxhash.c b/src/borg/algorithms/xxh64/xxhash.c new file mode 100644 index 00000000..0d0b3a52 --- /dev/null +++ b/src/borg/algorithms/xxh64/xxhash.c @@ -0,0 +1,615 @@ +/* +* xxHash - Fast Hash algorithm +* Copyright (C) 2012-2016, Yann Collet +* +* BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php) +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are +* met: +* +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * Redistributions in binary form must reproduce the above +* copyright notice, this list of conditions and the following disclaimer +* in the documentation and/or other materials provided with the +* distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +* +* You can contact the author at : +* - xxHash homepage: http://www.xxhash.com +* - xxHash source repository : https://github.com/Cyan4973/xxHash +*/ + + +/* ************************************* +* Tuning parameters +***************************************/ +/*!XXH_FORCE_MEMORY_ACCESS : + * By default, access to unaligned memory is controlled by `memcpy()`, which is safe and portable. + * Unfortunately, on some target/compiler combinations, the generated assembly is sub-optimal. + * The below switch allow to select different access method for improved performance. + * Method 0 (default) : use `memcpy()`. Safe and portable. + * Method 1 : `__packed` statement. It depends on compiler extension (ie, not portable). + * This method is safe if your compiler supports it, and *generally* as fast or faster than `memcpy`. + * Method 2 : direct access. This method doesn't depend on compiler but violate C standard. + * It can generate buggy code on targets which do not support unaligned memory accesses. + * But in some circumstances, it's the only known way to get the most performance (ie GCC + ARMv6) + * See http://stackoverflow.com/a/32095106/646947 for details. + * Prefer these methods in priority order (0 > 1 > 2) + */ +#ifndef XXH_FORCE_MEMORY_ACCESS /* can be defined externally, on command line for example */ +# if defined(__GNUC__) && ( defined(__ARM_ARCH_6__) || defined(__ARM_ARCH_6J__) || defined(__ARM_ARCH_6K__) || defined(__ARM_ARCH_6Z__) || defined(__ARM_ARCH_6ZK__) || defined(__ARM_ARCH_6T2__) ) +# define XXH_FORCE_MEMORY_ACCESS 2 +# elif defined(__INTEL_COMPILER) || \ + (defined(__GNUC__) && ( defined(__ARM_ARCH_7__) || defined(__ARM_ARCH_7A__) || defined(__ARM_ARCH_7R__) || defined(__ARM_ARCH_7M__) || defined(__ARM_ARCH_7S__) )) +# define XXH_FORCE_MEMORY_ACCESS 1 +# endif +#endif + +/*!XXH_ACCEPT_NULL_INPUT_POINTER : + * If the input pointer is a null pointer, xxHash default behavior is to trigger a memory access error, since it is a bad pointer. + * When this option is enabled, xxHash output for null input pointers will be the same as a null-length input. + * By default, this option is disabled. To enable it, uncomment below define : + */ +/* #define XXH_ACCEPT_NULL_INPUT_POINTER 1 */ + +/*!XXH_FORCE_NATIVE_FORMAT : + * By default, xxHash library provides endian-independant Hash values, based on little-endian convention. + * Results are therefore identical for little-endian and big-endian CPU. + * This comes at a performance cost for big-endian CPU, since some swapping is required to emulate little-endian format. + * Should endian-independance be of no importance for your application, you may set the #define below to 1, + * to improve speed for Big-endian CPU. + * This option has no impact on Little_Endian CPU. + */ +#ifndef XXH_FORCE_NATIVE_FORMAT /* can be defined externally */ +# define XXH_FORCE_NATIVE_FORMAT 0 +#endif + +/*!XXH_FORCE_ALIGN_CHECK : + * This is a minor performance trick, only useful with lots of very small keys. + * It means : check for aligned/unaligned input. + * The check costs one initial branch per hash; set to 0 when the input data + * is guaranteed to be aligned. + */ +#ifndef XXH_FORCE_ALIGN_CHECK /* can be defined externally */ +# if defined(__i386) || defined(_M_IX86) || defined(__x86_64__) || defined(_M_X64) +# define XXH_FORCE_ALIGN_CHECK 0 +# else +# define XXH_FORCE_ALIGN_CHECK 1 +# endif +#endif + + +/* ************************************* +* Includes & Memory related functions +***************************************/ +/* Modify the local functions below should you wish to use some other memory routines */ +/* for malloc(), free() */ +#include +static void* XXH_malloc(size_t s) { return malloc(s); } +static void XXH_free (void* p) { free(p); } +/* for memcpy() */ +#include +static void* XXH_memcpy(void* dest, const void* src, size_t size) { return memcpy(dest,src,size); } + +#define XXH_STATIC_LINKING_ONLY +#include "xxhash.h" + + +/* ************************************* +* Compiler Specific Options +***************************************/ +#ifdef _MSC_VER /* Visual Studio */ +# pragma warning(disable : 4127) /* disable: C4127: conditional expression is constant */ +# define FORCE_INLINE static __forceinline +#else +# if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L /* C99 */ +# ifdef __GNUC__ +# define FORCE_INLINE static inline __attribute__((always_inline)) +# else +# define FORCE_INLINE static inline +# endif +# else +# define FORCE_INLINE static +# endif /* __STDC_VERSION__ */ +#endif + + +/* ************************************* +* Basic Types +***************************************/ +#ifndef MEM_MODULE +# if !defined (__VMS) && (defined (__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) ) +# include + typedef uint8_t BYTE; + typedef uint16_t U16; + typedef uint32_t U32; + typedef int32_t S32; +# else + typedef unsigned char BYTE; + typedef unsigned short U16; + typedef unsigned int U32; + typedef signed int S32; +# endif +#endif + +#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==2)) + +/* Force direct memory access. Only works on CPU which support unaligned memory access in hardware */ +static U32 XXH_read32(const void* memPtr) { return *(const U32*) memPtr; } + +#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==1)) + +/* __pack instructions are safer, but compiler specific, hence potentially problematic for some compilers */ +/* currently only defined for gcc and icc */ +typedef union { U32 u32; } __attribute__((packed)) unalign; +static U32 XXH_read32(const void* ptr) { return ((const unalign*)ptr)->u32; } + +#else + +/* portable and safe solution. Generally efficient. + * see : http://stackoverflow.com/a/32095106/646947 + */ +static U32 XXH_read32(const void* memPtr) +{ + U32 val; + memcpy(&val, memPtr, sizeof(val)); + return val; +} + +#endif /* XXH_FORCE_DIRECT_MEMORY_ACCESS */ + + +/* **************************************** +* Compiler-specific Functions and Macros +******************************************/ +#define GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__) + +/* Note : although _rotl exists for minGW (GCC under windows), performance seems poor */ +#if defined(_MSC_VER) +# define XXH_rotl32(x,r) _rotl(x,r) +# define XXH_rotl64(x,r) _rotl64(x,r) +#else +# define XXH_rotl32(x,r) ((x << r) | (x >> (32 - r))) +# define XXH_rotl64(x,r) ((x << r) | (x >> (64 - r))) +#endif + +#if defined(_MSC_VER) /* Visual Studio */ +# define XXH_swap32 _byteswap_ulong +#elif GCC_VERSION >= 403 +# define XXH_swap32 __builtin_bswap32 +#else +static U32 XXH_swap32 (U32 x) +{ + return ((x << 24) & 0xff000000 ) | + ((x << 8) & 0x00ff0000 ) | + ((x >> 8) & 0x0000ff00 ) | + ((x >> 24) & 0x000000ff ); +} +#endif + + +/* ************************************* +* Architecture Macros +***************************************/ +typedef enum { XXH_bigEndian=0, XXH_littleEndian=1 } XXH_endianess; + +/* XXH_CPU_LITTLE_ENDIAN can be defined externally, for example on the compiler command line */ +#ifndef XXH_CPU_LITTLE_ENDIAN + static const int g_one = 1; +# define XXH_CPU_LITTLE_ENDIAN (*(const char*)(&g_one)) +#endif + + +/* *************************** +* Memory reads +*****************************/ +typedef enum { XXH_aligned, XXH_unaligned } XXH_alignment; + +FORCE_INLINE U32 XXH_readLE32_align(const void* ptr, XXH_endianess endian, XXH_alignment align) +{ + if (align==XXH_unaligned) + return endian==XXH_littleEndian ? XXH_read32(ptr) : XXH_swap32(XXH_read32(ptr)); + else + return endian==XXH_littleEndian ? *(const U32*)ptr : XXH_swap32(*(const U32*)ptr); +} + +FORCE_INLINE U32 XXH_readLE32(const void* ptr, XXH_endianess endian) +{ + return XXH_readLE32_align(ptr, endian, XXH_unaligned); +} + +/* ************************************* +* Macros +***************************************/ +#define XXH_STATIC_ASSERT(c) { enum { XXH_static_assert = 1/(int)(!!(c)) }; } /* use only *after* variable declarations */ +XXH_PUBLIC_API unsigned XXH_versionNumber (void) { return XXH_VERSION_NUMBER; } + +#ifndef XXH_NO_LONG_LONG + +/* ******************************************************************* +* 64-bits hash functions +*********************************************************************/ + +#define XXH_get32bits(p) XXH_readLE32_align(p, endian, align) + +/*====== Memory access ======*/ + +#ifndef MEM_MODULE +# define MEM_MODULE +# if !defined (__VMS) && (defined (__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) ) +# include + typedef uint64_t U64; +# else + typedef unsigned long long U64; /* if your compiler doesn't support unsigned long long, replace by another 64-bit type here. Note that xxhash.h will also need to be updated. */ +# endif +#endif + + +#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==2)) + +/* Force direct memory access. Only works on CPU which support unaligned memory access in hardware */ +static U64 XXH_read64(const void* memPtr) { return *(const U64*) memPtr; } + +#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==1)) + +/* __pack instructions are safer, but compiler specific, hence potentially problematic for some compilers */ +/* currently only defined for gcc and icc */ +typedef union { U32 u32; U64 u64; } __attribute__((packed)) unalign64; + +static U64 XXH_read64(const void* ptr) { return ((const unalign64*)ptr)->u64; } + +#else + +/* portable and safe solution. Generally efficient. + * see : http://stackoverflow.com/a/32095106/646947 + */ + +static U64 XXH_read64(const void* memPtr) +{ + U64 val; + memcpy(&val, memPtr, sizeof(val)); + return val; +} + +#endif /* XXH_FORCE_DIRECT_MEMORY_ACCESS */ + +#if defined(_MSC_VER) /* Visual Studio */ +# define XXH_swap64 _byteswap_uint64 +#elif GCC_VERSION >= 403 +# define XXH_swap64 __builtin_bswap64 +#else +static U64 XXH_swap64 (U64 x) +{ + return ((x << 56) & 0xff00000000000000ULL) | + ((x << 40) & 0x00ff000000000000ULL) | + ((x << 24) & 0x0000ff0000000000ULL) | + ((x << 8) & 0x000000ff00000000ULL) | + ((x >> 8) & 0x00000000ff000000ULL) | + ((x >> 24) & 0x0000000000ff0000ULL) | + ((x >> 40) & 0x000000000000ff00ULL) | + ((x >> 56) & 0x00000000000000ffULL); +} +#endif + +FORCE_INLINE U64 XXH_readLE64_align(const void* ptr, XXH_endianess endian, XXH_alignment align) +{ + if (align==XXH_unaligned) + return endian==XXH_littleEndian ? XXH_read64(ptr) : XXH_swap64(XXH_read64(ptr)); + else + return endian==XXH_littleEndian ? *(const U64*)ptr : XXH_swap64(*(const U64*)ptr); +} + +FORCE_INLINE U64 XXH_readLE64(const void* ptr, XXH_endianess endian) +{ + return XXH_readLE64_align(ptr, endian, XXH_unaligned); +} + +static U64 XXH_readBE64(const void* ptr) +{ + return XXH_CPU_LITTLE_ENDIAN ? XXH_swap64(XXH_read64(ptr)) : XXH_read64(ptr); +} + + +/*====== xxh64 ======*/ + +static const U64 PRIME64_1 = 11400714785074694791ULL; +static const U64 PRIME64_2 = 14029467366897019727ULL; +static const U64 PRIME64_3 = 1609587929392839161ULL; +static const U64 PRIME64_4 = 9650029242287828579ULL; +static const U64 PRIME64_5 = 2870177450012600261ULL; + +static U64 XXH64_round(U64 acc, U64 input) +{ + acc += input * PRIME64_2; + acc = XXH_rotl64(acc, 31); + acc *= PRIME64_1; + return acc; +} + +static U64 XXH64_mergeRound(U64 acc, U64 val) +{ + val = XXH64_round(0, val); + acc ^= val; + acc = acc * PRIME64_1 + PRIME64_4; + return acc; +} + +FORCE_INLINE U64 XXH64_endian_align(const void* input, size_t len, U64 seed, XXH_endianess endian, XXH_alignment align) +{ + const BYTE* p = (const BYTE*)input; + const BYTE* const bEnd = p + len; + U64 h64; +#define XXH_get64bits(p) XXH_readLE64_align(p, endian, align) + +#ifdef XXH_ACCEPT_NULL_INPUT_POINTER + if (p==NULL) { + len=0; + bEnd=p=(const BYTE*)(size_t)32; + } +#endif + + if (len>=32) { + const BYTE* const limit = bEnd - 32; + U64 v1 = seed + PRIME64_1 + PRIME64_2; + U64 v2 = seed + PRIME64_2; + U64 v3 = seed + 0; + U64 v4 = seed - PRIME64_1; + + do { + v1 = XXH64_round(v1, XXH_get64bits(p)); p+=8; + v2 = XXH64_round(v2, XXH_get64bits(p)); p+=8; + v3 = XXH64_round(v3, XXH_get64bits(p)); p+=8; + v4 = XXH64_round(v4, XXH_get64bits(p)); p+=8; + } while (p<=limit); + + h64 = XXH_rotl64(v1, 1) + XXH_rotl64(v2, 7) + XXH_rotl64(v3, 12) + XXH_rotl64(v4, 18); + h64 = XXH64_mergeRound(h64, v1); + h64 = XXH64_mergeRound(h64, v2); + h64 = XXH64_mergeRound(h64, v3); + h64 = XXH64_mergeRound(h64, v4); + + } else { + h64 = seed + PRIME64_5; + } + + h64 += (U64) len; + + while (p+8<=bEnd) { + U64 const k1 = XXH64_round(0, XXH_get64bits(p)); + h64 ^= k1; + h64 = XXH_rotl64(h64,27) * PRIME64_1 + PRIME64_4; + p+=8; + } + + if (p+4<=bEnd) { + h64 ^= (U64)(XXH_get32bits(p)) * PRIME64_1; + h64 = XXH_rotl64(h64, 23) * PRIME64_2 + PRIME64_3; + p+=4; + } + + while (p> 33; + h64 *= PRIME64_2; + h64 ^= h64 >> 29; + h64 *= PRIME64_3; + h64 ^= h64 >> 32; + + return h64; +} + + +XXH_PUBLIC_API unsigned long long XXH64 (const void* input, size_t len, unsigned long long seed) +{ +#if 0 + /* Simple version, good for code maintenance, but unfortunately slow for small inputs */ + XXH64_CREATESTATE_STATIC(state); + XXH64_reset(state, seed); + XXH64_update(state, input, len); + return XXH64_digest(state); +#else + XXH_endianess endian_detected = (XXH_endianess)XXH_CPU_LITTLE_ENDIAN; + + if (XXH_FORCE_ALIGN_CHECK) { + if ((((size_t)input) & 7)==0) { /* Input is aligned, let's leverage the speed advantage */ + if ((endian_detected==XXH_littleEndian) || XXH_FORCE_NATIVE_FORMAT) + return XXH64_endian_align(input, len, seed, XXH_littleEndian, XXH_aligned); + else + return XXH64_endian_align(input, len, seed, XXH_bigEndian, XXH_aligned); + } } + + if ((endian_detected==XXH_littleEndian) || XXH_FORCE_NATIVE_FORMAT) + return XXH64_endian_align(input, len, seed, XXH_littleEndian, XXH_unaligned); + else + return XXH64_endian_align(input, len, seed, XXH_bigEndian, XXH_unaligned); +#endif +} + +/*====== Hash Streaming ======*/ + +XXH_PUBLIC_API XXH64_state_t* XXH64_createState(void) +{ + return (XXH64_state_t*)XXH_malloc(sizeof(XXH64_state_t)); +} +XXH_PUBLIC_API XXH_errorcode XXH64_freeState(XXH64_state_t* statePtr) +{ + XXH_free(statePtr); + return XXH_OK; +} + +XXH_PUBLIC_API void XXH64_copyState(XXH64_state_t* restrict dstState, const XXH64_state_t* restrict srcState) +{ + memcpy(dstState, srcState, sizeof(*dstState)); +} + +XXH_PUBLIC_API XXH_errorcode XXH64_reset(XXH64_state_t* statePtr, unsigned long long seed) +{ + XXH64_state_t state; /* using a local state to memcpy() in order to avoid strict-aliasing warnings */ + memset(&state, 0, sizeof(state)-8); /* do not write into reserved, for future removal */ + state.v1 = seed + PRIME64_1 + PRIME64_2; + state.v2 = seed + PRIME64_2; + state.v3 = seed + 0; + state.v4 = seed - PRIME64_1; + memcpy(statePtr, &state, sizeof(state)); + return XXH_OK; +} + +FORCE_INLINE XXH_errorcode XXH64_update_endian (XXH64_state_t* state, const void* input, size_t len, XXH_endianess endian) +{ + const BYTE* p = (const BYTE*)input; + const BYTE* const bEnd = p + len; + +#ifdef XXH_ACCEPT_NULL_INPUT_POINTER + if (input==NULL) return XXH_ERROR; +#endif + + state->total_len += len; + + if (state->memsize + len < 32) { /* fill in tmp buffer */ + XXH_memcpy(((BYTE*)state->mem64) + state->memsize, input, len); + state->memsize += (U32)len; + return XXH_OK; + } + + if (state->memsize) { /* tmp buffer is full */ + XXH_memcpy(((BYTE*)state->mem64) + state->memsize, input, 32-state->memsize); + state->v1 = XXH64_round(state->v1, XXH_readLE64(state->mem64+0, endian)); + state->v2 = XXH64_round(state->v2, XXH_readLE64(state->mem64+1, endian)); + state->v3 = XXH64_round(state->v3, XXH_readLE64(state->mem64+2, endian)); + state->v4 = XXH64_round(state->v4, XXH_readLE64(state->mem64+3, endian)); + p += 32-state->memsize; + state->memsize = 0; + } + + if (p+32 <= bEnd) { + const BYTE* const limit = bEnd - 32; + U64 v1 = state->v1; + U64 v2 = state->v2; + U64 v3 = state->v3; + U64 v4 = state->v4; + + do { + v1 = XXH64_round(v1, XXH_readLE64(p, endian)); p+=8; + v2 = XXH64_round(v2, XXH_readLE64(p, endian)); p+=8; + v3 = XXH64_round(v3, XXH_readLE64(p, endian)); p+=8; + v4 = XXH64_round(v4, XXH_readLE64(p, endian)); p+=8; + } while (p<=limit); + + state->v1 = v1; + state->v2 = v2; + state->v3 = v3; + state->v4 = v4; + } + + if (p < bEnd) { + XXH_memcpy(state->mem64, p, (size_t)(bEnd-p)); + state->memsize = (unsigned)(bEnd-p); + } + + return XXH_OK; +} + +XXH_PUBLIC_API XXH_errorcode XXH64_update (XXH64_state_t* state_in, const void* input, size_t len) +{ + XXH_endianess endian_detected = (XXH_endianess)XXH_CPU_LITTLE_ENDIAN; + + if ((endian_detected==XXH_littleEndian) || XXH_FORCE_NATIVE_FORMAT) + return XXH64_update_endian(state_in, input, len, XXH_littleEndian); + else + return XXH64_update_endian(state_in, input, len, XXH_bigEndian); +} + +FORCE_INLINE U64 XXH64_digest_endian (const XXH64_state_t* state, XXH_endianess endian) +{ + const BYTE * p = (const BYTE*)state->mem64; + const BYTE* const bEnd = (const BYTE*)state->mem64 + state->memsize; + U64 h64; + + if (state->total_len >= 32) { + U64 const v1 = state->v1; + U64 const v2 = state->v2; + U64 const v3 = state->v3; + U64 const v4 = state->v4; + + h64 = XXH_rotl64(v1, 1) + XXH_rotl64(v2, 7) + XXH_rotl64(v3, 12) + XXH_rotl64(v4, 18); + h64 = XXH64_mergeRound(h64, v1); + h64 = XXH64_mergeRound(h64, v2); + h64 = XXH64_mergeRound(h64, v3); + h64 = XXH64_mergeRound(h64, v4); + } else { + h64 = state->v3 + PRIME64_5; + } + + h64 += (U64) state->total_len; + + while (p+8<=bEnd) { + U64 const k1 = XXH64_round(0, XXH_readLE64(p, endian)); + h64 ^= k1; + h64 = XXH_rotl64(h64,27) * PRIME64_1 + PRIME64_4; + p+=8; + } + + if (p+4<=bEnd) { + h64 ^= (U64)(XXH_readLE32(p, endian)) * PRIME64_1; + h64 = XXH_rotl64(h64, 23) * PRIME64_2 + PRIME64_3; + p+=4; + } + + while (p> 33; + h64 *= PRIME64_2; + h64 ^= h64 >> 29; + h64 *= PRIME64_3; + h64 ^= h64 >> 32; + + return h64; +} + +XXH_PUBLIC_API unsigned long long XXH64_digest (const XXH64_state_t* state_in) +{ + XXH_endianess endian_detected = (XXH_endianess)XXH_CPU_LITTLE_ENDIAN; + + if ((endian_detected==XXH_littleEndian) || XXH_FORCE_NATIVE_FORMAT) + return XXH64_digest_endian(state_in, XXH_littleEndian); + else + return XXH64_digest_endian(state_in, XXH_bigEndian); +} + + +/*====== Canonical representation ======*/ + +XXH_PUBLIC_API void XXH64_canonicalFromHash(XXH64_canonical_t* dst, XXH64_hash_t hash) +{ + XXH_STATIC_ASSERT(sizeof(XXH64_canonical_t) == sizeof(XXH64_hash_t)); + if (XXH_CPU_LITTLE_ENDIAN) hash = XXH_swap64(hash); + memcpy(dst, &hash, sizeof(*dst)); +} + +XXH_PUBLIC_API XXH64_hash_t XXH64_hashFromCanonical(const XXH64_canonical_t* src) +{ + return XXH_readBE64(src); +} + +#endif /* XXH_NO_LONG_LONG */ diff --git a/src/borg/algorithms/xxh64/xxhash.h b/src/borg/algorithms/xxh64/xxhash.h new file mode 100644 index 00000000..5e5d4cf4 --- /dev/null +++ b/src/borg/algorithms/xxh64/xxhash.h @@ -0,0 +1,245 @@ +/* + xxHash - Extremely Fast Hash algorithm + Header File + Copyright (C) 2012-2016, Yann Collet. + + BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php) + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + You can contact the author at : + - xxHash source repository : https://github.com/Cyan4973/xxHash +*/ + +/* Notice extracted from xxHash homepage : + +xxHash is an extremely fast Hash algorithm, running at RAM speed limits. +It also successfully passes all tests from the SMHasher suite. + +Comparison (single thread, Windows Seven 32 bits, using SMHasher on a Core 2 Duo @3GHz) + +Name Speed Q.Score Author +xxHash 5.4 GB/s 10 +CrapWow 3.2 GB/s 2 Andrew +MumurHash 3a 2.7 GB/s 10 Austin Appleby +SpookyHash 2.0 GB/s 10 Bob Jenkins +SBox 1.4 GB/s 9 Bret Mulvey +Lookup3 1.2 GB/s 9 Bob Jenkins +SuperFastHash 1.2 GB/s 1 Paul Hsieh +CityHash64 1.05 GB/s 10 Pike & Alakuijala +FNV 0.55 GB/s 5 Fowler, Noll, Vo +CRC32 0.43 GB/s 9 +MD5-32 0.33 GB/s 10 Ronald L. Rivest +SHA1-32 0.28 GB/s 10 + +Q.Score is a measure of quality of the hash function. +It depends on successfully passing SMHasher test set. +10 is a perfect score. + +A 64-bits version, named XXH64, is available since r35. +It offers much better speed, but for 64-bits applications only. +Name Speed on 64 bits Speed on 32 bits +XXH64 13.8 GB/s 1.9 GB/s +XXH32 6.8 GB/s 6.0 GB/s +*/ + +#ifndef XXHASH_H_5627135585666179 +#define XXHASH_H_5627135585666179 1 + +#define XXH_STATIC_LINKING_ONLY + +#if defined (__cplusplus) +extern "C" { +#endif + + +/* **************************** +* Compiler specifics +******************************/ +#if !(defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) /* ! C99 */ +# define restrict /* disable restrict */ +#endif + + +/* **************************** +* Definitions +******************************/ +#include /* size_t */ +typedef enum { XXH_OK=0, XXH_ERROR } XXH_errorcode; + + +/* **************************** +* API modifier +******************************/ +/** XXH_PRIVATE_API +* This is useful to include xxhash functions in `static` mode +* in order to inline them, and remove their symbol from the public list. +* Methodology : +* #define XXH_PRIVATE_API +* #include "xxhash.h" +* `xxhash.c` is automatically included. +* It's not useful to compile and link it as a separate module. +*/ +#ifdef XXH_PRIVATE_API +# ifndef XXH_STATIC_LINKING_ONLY +# define XXH_STATIC_LINKING_ONLY +# endif +# if defined(__GNUC__) +# define XXH_PUBLIC_API static __inline __attribute__((unused)) +# elif defined (__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) +# define XXH_PUBLIC_API static inline +# elif defined(_MSC_VER) +# define XXH_PUBLIC_API static __inline +# else +# define XXH_PUBLIC_API static /* this version may generate warnings for unused static functions; disable the relevant warning */ +# endif +#else +# define XXH_PUBLIC_API /* do nothing */ +#endif /* XXH_PRIVATE_API */ + +/*!XXH_NAMESPACE, aka Namespace Emulation : + +If you want to include _and expose_ xxHash functions from within your own library, +but also want to avoid symbol collisions with other libraries which may also include xxHash, + +you can use XXH_NAMESPACE, to automatically prefix any public symbol from xxhash library +with the value of XXH_NAMESPACE (therefore, avoid NULL and numeric values). + +Note that no change is required within the calling program as long as it includes `xxhash.h` : +regular symbol name will be automatically translated by this header. +*/ +#ifdef XXH_NAMESPACE +# define XXH_CAT(A,B) A##B +# define XXH_NAME2(A,B) XXH_CAT(A,B) +# define XXH_versionNumber XXH_NAME2(XXH_NAMESPACE, XXH_versionNumber) +# define XXH32 XXH_NAME2(XXH_NAMESPACE, XXH32) +# define XXH32_createState XXH_NAME2(XXH_NAMESPACE, XXH32_createState) +# define XXH32_freeState XXH_NAME2(XXH_NAMESPACE, XXH32_freeState) +# define XXH32_reset XXH_NAME2(XXH_NAMESPACE, XXH32_reset) +# define XXH32_update XXH_NAME2(XXH_NAMESPACE, XXH32_update) +# define XXH32_digest XXH_NAME2(XXH_NAMESPACE, XXH32_digest) +# define XXH32_copyState XXH_NAME2(XXH_NAMESPACE, XXH32_copyState) +# define XXH32_canonicalFromHash XXH_NAME2(XXH_NAMESPACE, XXH32_canonicalFromHash) +# define XXH32_hashFromCanonical XXH_NAME2(XXH_NAMESPACE, XXH32_hashFromCanonical) +# define XXH64 XXH_NAME2(XXH_NAMESPACE, XXH64) +# define XXH64_createState XXH_NAME2(XXH_NAMESPACE, XXH64_createState) +# define XXH64_freeState XXH_NAME2(XXH_NAMESPACE, XXH64_freeState) +# define XXH64_reset XXH_NAME2(XXH_NAMESPACE, XXH64_reset) +# define XXH64_update XXH_NAME2(XXH_NAMESPACE, XXH64_update) +# define XXH64_digest XXH_NAME2(XXH_NAMESPACE, XXH64_digest) +# define XXH64_copyState XXH_NAME2(XXH_NAMESPACE, XXH64_copyState) +# define XXH64_canonicalFromHash XXH_NAME2(XXH_NAMESPACE, XXH64_canonicalFromHash) +# define XXH64_hashFromCanonical XXH_NAME2(XXH_NAMESPACE, XXH64_hashFromCanonical) +#endif + + +/* ************************************* +* Version +***************************************/ +#define XXH_VERSION_MAJOR 0 +#define XXH_VERSION_MINOR 6 +#define XXH_VERSION_RELEASE 2 +#define XXH_VERSION_NUMBER (XXH_VERSION_MAJOR *100*100 + XXH_VERSION_MINOR *100 + XXH_VERSION_RELEASE) +XXH_PUBLIC_API unsigned XXH_versionNumber (void); + +#ifndef XXH_NO_LONG_LONG +/*-********************************************************************** +* 64-bits hash +************************************************************************/ +typedef unsigned long long XXH64_hash_t; + +/*! XXH64() : + Calculate the 64-bits hash of sequence of length "len" stored at memory address "input". + "seed" can be used to alter the result predictably. + This function runs faster on 64-bits systems, but slower on 32-bits systems (see benchmark). +*/ +XXH_PUBLIC_API XXH64_hash_t XXH64 (const void* input, size_t length, unsigned long long seed); + +/*====== Streaming ======*/ +typedef struct XXH64_state_s XXH64_state_t; /* incomplete type */ +XXH_PUBLIC_API XXH64_state_t* XXH64_createState(void); +XXH_PUBLIC_API XXH_errorcode XXH64_freeState(XXH64_state_t* statePtr); +XXH_PUBLIC_API void XXH64_copyState(XXH64_state_t* restrict dst_state, const XXH64_state_t* restrict src_state); + +XXH_PUBLIC_API XXH_errorcode XXH64_reset (XXH64_state_t* statePtr, unsigned long long seed); +XXH_PUBLIC_API XXH_errorcode XXH64_update (XXH64_state_t* statePtr, const void* input, size_t length); +XXH_PUBLIC_API XXH64_hash_t XXH64_digest (const XXH64_state_t* statePtr); + +/*====== Canonical representation ======*/ +typedef struct { unsigned char digest[8]; } XXH64_canonical_t; +XXH_PUBLIC_API void XXH64_canonicalFromHash(XXH64_canonical_t* dst, XXH64_hash_t hash); +XXH_PUBLIC_API XXH64_hash_t XXH64_hashFromCanonical(const XXH64_canonical_t* src); +#endif /* XXH_NO_LONG_LONG */ + + +#ifdef XXH_STATIC_LINKING_ONLY + +/* ================================================================================================ + This section contains definitions which are not guaranteed to remain stable. + They may change in future versions, becoming incompatible with a different version of the library. + They shall only be used with static linking. + Never use these definitions in association with dynamic linking ! +=================================================================================================== */ + +/* These definitions are only meant to allow allocation of XXH state + statically, on stack, or in a struct for example. + Do not use members directly. */ + + struct XXH32_state_s { + unsigned total_len_32; + unsigned large_len; + unsigned v1; + unsigned v2; + unsigned v3; + unsigned v4; + unsigned mem32[4]; /* buffer defined as U32 for alignment */ + unsigned memsize; + unsigned reserved; /* never read nor write, will be removed in a future version */ + }; /* typedef'd to XXH32_state_t */ + +#ifndef XXH_NO_LONG_LONG + struct XXH64_state_s { + unsigned long long total_len; + unsigned long long v1; + unsigned long long v2; + unsigned long long v3; + unsigned long long v4; + unsigned long long mem64[4]; /* buffer defined as U64 for alignment */ + unsigned memsize; + unsigned reserved[2]; /* never read nor write, will be removed in a future version */ + }; /* typedef'd to XXH64_state_t */ +#endif + +# ifdef XXH_PRIVATE_API +# include "xxhash.c" /* include xxhash function bodies as `static`, for inlining */ +# endif + +#endif /* XXH_STATIC_LINKING_ONLY */ + + +#if defined (__cplusplus) +} +#endif + +#endif /* XXHASH_H_5627135585666179 */ diff --git a/src/borg/crypto/file_integrity.py b/src/borg/crypto/file_integrity.py index 032b8672..ebcdbfff 100644 --- a/src/borg/crypto/file_integrity.py +++ b/src/borg/crypto/file_integrity.py @@ -6,6 +6,7 @@ from hmac import compare_digest from ..helpers import IntegrityError from ..logger import create_logger +from ..algorithms.crc32 import StreamingXXH64 logger = create_logger() @@ -37,7 +38,7 @@ class FileLikeWrapper: return self.fd.fileno() -class SHA512FileHashingWrapper(FileLikeWrapper): +class FileHashingWrapper(FileLikeWrapper): """ Wrapper for file-like objects that computes a hash on-the-fly while reading/writing. @@ -53,12 +54,13 @@ class SHA512FileHashingWrapper(FileLikeWrapper): are illegal. """ - ALGORITHM = 'SHA512' + ALGORITHM = None + FACTORY = None def __init__(self, backing_fd, write): self.fd = backing_fd self.writing = write - self.hash = hashlib.new(self.ALGORITHM) + self.hash = self.FACTORY() def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: @@ -99,6 +101,22 @@ class SHA512FileHashingWrapper(FileLikeWrapper): self.hash.update(str(self.tell()).encode()) +class SHA512FileHashingWrapper(FileHashingWrapper): + ALGORITHM = 'SHA512' + FACTORY = hashlib.sha512 + + +class XXH64FileHashingWrapper(FileHashingWrapper): + ALGORITHM = 'XXH64' + FACTORY = StreamingXXH64 + + +SUPPORTED_ALGORITHMS = { + SHA512FileHashingWrapper.ALGORITHM: SHA512FileHashingWrapper, + XXH64FileHashingWrapper.ALGORITHM: XXH64FileHashingWrapper, +} + + class FileIntegrityError(IntegrityError): """File failed integrity check: {}""" @@ -109,18 +127,26 @@ class IntegrityCheckedFile(FileLikeWrapper): self.writing = write mode = 'wb' if write else 'rb' self.file_fd = override_fd or open(path, mode) + self.digests = {} - self.fd = self.hasher = SHA512FileHashingWrapper(backing_fd=self.file_fd, write=write) + hash_cls = XXH64FileHashingWrapper - self.hash_filename(filename) + if not write: + algorithm_and_digests = self.load_integrity_data(path, integrity_data) + if algorithm_and_digests: + algorithm, self.digests = algorithm_and_digests + hash_cls = SUPPORTED_ALGORITHMS[algorithm] - if write or not integrity_data: - self.digests = {} - else: - self.digests = self.parse_integrity_data(path, integrity_data, self.hasher) # TODO: When we're reading but don't have any digests, i.e. no integrity file existed, # TODO: then we could just short-circuit. + self.fd = self.hasher = hash_cls(backing_fd=self.file_fd, write=write) + self.hash_filename(filename) + + def load_integrity_data(self, path, integrity_data): + if integrity_data is not None: + return self.parse_integrity_data(path, integrity_data) + def hash_filename(self, filename=None): # Hash the name of the file, but only the basename, ie. not the path. # In Borg the name itself encodes the context (eg. index.N, cache, files), @@ -133,18 +159,18 @@ class IntegrityCheckedFile(FileLikeWrapper): self.hasher.update(filename.encode()) @classmethod - def parse_integrity_data(cls, path: str, data: str, hasher: SHA512FileHashingWrapper): + def parse_integrity_data(cls, path: str, data: str): try: integrity_data = json.loads(data) # Provisions for agility now, implementation later, but make sure the on-disk joint is oiled. algorithm = integrity_data['algorithm'] - if algorithm != hasher.ALGORITHM: + if algorithm not in SUPPORTED_ALGORITHMS: logger.warning('Cannot verify integrity of %s: Unknown algorithm %r', path, algorithm) return digests = integrity_data['digests'] # Require at least presence of the final digest digests['final'] - return digests + return algorithm, digests except (ValueError, TypeError, KeyError) as e: logger.warning('Could not parse integrity data for %s: %s', path, e) raise FileIntegrityError(path) @@ -186,18 +212,20 @@ class DetachedIntegrityCheckedFile(IntegrityCheckedFile): filename = filename or os.path.basename(path) output_dir = os.path.dirname(path) self.output_integrity_file = self.integrity_file_path(os.path.join(output_dir, filename)) - if not write: - self.digests = self.read_integrity_file(self.path, self.hasher) + + def load_integrity_data(self, path, integrity_data): + assert not integrity_data, 'Cannot pass explicit integrity_data to DetachedIntegrityCheckedFile' + return self.read_integrity_file(self.path) @staticmethod def integrity_file_path(path): return path + '.integrity' @classmethod - def read_integrity_file(cls, path, hasher): + def read_integrity_file(cls, path): try: with open(cls.integrity_file_path(path), 'r') as fd: - return cls.parse_integrity_data(path, fd.read(), hasher) + return cls.parse_integrity_data(path, fd.read()) except FileNotFoundError: logger.info('No integrity file found for %s', path) except OSError as e: diff --git a/src/borg/testsuite/crc32.py b/src/borg/testsuite/crc32.py index 4eb59fa8..69163753 100644 --- a/src/borg/testsuite/crc32.py +++ b/src/borg/testsuite/crc32.py @@ -1,9 +1,11 @@ import os import zlib +from binascii import unhexlify import pytest from ..algorithms import crc32 +from ..helpers import bin_to_hex crc32_implementations = [crc32.crc32_slice_by_8] if crc32.have_clmul: @@ -19,3 +21,19 @@ def test_crc32(implementation): for i in range(0, 256): d = data[:i] assert zlib.crc32(d, initial_crc) == implementation(d, initial_crc) + + +def test_xxh64(): + assert bin_to_hex(crc32.xxh64(b'test', 123)) == '2b81b9401bef86cf' + assert bin_to_hex(crc32.xxh64(b'test')) == '4fdcca5ddb678139' + assert bin_to_hex(crc32.xxh64(unhexlify('6f663f01c118abdea553373d5eae44e7dac3b6829b46b9bbeff202b6c592c22d724' + 'fb3d25a347cca6c5b8f20d567e4bb04b9cfa85d17f691590f9a9d32e8ccc9102e9d' + 'cf8a7e6716280cd642ce48d03fdf114c9f57c20d9472bb0f81c147645e6fa3d331'))) == \ + '35d5d2f545d9511a' + + +def test_streaming_xxh64(): + hasher = crc32.StreamingXXH64(123) + hasher.update(b'te') + hasher.update(b'st') + assert bin_to_hex(hasher.digest()) == hasher.hexdigest() == '2b81b9401bef86cf' diff --git a/src/borg/testsuite/file_integrity.py b/src/borg/testsuite/file_integrity.py index 0dd323d6..6dee247a 100644 --- a/src/borg/testsuite/file_integrity.py +++ b/src/borg/testsuite/file_integrity.py @@ -8,23 +8,20 @@ class TestReadIntegrityFile: def test_no_integrity(self, tmpdir): protected_file = tmpdir.join('file') protected_file.write('1234') - assert DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file), None) is None + assert DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file)) is None def test_truncated_integrity(self, tmpdir): protected_file = tmpdir.join('file') protected_file.write('1234') tmpdir.join('file.integrity').write('') with pytest.raises(FileIntegrityError): - DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file), None) + DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file)) def test_unknown_algorithm(self, tmpdir): - class SomeHasher: - ALGORITHM = 'HMAC_FOOHASH9000' - protected_file = tmpdir.join('file') protected_file.write('1234') tmpdir.join('file.integrity').write('{"algorithm": "HMAC_SERIOUSHASH", "digests": "1234"}') - assert DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file), SomeHasher()) is None + assert DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file)) is None @pytest.mark.parametrize('json', ( '{"ALGORITHM": "HMAC_SERIOUSHASH", "digests": "1234"}', @@ -38,16 +35,7 @@ class TestReadIntegrityFile: protected_file.write('1234') tmpdir.join('file.integrity').write(json) with pytest.raises(FileIntegrityError): - DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file), None) - - def test_valid(self, tmpdir): - class SomeHasher: - ALGORITHM = 'HMAC_FOO1' - - protected_file = tmpdir.join('file') - protected_file.write('1234') - tmpdir.join('file.integrity').write('{"algorithm": "HMAC_FOO1", "digests": {"final": "1234"}}') - assert DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file), SomeHasher()) == {'final': '1234'} + DetachedIntegrityCheckedFile.read_integrity_file(str(protected_file)) class TestDetachedIntegrityCheckedFile: From 6c91a750d15f8186311e20f270fd6696a82c6ab9 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 31 May 2017 23:17:27 +0200 Subject: [PATCH 0923/1387] algorithms: rename crc32 to checksums --- setup.py | 35 ++++++++++--------- .../algorithms/{crc32.pyx => checksums.pyx} | 8 ++--- src/borg/archiver.py | 2 +- src/borg/crypto/file_integrity.py | 2 +- src/borg/repository.py | 2 +- src/borg/testsuite/{crc32.py => checksums.py} | 22 ++++++------ 6 files changed, 36 insertions(+), 35 deletions(-) rename src/borg/algorithms/{crc32.pyx => checksums.pyx} (90%) rename src/borg/testsuite/{crc32.py => checksums.py} (52%) diff --git a/setup.py b/setup.py index 5b6ab972..726c849c 100644 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ crypto_ll_source = 'src/borg/crypto/low_level.pyx' chunker_source = 'src/borg/algorithms/chunker.pyx' hashindex_source = 'src/borg/hashindex.pyx' item_source = 'src/borg/item.pyx' -crc32_source = 'src/borg/algorithms/crc32.pyx' +checksums_source = 'src/borg/algorithms/checksums.pyx' platform_posix_source = 'src/borg/platform/posix.pyx' platform_linux_source = 'src/borg/platform/linux.pyx' platform_darwin_source = 'src/borg/platform/darwin.pyx' @@ -67,7 +67,7 @@ cython_sources = [ chunker_source, hashindex_source, item_source, - crc32_source, + checksums_source, platform_posix_source, platform_linux_source, @@ -92,8 +92,9 @@ try: 'src/borg/algorithms/chunker.c', 'src/borg/algorithms/buzhash.c', 'src/borg/hashindex.c', 'src/borg/_hashindex.c', 'src/borg/item.c', - 'src/borg/algorithms/crc32.c', + 'src/borg/algorithms/checksums.c', 'src/borg/algorithms/crc32_dispatch.c', 'src/borg/algorithms/crc32_clmul.c', 'src/borg/algorithms/crc32_slice_by_8.c', + 'src/borg/algorithms/xxh64/xxhash.h', 'src/borg/algorithms/xxh64/xxhash.c', 'src/borg/platform/posix.c', 'src/borg/platform/linux.c', 'src/borg/platform/freebsd.c', @@ -111,14 +112,14 @@ except ImportError: chunker_source = chunker_source.replace('.pyx', '.c') hashindex_source = hashindex_source.replace('.pyx', '.c') item_source = item_source.replace('.pyx', '.c') - crc32_source = crc32_source.replace('.pyx', '.c') + checksums_source = checksums_source.replace('.pyx', '.c') platform_posix_source = platform_posix_source.replace('.pyx', '.c') platform_linux_source = platform_linux_source.replace('.pyx', '.c') platform_freebsd_source = platform_freebsd_source.replace('.pyx', '.c') platform_darwin_source = platform_darwin_source.replace('.pyx', '.c') from distutils.command.build_ext import build_ext if not on_rtd and not all(os.path.exists(path) for path in [ - compress_source, crypto_ll_source, chunker_source, hashindex_source, item_source, crc32_source, + compress_source, crypto_ll_source, chunker_source, hashindex_source, item_source, checksums_source, platform_posix_source, platform_linux_source, platform_freebsd_source, platform_darwin_source]): raise ImportError('The GIT version of Borg needs Cython. Install Cython or use a released version.') @@ -568,23 +569,23 @@ class build_man(Command): write(option.ljust(padding), desc) +def rm(file): + try: + os.unlink(file) + print('rm', file) + except FileNotFoundError: + pass + + class Clean(clean): def run(self): super().run() for source in cython_sources: genc = source.replace('.pyx', '.c') - try: - os.unlink(genc) - print('rm', genc) - except FileNotFoundError: - pass + rm(genc) compiled_glob = source.replace('.pyx', '.cpython*') - for compiled in glob(compiled_glob): - try: - os.unlink(compiled) - print('rm', compiled) - except FileNotFoundError: - pass + for compiled in sorted(glob(compiled_glob)): + rm(compiled) cmdclass = { 'build_ext': build_ext, @@ -602,7 +603,7 @@ if not on_rtd: Extension('borg.hashindex', [hashindex_source]), Extension('borg.item', [item_source]), Extension('borg.algorithms.chunker', [chunker_source]), - Extension('borg.algorithms.crc32', [crc32_source]), + Extension('borg.algorithms.checksums', [checksums_source]), ] if not sys.platform.startswith(('win32', )): ext_modules.append(Extension('borg.platform.posix', [platform_posix_source])) diff --git a/src/borg/algorithms/crc32.pyx b/src/borg/algorithms/checksums.pyx similarity index 90% rename from src/borg/algorithms/crc32.pyx rename to src/borg/algorithms/checksums.pyx index b725cc5a..6645dd0f 100644 --- a/src/borg/algorithms/crc32.pyx +++ b/src/borg/algorithms/checksums.pyx @@ -25,11 +25,11 @@ cdef extern from "xxh64/xxhash.c": XXH_OK, XXH_ERROR - XXH64_hash_t XXH64 (const void* input, size_t length, unsigned long long seed); + XXH64_hash_t XXH64(const void* input, size_t length, unsigned long long seed); - XXH_errorcode XXH64_reset (XXH64_state_t* statePtr, unsigned long long seed); - XXH_errorcode XXH64_update (XXH64_state_t* statePtr, const void* input, size_t length); - XXH64_hash_t XXH64_digest (const XXH64_state_t* statePtr); + XXH_errorcode XXH64_reset(XXH64_state_t* statePtr, unsigned long long seed); + XXH_errorcode XXH64_update(XXH64_state_t* statePtr, const void* input, size_t length); + XXH64_hash_t XXH64_digest(const XXH64_state_t* statePtr); void XXH64_canonicalFromHash(XXH64_canonical_t* dst, XXH64_hash_t hash); XXH64_hash_t XXH64_hashFromCanonical(const XXH64_canonical_t* src); diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f6bb69e6..2081b44e 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -33,7 +33,7 @@ import msgpack import borg from . import __version__ from . import helpers -from .algorithms.crc32 import crc32 +from .algorithms.checksums import crc32 from .archive import Archive, ArchiveChecker, ArchiveRecreater, Statistics, is_special from .archive import BackupOSError, backup_io from .cache import Cache, assert_secure diff --git a/src/borg/crypto/file_integrity.py b/src/borg/crypto/file_integrity.py index ebcdbfff..84b22a7e 100644 --- a/src/borg/crypto/file_integrity.py +++ b/src/borg/crypto/file_integrity.py @@ -6,7 +6,7 @@ from hmac import compare_digest from ..helpers import IntegrityError from ..logger import create_logger -from ..algorithms.crc32 import StreamingXXH64 +from ..algorithms.checksums import StreamingXXH64 logger = create_logger() diff --git a/src/borg/repository.py b/src/borg/repository.py index 1caf4277..a4fbb7cc 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -23,7 +23,7 @@ from .locking import Lock, LockError, LockErrorT from .logger import create_logger from .lrucache import LRUCache from .platform import SaveFile, SyncFile, sync_dir, safe_fadvise -from .algorithms.crc32 import crc32 +from .algorithms.checksums import crc32 logger = create_logger(__name__) diff --git a/src/borg/testsuite/crc32.py b/src/borg/testsuite/checksums.py similarity index 52% rename from src/borg/testsuite/crc32.py rename to src/borg/testsuite/checksums.py index 69163753..5b0d9fb9 100644 --- a/src/borg/testsuite/crc32.py +++ b/src/borg/testsuite/checksums.py @@ -4,12 +4,12 @@ from binascii import unhexlify import pytest -from ..algorithms import crc32 +from ..algorithms import checksums from ..helpers import bin_to_hex -crc32_implementations = [crc32.crc32_slice_by_8] -if crc32.have_clmul: - crc32_implementations.append(crc32.crc32_clmul) +crc32_implementations = [checksums.crc32_slice_by_8] +if checksums.have_clmul: + crc32_implementations.append(checksums.crc32_clmul) @pytest.mark.parametrize('implementation', crc32_implementations) @@ -24,16 +24,16 @@ def test_crc32(implementation): def test_xxh64(): - assert bin_to_hex(crc32.xxh64(b'test', 123)) == '2b81b9401bef86cf' - assert bin_to_hex(crc32.xxh64(b'test')) == '4fdcca5ddb678139' - assert bin_to_hex(crc32.xxh64(unhexlify('6f663f01c118abdea553373d5eae44e7dac3b6829b46b9bbeff202b6c592c22d724' - 'fb3d25a347cca6c5b8f20d567e4bb04b9cfa85d17f691590f9a9d32e8ccc9102e9d' - 'cf8a7e6716280cd642ce48d03fdf114c9f57c20d9472bb0f81c147645e6fa3d331'))) == \ - '35d5d2f545d9511a' + assert bin_to_hex(checksums.xxh64(b'test', 123)) == '2b81b9401bef86cf' + assert bin_to_hex(checksums.xxh64(b'test')) == '4fdcca5ddb678139' + assert bin_to_hex(checksums.xxh64(unhexlify( + '6f663f01c118abdea553373d5eae44e7dac3b6829b46b9bbeff202b6c592c22d724' + 'fb3d25a347cca6c5b8f20d567e4bb04b9cfa85d17f691590f9a9d32e8ccc9102e9d' + 'cf8a7e6716280cd642ce48d03fdf114c9f57c20d9472bb0f81c147645e6fa3d331'))) == '35d5d2f545d9511a' def test_streaming_xxh64(): - hasher = crc32.StreamingXXH64(123) + hasher = checksums.StreamingXXH64(123) hasher.update(b'te') hasher.update(b'st') assert bin_to_hex(hasher.digest()) == hasher.hexdigest() == '2b81b9401bef86cf' From 16a296c11aeb559246205079845a5997ebbed1f6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 2 Jun 2017 02:29:05 +0200 Subject: [PATCH 0924/1387] vagrant: control VM cpus and pytest workers via env --- Vagrantfile | 36 +++++++++++++++++++++++------------- docs/development.rst | 2 ++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index e42f7333..5ee6b40e 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -3,6 +3,10 @@ # Automated creation of testing environments / binaries on misc. platforms +$cpus = Integer(ENV.fetch('VMCPUS', '4')) # create VMs with that many cpus +$xdistn = Integer(ENV.fetch('XDISTN', '4')) # dispatch tests to that many pytest workers +$wmem = $xdistn * 256 # give the VM additional memory for workers [MB] + def packages_prepare_wheezy return <<-EOF # debian 7 wheezy does not have lz4, but it is available from wheezy-backports: @@ -209,7 +213,7 @@ def install_cygwin_venv end def install_pyenv(boxname) - return <<-EOF + script = <<-EOF curl -s -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash echo 'export PATH="$HOME/.pyenv/bin:/vagrant/borg:$PATH"' >> ~/.bash_profile echo 'eval "$(pyenv init -)"' >> ~/.bash_profile @@ -217,6 +221,8 @@ def install_pyenv(boxname) echo 'export PYTHON_CONFIGURE_OPTS="--enable-shared"' >> ~/.bash_profile echo 'export LANG=en_US.UTF-8' >> ~/.bash_profile EOF + script += "echo 'export XDISTN=%d' >> ~/.bash_profile\n" % [$xdistn] + return script end def fix_pyenv_darwin(boxname) @@ -348,14 +354,14 @@ Vagrant.configure(2) do |config| config.vm.provider :virtualbox do |v| #v.gui = true - v.cpus = 1 + v.cpus = $cpus end # Linux config.vm.define "centos7_64" do |b| b.vm.box = "centos/7" b.vm.provider :virtualbox do |v| - v.memory = 1536 + v.memory = 1024 + $wmem end b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos7_64") @@ -368,7 +374,7 @@ Vagrant.configure(2) do |config| config.vm.define "centos6_32" do |b| b.vm.box = "centos6-32" b.vm.provider :virtualbox do |v| - v.memory = 1024 + v.memory = 768 + $wmem end b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos6_32") @@ -381,7 +387,7 @@ Vagrant.configure(2) do |config| config.vm.define "centos6_64" do |b| b.vm.box = "centos6-64" b.vm.provider :virtualbox do |v| - v.memory = 1536 + v.memory = 1024 + $wmem end b.vm.provision "install system packages", :type => :shell, :inline => packages_redhatted b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("centos6_64") @@ -394,7 +400,7 @@ Vagrant.configure(2) do |config| config.vm.define "xenial64" do |b| b.vm.box = "ubuntu/xenial64" b.vm.provider :virtualbox do |v| - v.memory = 1536 + v.memory = 1024 + $wmem end b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("xenial64") @@ -405,7 +411,7 @@ Vagrant.configure(2) do |config| config.vm.define "trusty64" do |b| b.vm.box = "ubuntu/trusty64" b.vm.provider :virtualbox do |v| - v.memory = 1536 + v.memory = 1024 + $wmem end b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("trusty64") @@ -416,7 +422,7 @@ Vagrant.configure(2) do |config| config.vm.define "jessie64" do |b| b.vm.box = "debian/jessie64" b.vm.provider :virtualbox do |v| - v.memory = 1536 + v.memory = 1024 + $wmem end b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("jessie64") @@ -427,7 +433,7 @@ Vagrant.configure(2) do |config| config.vm.define "wheezy32" do |b| b.vm.box = "boxcutter/debian7-i386" b.vm.provider :virtualbox do |v| - v.memory = 1024 + v.memory = 768 + $wmem end b.vm.provision "packages prepare wheezy", :type => :shell, :inline => packages_prepare_wheezy b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid @@ -442,6 +448,9 @@ Vagrant.configure(2) do |config| config.vm.define "wheezy64" do |b| b.vm.box = "boxcutter/debian7" + b.vm.provider :virtualbox do |v| + v.memory = 1024 + $wmem + end b.vm.provision "packages prepare wheezy", :type => :shell, :inline => packages_prepare_wheezy b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("wheezy64") @@ -457,6 +466,7 @@ Vagrant.configure(2) do |config| config.vm.define "darwin64" do |b| b.vm.box = "jhcook/yosemite-clitools" b.vm.provider :virtualbox do |v| + v.memory = 1536 + $wmem v.customize ['modifyvm', :id, '--ostype', 'MacOS1010_64'] v.customize ['modifyvm', :id, '--paravirtprovider', 'default'] # Adjust CPU settings according to @@ -482,7 +492,7 @@ Vagrant.configure(2) do |config| config.vm.define "freebsd64" do |b| b.vm.box = "freebsd/FreeBSD-10.3-RELEASE" b.vm.provider :virtualbox do |v| - v.memory = 1536 + v.memory = 1024 + $wmem end b.ssh.shell = "sh" b.vm.provision "install system packages", :type => :shell, :inline => packages_freebsd @@ -498,7 +508,7 @@ Vagrant.configure(2) do |config| config.vm.define "openbsd64" do |b| b.vm.box = "openbsd60-64" # note: basic openbsd install for vagrant WITH sudo and rsync pre-installed b.vm.provider :virtualbox do |v| - v.memory = 1536 + v.memory = 1024 + $wmem end b.vm.provision "packages openbsd", :type => :shell, :inline => packages_openbsd b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("openbsd64") @@ -509,7 +519,7 @@ Vagrant.configure(2) do |config| config.vm.define "netbsd64" do |b| b.vm.box = "netbsd70-64" b.vm.provider :virtualbox do |v| - v.memory = 1536 + v.memory = 1024 + $wmem end b.vm.provision "packages netbsd", :type => :shell, :inline => packages_netbsd b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("netbsd64") @@ -529,7 +539,7 @@ Vagrant.configure(2) do |config| b.ssh.insert_key = false b.vm.provider :virtualbox do |v| - v.memory = 2048 + v.memory = 1536 + $wmem #v.gui = true end diff --git a/docs/development.rst b/docs/development.rst index f8277aaa..7d4c0774 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -249,6 +249,8 @@ Usage:: # To create and provision the VM: vagrant up OS + # same, but use 6 VM cpus and 12 workers for pytest: + VMCPUS=6 XDISTN=12 vagrant up OS # To create an ssh session to the VM: vagrant ssh OS # To execute a command via ssh in the VM: From a4729097cc2bda049c20f40f4850189e432b4c74 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 2 Jun 2017 02:45:07 +0200 Subject: [PATCH 0925/1387] vagrant: fix openbsd shell --- Vagrantfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Vagrantfile b/Vagrantfile index 5ee6b40e..3655eb2b 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -510,6 +510,7 @@ Vagrant.configure(2) do |config| b.vm.provider :virtualbox do |v| v.memory = 1024 + $wmem end + b.ssh.shell = "sh" b.vm.provision "packages openbsd", :type => :shell, :inline => packages_openbsd b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("openbsd64") b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(false) From 9b35fa1d391c103ed0e859f817c8b46a427cc33b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 2 Jun 2017 06:12:30 +0200 Subject: [PATCH 0926/1387] vagrant: update cleaning --- Vagrantfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 3655eb2b..e7186109 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -269,10 +269,10 @@ def install_borg(fuse) pip install -U wheel # upgrade wheel, too old for 3.5 cd borg # clean up (wrong/outdated) stuff we likely got via rsync: - rm -f borg/*.so borg/*.cpy* - rm -f borg/{chunker,crypto,compress,hashindex,platform_linux}.c - rm -rf borg/__pycache__ borg/support/__pycache__ borg/testsuite/__pycache__ + rm -rf __pycache__ + find src -name '__pycache__' -exec rm -rf {} \; pip install -r requirements.d/development.txt + python setup.py clean EOF if fuse script += <<-EOF From 3c951df4cd084137ef81b109ccc5e099a2fb1558 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Jun 2017 12:21:59 +0200 Subject: [PATCH 0927/1387] docs/security: security track record of OpenSSL and msgpack --- docs/internals/security.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/internals/security.rst b/docs/internals/security.rst index 978338b7..8421685c 100644 --- a/docs/internals/security.rst +++ b/docs/internals/security.rst @@ -336,3 +336,30 @@ like remote code execution are inhibited by the design of the protocol: general pattern of server-sent responses and are sent instead of response data for a request. +The msgpack implementation used (msgpack-python) has a good security track record, +a large test suite and no issues found by fuzzing. It is based on the msgpack-c implementation, +sharing the unpacking engine and some support code. msgpack-c has a good track record as well. +Some issues [#]_ in the past were located in code not included in msgpack-python. +Borg does not use msgpack-c. + +.. [#] - `MessagePack fuzzing `_ + - `Fixed integer overflow and EXT size problem `_ + - `Fixed array and map size overflow `_ + +Using OpenSSL +============= + +Borg uses the OpenSSL library for most cryptography (see `Implementations used`_ above). +OpenSSL is bundled with static releases, thus the bundled copy is not updated with system +updates. + +OpenSSL is a large and complex piece of software and has had its share of vulnerabilities, +however, it is important to note that Borg links against ``libcrypto`` **not** ``libssl``. +libcrypto is the low-level cryptography part of OpenSSL, while libssl implements TLS and related protocols. +The latter is not used by Borg (cf. `Remote RPC protocol security`_, Borg does not implement +any network access) and historically contained most vulnerabilities, especially critical ones. + +Historic vulnerabilities affecting libcrypto in ways relevant to Borg were flaws in primtives +enabling side-channel and similar attacks. + +Therefore, both using and bundling OpenSSL is considered unproblematic for Borg. From 107e320a20f32bb05e13aab6be81aedad66fec17 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Jun 2017 12:52:30 +0200 Subject: [PATCH 0928/1387] binaries: don't bundle libssl ArchiverTestCaseBinary passes. --- docs/internals/security.rst | 12 +++++------- scripts/borg.exe.spec | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/docs/internals/security.rst b/docs/internals/security.rst index 8421685c..d028c7f9 100644 --- a/docs/internals/security.rst +++ b/docs/internals/security.rst @@ -355,11 +355,9 @@ updates. OpenSSL is a large and complex piece of software and has had its share of vulnerabilities, however, it is important to note that Borg links against ``libcrypto`` **not** ``libssl``. -libcrypto is the low-level cryptography part of OpenSSL, while libssl implements TLS and related protocols. -The latter is not used by Borg (cf. `Remote RPC protocol security`_, Borg does not implement +libcrypto is the low-level cryptography part of OpenSSL, +while libssl implements TLS and related protocols. + +The latter is not used by Borg (cf. `Remote RPC protocol security`_, Borg itself does not implement any network access) and historically contained most vulnerabilities, especially critical ones. - -Historic vulnerabilities affecting libcrypto in ways relevant to Borg were flaws in primtives -enabling side-channel and similar attacks. - -Therefore, both using and bundling OpenSSL is considered unproblematic for Borg. +The static binaries released by the project contain neither libssl nor the Python ssl/_ssl modules. diff --git a/scripts/borg.exe.spec b/scripts/borg.exe.spec index 07dcdfbe..ea86a91d 100644 --- a/scripts/borg.exe.spec +++ b/scripts/borg.exe.spec @@ -16,7 +16,9 @@ a = Analysis([os.path.join(basepath, 'src/borg/__main__.py'), ], hiddenimports=['borg.platform.posix'], hookspath=[], runtime_hooks=[], - excludes=[], + excludes=[ + '_ssl', 'ssl', + ], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher) @@ -38,3 +40,16 @@ exe = EXE(pyz, strip=False, upx=True, console=True ) + +if False: + # Enable this block to build a directory-based binary instead of + # a packed single file. This allows to easily look at all included + # files (e.g. without having to strace or halt the built binary + # and introspect /tmp). + coll = COLLECT(exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + name='borg-dir') From b996afbc06ec8d5f31e851fb4672944fde585d93 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Jun 2017 13:05:54 +0200 Subject: [PATCH 0929/1387] docs/security: used implementations; note python libraries --- docs/internals/security.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/internals/security.rst b/docs/internals/security.rst index d028c7f9..34621938 100644 --- a/docs/internals/security.rst +++ b/docs/internals/security.rst @@ -254,9 +254,13 @@ on widely used libraries providing them: We think this is not an additional risk, since we don't ever use OpenSSL's networking, TLS or X.509 code, but only their primitives implemented in libcrypto. -- SHA-256 and SHA-512 from Python's hashlib_ standard library module are used +- SHA-256 and SHA-512 from Python's hashlib_ standard library module are used. + Borg requires a Python built with OpenSSL support (due to PBKDF2), therefore + these functions are delegated to OpenSSL by Python. - HMAC, PBKDF2 and a constant-time comparison from Python's hmac_ standard - library module is used. + library module is used. While the HMAC implementation is written in Python, + the PBKDF2 implementation is provided by OpenSSL. The constant-time comparison + (``compare_digest``) is written in C and part of Python. - BLAKE2b is either provided by the system's libb2, an official implementation, or a bundled copy of the BLAKE2 reference implementation (written in C). From 089224975b25f9086143f40b6153e24d75587fd3 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Jun 2017 13:12:43 +0200 Subject: [PATCH 0930/1387] docs: quotas: clarify compatbility; only relevant to serve side also cf. "Enforcing the quota": The quota is enforcible only if *all* :ref:`borg_serve` versions accessible to clients support quotas --- docs/internals/data-structures.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index e7fbb496..d7c70ce0 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -233,8 +233,11 @@ state). .. rubric:: Compatibility with older servers and enabling quota after-the-fact If no quota data is stored in the hints file, Borg assumes zero quota is used. -Thus, if a repository with an enabled quota is written to with an older version -that does not understand quotas, then the quota usage will be erased. +Thus, if a repository with an enabled quota is written to with an older ``borg serve`` +version that does not understand quotas, then the quota usage will be erased. + +The client version is irrelevant to the storage quota and has no part in it. +The form of error messages due to exceeding quota varies with client versions. A similar situation arises when upgrading from a Borg release that did not have quotas. Borg will start tracking quota use from the time of the upgrade, starting at zero. From d51f2bbbae10a9ae14cf5ddf570a0b7f35739bae Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Jun 2017 14:48:14 +0200 Subject: [PATCH 0931/1387] docs: quotas: local repo disclaimer ... --- docs/internals/data-structures.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index d7c70ce0..2a688f8b 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -215,6 +215,10 @@ Tracking effective size on the other hand accounts DELETEs immediately as freein The storage quota is meant as a robust mechanism for service providers, therefore :ref:`borg_serve` has to enforce it without loopholes (e.g. modified clients). +The following sections refer to using quotas on remotely accessed repositories. +For local access, consider *client* and *serve* the same. +Accordingly, quotas cannot be enforced with local access, +since the quota can be changed in the repository config. The quota is enforcible only if *all* :ref:`borg_serve` versions accessible to clients support quotas (see next section). Further, quota is From 740898d83ba9d83589bcbd607050d23007de318a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 7 Mar 2017 15:13:59 +0100 Subject: [PATCH 0932/1387] CacheSynchronizer --- setup.py | 2 +- src/borg/_cache.c | 159 +++++++++++++++++++++++++++++++++++++++++ src/borg/cache.py | 12 +--- src/borg/hashindex.pyx | 38 ++++++++-- src/borg/helpers.py | 2 +- 5 files changed, 198 insertions(+), 15 deletions(-) create mode 100644 src/borg/_cache.c diff --git a/setup.py b/setup.py index 726c849c..6878ed96 100644 --- a/setup.py +++ b/setup.py @@ -600,7 +600,7 @@ if not on_rtd: ext_modules += [ Extension('borg.compress', [compress_source], libraries=['lz4'], include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), Extension('borg.crypto.low_level', [crypto_ll_source], libraries=crypto_libraries, include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), - Extension('borg.hashindex', [hashindex_source]), + Extension('borg.hashindex', [hashindex_source], libraries=['msgpackc']), Extension('borg.item', [item_source]), Extension('borg.algorithms.chunker', [chunker_source]), Extension('borg.algorithms.checksums', [checksums_source]), diff --git a/src/borg/_cache.c b/src/borg/_cache.c new file mode 100644 index 00000000..880608ff --- /dev/null +++ b/src/borg/_cache.c @@ -0,0 +1,159 @@ + +#include + +// 2**32 - 1025 +#define _MAX_VALUE ( (uint32_t) 4294966271 ) + +#define MIN(x, y) ((x) < (y) ? (x): (y)) + +typedef struct { + HashIndex *chunks; + + msgpack_unpacker unpacker; + msgpack_unpacked unpacked; + const char *error; +} CacheSyncCtx; + +static CacheSyncCtx * +cache_sync_init(HashIndex *chunks) +{ + CacheSyncCtx *ctx; + if (!(ctx = malloc(sizeof(CacheSyncCtx)))) { + return NULL; + } + + ctx->chunks = chunks; + ctx->error = NULL; + + if(!msgpack_unpacker_init(&ctx->unpacker, MSGPACK_UNPACKER_INIT_BUFFER_SIZE)) { + free(ctx); + return NULL; + } + + msgpack_unpacked_init(&ctx->unpacked); + + return ctx; +} + +static void +cache_sync_free(CacheSyncCtx *ctx) +{ + msgpack_unpacker_destroy(&ctx->unpacker); + msgpack_unpacked_destroy(&ctx->unpacked); + free(ctx); +} + +static const char * +cache_sync_error(CacheSyncCtx *ctx) +{ + return ctx->error; +} + +static int +cache_process_chunks(CacheSyncCtx *ctx, msgpack_object_array *array) +{ + uint32_t i; + const char *key; + uint32_t cache_values[3]; + uint32_t *cache_entry; + uint64_t refcount; + msgpack_object *current; + for (i = 0; i < array->size; i++) { + current = &array->ptr[i]; + + if (current->type != MSGPACK_OBJECT_ARRAY || current->via.array.size != 3 + || current->via.array.ptr[0].type != MSGPACK_OBJECT_STR || current->via.array.ptr[0].via.str.size != 32 + || current->via.array.ptr[1].type != MSGPACK_OBJECT_POSITIVE_INTEGER + || current->via.array.ptr[2].type != MSGPACK_OBJECT_POSITIVE_INTEGER) { + ctx->error = "Malformed chunk list entry"; + return 0; + } + + key = current->via.array.ptr[0].via.str.ptr; + cache_entry = (uint32_t*) hashindex_get(ctx->chunks, key); + if (cache_entry) { + refcount = _le32toh(cache_entry[0]); + refcount += 1; + cache_entry[0] = _htole32(MIN(refcount, _MAX_VALUE)); + } else { + /* refcount, size, csize */ + cache_values[0] = 1; + cache_values[1] = current->via.array.ptr[1].via.u64; + cache_values[2] = current->via.array.ptr[2].via.u64; + if (!hashindex_set(ctx->chunks, key, cache_values)) { + ctx->error = "hashindex_set failed"; + return 0; + } + } + } + return 1; +} + +/** + * feed data to the cache synchronizer + * 0 = abort, 1 = continue + * abort is a regular condition, check cache_sync_error + */ +static int +cache_sync_feed(CacheSyncCtx *ctx, void *data, uint32_t length) +{ + msgpack_unpack_return unpack_status; + + /* grow buffer if necessary */ + if (msgpack_unpacker_buffer_capacity(&ctx->unpacker) < length) { + if (!msgpack_unpacker_reserve_buffer(&ctx->unpacker, length)) { + return 0; + } + } + + memcpy(msgpack_unpacker_buffer(&ctx->unpacker), data, length); + msgpack_unpacker_buffer_consumed(&ctx->unpacker, length); + + do { + unpack_status = msgpack_unpacker_next(&ctx->unpacker, &ctx->unpacked); + + switch (unpack_status) { + case MSGPACK_UNPACK_SUCCESS: + { + uint32_t i; + msgpack_object *item = &ctx->unpacked.data; + msgpack_object_kv *current; + + if (item->type != MSGPACK_OBJECT_MAP) { + ctx->error = "Unexpected data type in item stream"; + return 0; + } + + for (i = 0; i < item->via.map.size; i++) { + current = &item->via.map.ptr[i]; + + if (current->key.type != MSGPACK_OBJECT_STR) { + ctx->error = "Invalid key data type in item"; + return 0; + } + + if (current->key.via.str.size == 6 + && !memcmp(current->key.via.str.ptr, "chunks", 6)) { + + if (current->val.type != MSGPACK_OBJECT_ARRAY) { + ctx->error = "Unexpected value type of item chunks"; + return 0; + } + + if (!cache_process_chunks(ctx, ¤t->val.via.array)) { + return 0; + } + } + } + } + break; + case MSGPACK_UNPACK_PARSE_ERROR: + ctx->error = "Malformed msgpack"; + return 0; + default: + break; + } + } while (unpack_status != MSGPACK_UNPACK_CONTINUE); + + return 1; +} diff --git a/src/borg/cache.py b/src/borg/cache.py index 13045f0e..c9fa70b7 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -12,7 +12,7 @@ from .logger import create_logger logger = create_logger() from .constants import CACHE_README -from .hashindex import ChunkIndex, ChunkIndexEntry +from .hashindex import ChunkIndex, ChunkIndexEntry, CacheSynchronizer from .helpers import Location from .helpers import Error from .helpers import get_cache_dir, get_security_dir @@ -571,17 +571,11 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" archive = ArchiveItem(internal_dict=msgpack.unpackb(data)) if archive.version != 1: raise Exception('Unknown archive metadata version') - unpacker = msgpack.Unpacker() + sync = CacheSynchronizer(chunk_idx) for item_id, chunk in zip(archive.items, repository.get_many(archive.items)): data = key.decrypt(item_id, chunk) chunk_idx.add(item_id, 1, len(data), len(chunk)) - unpacker.feed(data) - for item in unpacker: - if not isinstance(item, dict): - logger.error('Error: Did not get expected metadata dict - archive corrupted!') - continue # XXX: continue?! - for chunk_id, size, csize in item.get(b'chunks', []): - chunk_idx.add(chunk_id, 1, size, csize) + sync.feed(data) if self.do_cache: fn = mkpath(archive_id) fn_tmp = mkpath(archive_id, suffix='.tmp') diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 2409836f..75ba1df3 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -8,7 +8,7 @@ from libc.stdint cimport uint32_t, UINT32_MAX, uint64_t from libc.errno cimport errno from cpython.exc cimport PyErr_SetFromErrnoWithFilename -API_VERSION = '1.1_01' +API_VERSION = '1.1_02' cdef extern from "_hashindex.c": @@ -31,6 +31,18 @@ cdef extern from "_hashindex.c": double HASH_MAX_LOAD +cdef extern from "_cache.c": + ctypedef struct CacheSyncCtx: + pass + + CacheSyncCtx *cache_sync_init(HashIndex *chunks) + const char *cache_sync_error(CacheSyncCtx *ctx) + int cache_sync_feed(CacheSyncCtx *ctx, void *data, uint32_t length) + void cache_sync_free(CacheSyncCtx *ctx) + + uint32_t _MAX_VALUE + + cdef _NoDefault = object() """ @@ -50,9 +62,6 @@ AssertionError is raised instead. assert UINT32_MAX == 2**32-1 -# module-level constant because cdef's in classes can't have default values -cdef uint32_t _MAX_VALUE = 2**32-1025 - assert _MAX_VALUE % 2 == 1 @@ -375,3 +384,24 @@ cdef class ChunkKeyIterator: cdef uint32_t refcount = _le32toh(value[0]) assert refcount <= _MAX_VALUE, "invalid reference count" return (self.key)[:self.key_size], ChunkIndexEntry(refcount, _le32toh(value[1]), _le32toh(value[2])) + + +cdef class CacheSynchronizer: + cdef ChunkIndex chunks + cdef CacheSyncCtx *sync + + def __cinit__(self, chunks): + self.chunks = chunks + self.sync = cache_sync_init(self.chunks.index) + if not self.sync: + raise Exception('cache_sync_init failed') + + def __dealloc__(self): + if self.sync: + cache_sync_free(self.sync) + + def feed(self, chunk): + if not cache_sync_feed(self.sync, chunk, len(chunk)): + error = cache_sync_error(self.sync) + if error is not None: + raise Exception('cache_sync_feed failed: ' + error.decode('ascii')) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index db66b822..7e4d4baf 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -126,7 +126,7 @@ def check_python(): def check_extension_modules(): from . import platform, compress, item - if hashindex.API_VERSION != '1.1_01': + if hashindex.API_VERSION != '1.1_02': raise ExtensionModuleError if chunker.API_VERSION != '1.1_01': raise ExtensionModuleError From 9f8b967a6f45bb7dbbf1f37cf231ea82f149a0f6 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 26 May 2017 12:30:15 +0200 Subject: [PATCH 0933/1387] cache sync: initialize master index to known capacity --- src/borg/cache.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index c9fa70b7..cd3a9951 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -603,6 +603,9 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" # deallocates old hashindex, creates empty hashindex: chunk_idx.clear() cleanup_outdated(cached_ids - archive_ids) + # Explicitly set the initial hash table capacity to avoid performance issues + # due to hash table "resonance". + master_index_capacity = int(len(self.repository) / ChunkIndex.MAX_LOAD_FACTOR) if archive_ids: chunk_idx = None if self.progress: @@ -630,7 +633,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" # Do not make this an else branch; the FileIntegrityError exception handler # above can remove *archive_id* from *cached_ids*. logger.info('Fetching and building archive index for %s ...', archive_name) - archive_chunk_idx = ChunkIndex() + archive_chunk_idx = ChunkIndex(master_index_capacity) fetch_and_build_idx(archive_id, repository, self.key, archive_chunk_idx) logger.info("Merging into master chunks index ...") if chunk_idx is None: @@ -641,7 +644,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" else: chunk_idx.merge(archive_chunk_idx) else: - chunk_idx = chunk_idx or ChunkIndex() + chunk_idx = chunk_idx or ChunkIndex(master_index_capacity) logger.info('Fetching archive index for %s ...', archive_name) fetch_and_build_idx(archive_id, repository, self.key, chunk_idx) if self.progress: From 167875b753290f468751a973f0be719972c265a9 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 26 May 2017 13:54:28 +0200 Subject: [PATCH 0934/1387] cache sync: fix n^2 behaviour in lookup_name --- src/borg/cache.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index cd3a9951..e34a3426 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -588,10 +588,16 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" else: os.rename(fn_tmp, fn) - def lookup_name(archive_id): + def get_archive_ids_to_names(archive_ids): + # Pass once over all archives and build a mapping from ids to names. + # The easier approach, doing a similar loop for each archive, has + # square complexity and does about a dozen million functions calls + # with 1100 archives (which takes 30s CPU seconds _alone_). + archive_names = {} for info in self.manifest.archives.list(): - if info.id == archive_id: - return info.name + if info.id in archive_ids: + archive_names[info.id] = info.name + return archive_names def create_master_idx(chunk_idx): logger.info('Synchronizing chunks cache...') @@ -612,8 +618,9 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" pi = ProgressIndicatorPercent(total=len(archive_ids), step=0.1, msg='%3.0f%% Syncing chunks cache. Processing archive %s', msgid='cache.sync') + archive_ids_to_names = get_archive_ids_to_names(archive_ids) for archive_id in archive_ids: - archive_name = lookup_name(archive_id) + archive_name = archive_ids_to_names.pop(archive_id) if self.progress: pi.show(info=[remove_surrogates(archive_name)]) if self.do_cache: From bf895950acb27872b4c084f394df0f7f0c027d60 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 20 May 2016 01:54:42 +0200 Subject: [PATCH 0935/1387] RepositoryCache: limit cache size Unbounded cache size is inacceptable. I don't see why a full-fledged repository needs to be used here, either, since this cache requires none of the consistency or durability guarantees made by it (and bought with a performance impact). A notable issue is that posix_fadvise is slow (for some reason) on tmpfs, which could eat 30-35 % of the total CPU time of a cache sync. --- src/borg/remote.py | 102 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 20 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 47c59741..c991dbef 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -7,6 +7,7 @@ import logging import os import select import shlex +import shutil import sys import tempfile import textwrap @@ -23,6 +24,7 @@ from .helpers import get_home_dir from .helpers import hostname_is_unique from .helpers import replace_placeholders from .helpers import sysinfo +from .helpers import format_file_size from .logger import create_logger, setup_logging from .repository import Repository, MAX_OBJECT_SIZE, LIST_SCAN_LIMIT from .version import parse_version, format_version @@ -1058,45 +1060,105 @@ class RepositoryNoCache: self.close() def get(self, key): - return next(self.get_many([key])) + return next(self.get_many([key], cache=False)) - def get_many(self, keys): + def get_many(self, keys, cache=True): for data in self.repository.get_many(keys): yield data class RepositoryCache(RepositoryNoCache): - """A caching Repository wrapper - - Caches Repository GET operations using a local temporary Repository. """ - # maximum object size that will be cached, 64 kiB. - THRESHOLD = 2**16 + A caching Repository wrapper. + + Caches Repository GET operations locally. + """ def __init__(self, repository): super().__init__(repository) - tmppath = tempfile.mkdtemp(prefix='borg-tmp') - self.caching_repo = Repository(tmppath, create=True, exclusive=True) - self.caching_repo.__enter__() # handled by context manager in base class + self.cache = set() + self.basedir = tempfile.mkdtemp(prefix='borg-cache-') + self.query_size_limit() + self.size = 0 + # Instrumentation + self.hits = 0 + self.misses = 0 + self.slow_misses = 0 + self.slow_lat = 0.0 + self.evictions = 0 + self.enospc = 0 + + def query_size_limit(self): + stat_fs = os.statvfs(self.basedir) + available_space = stat_fs.f_bsize * stat_fs.f_bavail + self.size_limit = int(min(available_space * 0.25, 2**31)) + + def key_filename(self, key): + return os.path.join(self.basedir, bin_to_hex(key)) + + def backoff(self): + self.query_size_limit() + target_size = int(0.9 * self.size_limit) + while self.size > target_size and self.cache: + key = self.cache.pop() + file = self.key_filename(key) + self.size -= os.stat(file).st_size + os.unlink(file) + self.evictions += 1 + + def add_entry(self, key, data): + file = self.key_filename(key) + try: + with open(file, 'wb') as fd: + fd.write(data) + except OSError as os_error: + if os_error.errno == errno.ENOSPC: + self.enospc += 1 + self.backoff() + else: + raise + else: + self.size += len(data) + self.cache.add(key) + if self.size > self.size_limit: + self.backoff() + return data def close(self): - if self.caching_repo is not None: - self.caching_repo.destroy() - self.caching_repo = None + logger.debug('RepositoryCache: current items %d, size %s / %s, %d hits, %d misses, %d slow misses (+%.1fs), ' + '%d evictions, %d ENOSPC hit', + len(self.cache), format_file_size(self.size), format_file_size(self.size_limit), + self.hits, self.misses, self.slow_misses, self.slow_lat, + self.evictions, self.enospc) + self.cache.clear() + shutil.rmtree(self.basedir) - def get_many(self, keys): - unknown_keys = [key for key in keys if key not in self.caching_repo] + def get_many(self, keys, cache=True): + unknown_keys = [key for key in keys if key not in self.cache] repository_iterator = zip(unknown_keys, self.repository.get_many(unknown_keys)) for key in keys: - try: - yield self.caching_repo.get(key) - except Repository.ObjectNotFound: + if key in self.cache: + file = self.key_filename(key) + with open(file, 'rb') as fd: + self.hits += 1 + yield fd.read() + else: for key_, data in repository_iterator: if key_ == key: - if len(data) <= self.THRESHOLD: - self.caching_repo.put(key, data) + if cache: + self.add_entry(key, data) + self.misses += 1 yield data break + else: + # slow path: eviction during this get_many removed this key from the cache + t0 = time.perf_counter() + data = self.repository.get(key) + self.slow_lat += time.perf_counter() - t0 + if cache: + self.add_entry(key, data) + self.slow_misses += 1 + yield data # Consume any pending requests for _ in repository_iterator: pass From c786a5941eb560b51264c24e18f9c1442fb721bb Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 26 May 2017 22:54:27 +0200 Subject: [PATCH 0936/1387] CacheSynchronizer: redo as quasi FSM on top of unpack.h This is a (relatively) simple state machine running in the data callbacks invoked by the msgpack unpacking stack machine (the same machine is used in msgpack-c and msgpack-python, changes are minor and cosmetic, e.g. removal of msgpack_unpack_object, removal of the C++ template thus porting to C and so on). Compared to the previous solution this has multiple advantages - msgpack-c dependency is removed - this approach is faster and requires fewer and smaller memory allocations Testability of the two solutions does not differ in my professional opinion(tm). Two other changes were rolled up; _hashindex.c can be compiled without Python.h again (handy for fuzzing and testing); a "small" bug in the cache sync was fixed which allocated too large archive indices, leading to excessive archive.chunks.d disk usage (that actually gave me an idea). --- scripts/fuzz-cache-sync/HOWTO | 10 + scripts/fuzz-cache-sync/main.c | 30 ++ .../fuzz-cache-sync/testcase_dir/test_simple | Bin 0 -> 119 bytes setup.py | 4 +- src/borg/_cache.c | 159 -------- src/borg/_hashindex.c | 19 +- src/borg/cache.py | 2 +- src/borg/cache_sync/cache_sync.c | 108 +++++ src/borg/cache_sync/sysdep.h | 194 +++++++++ src/borg/cache_sync/unpack.h | 374 ++++++++++++++++++ src/borg/cache_sync/unpack_define.h | 95 +++++ src/borg/cache_sync/unpack_template.h | 359 +++++++++++++++++ src/borg/hashindex.pyx | 6 +- src/borg/testsuite/cache.py | 139 +++++++ 14 files changed, 1332 insertions(+), 167 deletions(-) create mode 100644 scripts/fuzz-cache-sync/HOWTO create mode 100644 scripts/fuzz-cache-sync/main.c create mode 100644 scripts/fuzz-cache-sync/testcase_dir/test_simple delete mode 100644 src/borg/_cache.c create mode 100644 src/borg/cache_sync/cache_sync.c create mode 100644 src/borg/cache_sync/sysdep.h create mode 100644 src/borg/cache_sync/unpack.h create mode 100644 src/borg/cache_sync/unpack_define.h create mode 100644 src/borg/cache_sync/unpack_template.h create mode 100644 src/borg/testsuite/cache.py diff --git a/scripts/fuzz-cache-sync/HOWTO b/scripts/fuzz-cache-sync/HOWTO new file mode 100644 index 00000000..ae144b28 --- /dev/null +++ b/scripts/fuzz-cache-sync/HOWTO @@ -0,0 +1,10 @@ +- Install AFL and the requirements for LLVM mode (see docs) +- Compile the fuzzing target, e.g. + + AFL_HARDEN=1 afl-clang-fast main.c -o fuzz-target -O3 + + (other options, like using ASan or MSan are possible as well) +- Add additional test cases to testcase_dir +- Run afl, easiest (but inefficient) way; + + afl-fuzz -i testcase_dir -o findings_dir ./fuzz-target diff --git a/scripts/fuzz-cache-sync/main.c b/scripts/fuzz-cache-sync/main.c new file mode 100644 index 00000000..b25d925d --- /dev/null +++ b/scripts/fuzz-cache-sync/main.c @@ -0,0 +1,30 @@ +#include "../../src/borg/_hashindex.c" +#include "../../src/borg/cache_sync/cache_sync.c" + +#define BUFSZ 32768 + +int main() { + char buf[BUFSZ]; + int len, ret; + CacheSyncCtx *ctx; + HashIndex *idx; + + /* capacity, key size, value size */ + idx = hashindex_init(0, 32, 12); + ctx = cache_sync_init(idx); + + while (1) { + len = read(0, buf, BUFSZ); + if (!len) { + break; + } + ret = cache_sync_feed(ctx, buf, len); + if(!ret && cache_sync_error(ctx)) { + fprintf(stderr, "error: %s\n", cache_sync_error(ctx)); + return 1; + } + } + hashindex_free(idx); + cache_sync_free(ctx); + return 0; +} diff --git a/scripts/fuzz-cache-sync/testcase_dir/test_simple b/scripts/fuzz-cache-sync/testcase_dir/test_simple new file mode 100644 index 0000000000000000000000000000000000000000..0bf5a0ea1616fcbcec4e17de7b347b00f5661ca1 GIT binary patch literal 119 zcmZo&oR*)zI4Q9Rh^x-BTmmuAis>yWElw?3mYh+Vmt72{CQZJ@pkRO>7&0;up~{Gf F82}?xBQF2| literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index 6878ed96..951d61af 100644 --- a/setup.py +++ b/setup.py @@ -91,6 +91,8 @@ try: 'src/borg/crypto/low_level.c', 'src/borg/algorithms/chunker.c', 'src/borg/algorithms/buzhash.c', 'src/borg/hashindex.c', 'src/borg/_hashindex.c', + 'src/borg/cache_sync/cache_sync.c', 'src/borg/cache_sync/sysdep.h', 'src/borg/cache_sync/unpack.h', + 'src/borg/cache_sync/unpack_define.h', 'src/borg/cache_sync/unpack_template.h', 'src/borg/item.c', 'src/borg/algorithms/checksums.c', 'src/borg/algorithms/crc32_dispatch.c', 'src/borg/algorithms/crc32_clmul.c', 'src/borg/algorithms/crc32_slice_by_8.c', @@ -600,7 +602,7 @@ if not on_rtd: ext_modules += [ Extension('borg.compress', [compress_source], libraries=['lz4'], include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), Extension('borg.crypto.low_level', [crypto_ll_source], libraries=crypto_libraries, include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), - Extension('borg.hashindex', [hashindex_source], libraries=['msgpackc']), + Extension('borg.hashindex', [hashindex_source]), Extension('borg.item', [item_source]), Extension('borg.algorithms.chunker', [chunker_source]), Extension('borg.algorithms.checksums', [checksums_source]), diff --git a/src/borg/_cache.c b/src/borg/_cache.c deleted file mode 100644 index 880608ff..00000000 --- a/src/borg/_cache.c +++ /dev/null @@ -1,159 +0,0 @@ - -#include - -// 2**32 - 1025 -#define _MAX_VALUE ( (uint32_t) 4294966271 ) - -#define MIN(x, y) ((x) < (y) ? (x): (y)) - -typedef struct { - HashIndex *chunks; - - msgpack_unpacker unpacker; - msgpack_unpacked unpacked; - const char *error; -} CacheSyncCtx; - -static CacheSyncCtx * -cache_sync_init(HashIndex *chunks) -{ - CacheSyncCtx *ctx; - if (!(ctx = malloc(sizeof(CacheSyncCtx)))) { - return NULL; - } - - ctx->chunks = chunks; - ctx->error = NULL; - - if(!msgpack_unpacker_init(&ctx->unpacker, MSGPACK_UNPACKER_INIT_BUFFER_SIZE)) { - free(ctx); - return NULL; - } - - msgpack_unpacked_init(&ctx->unpacked); - - return ctx; -} - -static void -cache_sync_free(CacheSyncCtx *ctx) -{ - msgpack_unpacker_destroy(&ctx->unpacker); - msgpack_unpacked_destroy(&ctx->unpacked); - free(ctx); -} - -static const char * -cache_sync_error(CacheSyncCtx *ctx) -{ - return ctx->error; -} - -static int -cache_process_chunks(CacheSyncCtx *ctx, msgpack_object_array *array) -{ - uint32_t i; - const char *key; - uint32_t cache_values[3]; - uint32_t *cache_entry; - uint64_t refcount; - msgpack_object *current; - for (i = 0; i < array->size; i++) { - current = &array->ptr[i]; - - if (current->type != MSGPACK_OBJECT_ARRAY || current->via.array.size != 3 - || current->via.array.ptr[0].type != MSGPACK_OBJECT_STR || current->via.array.ptr[0].via.str.size != 32 - || current->via.array.ptr[1].type != MSGPACK_OBJECT_POSITIVE_INTEGER - || current->via.array.ptr[2].type != MSGPACK_OBJECT_POSITIVE_INTEGER) { - ctx->error = "Malformed chunk list entry"; - return 0; - } - - key = current->via.array.ptr[0].via.str.ptr; - cache_entry = (uint32_t*) hashindex_get(ctx->chunks, key); - if (cache_entry) { - refcount = _le32toh(cache_entry[0]); - refcount += 1; - cache_entry[0] = _htole32(MIN(refcount, _MAX_VALUE)); - } else { - /* refcount, size, csize */ - cache_values[0] = 1; - cache_values[1] = current->via.array.ptr[1].via.u64; - cache_values[2] = current->via.array.ptr[2].via.u64; - if (!hashindex_set(ctx->chunks, key, cache_values)) { - ctx->error = "hashindex_set failed"; - return 0; - } - } - } - return 1; -} - -/** - * feed data to the cache synchronizer - * 0 = abort, 1 = continue - * abort is a regular condition, check cache_sync_error - */ -static int -cache_sync_feed(CacheSyncCtx *ctx, void *data, uint32_t length) -{ - msgpack_unpack_return unpack_status; - - /* grow buffer if necessary */ - if (msgpack_unpacker_buffer_capacity(&ctx->unpacker) < length) { - if (!msgpack_unpacker_reserve_buffer(&ctx->unpacker, length)) { - return 0; - } - } - - memcpy(msgpack_unpacker_buffer(&ctx->unpacker), data, length); - msgpack_unpacker_buffer_consumed(&ctx->unpacker, length); - - do { - unpack_status = msgpack_unpacker_next(&ctx->unpacker, &ctx->unpacked); - - switch (unpack_status) { - case MSGPACK_UNPACK_SUCCESS: - { - uint32_t i; - msgpack_object *item = &ctx->unpacked.data; - msgpack_object_kv *current; - - if (item->type != MSGPACK_OBJECT_MAP) { - ctx->error = "Unexpected data type in item stream"; - return 0; - } - - for (i = 0; i < item->via.map.size; i++) { - current = &item->via.map.ptr[i]; - - if (current->key.type != MSGPACK_OBJECT_STR) { - ctx->error = "Invalid key data type in item"; - return 0; - } - - if (current->key.via.str.size == 6 - && !memcmp(current->key.via.str.ptr, "chunks", 6)) { - - if (current->val.type != MSGPACK_OBJECT_ARRAY) { - ctx->error = "Unexpected value type of item chunks"; - return 0; - } - - if (!cache_process_chunks(ctx, ¤t->val.via.array)) { - return 0; - } - } - } - } - break; - case MSGPACK_UNPACK_PARSE_ERROR: - ctx->error = "Malformed msgpack"; - return 0; - default: - break; - } - } while (unpack_status != MSGPACK_UNPACK_CONTINUE); - - return 1; -} diff --git a/src/borg/_hashindex.c b/src/borg/_hashindex.c index b41d57b7..824a6eec 100644 --- a/src/borg/_hashindex.c +++ b/src/borg/_hashindex.c @@ -1,6 +1,6 @@ -#include #include +#include #include #include #include @@ -56,8 +56,10 @@ typedef struct { int lower_limit; int upper_limit; int min_empty; +#ifdef Py_PYTHON_H /* buckets may be backed by a Python buffer. If buckets_buffer.buf is NULL then this is not used. */ Py_buffer buckets_buffer; +#endif } HashIndex; /* prime (or w/ big prime factors) hash table sizes @@ -106,8 +108,11 @@ static int hash_sizes[] = { #define EPRINTF(msg, ...) fprintf(stderr, "hashindex: " msg "(%s)\n", ##__VA_ARGS__, strerror(errno)) #define EPRINTF_PATH(path, msg, ...) fprintf(stderr, "hashindex: %s: " msg " (%s)\n", path, ##__VA_ARGS__, strerror(errno)) +#ifdef Py_PYTHON_H static HashIndex *hashindex_read(PyObject *file_py); static void hashindex_write(HashIndex *index, PyObject *file_py); +#endif + static HashIndex *hashindex_init(int capacity, int key_size, int value_size); static const void *hashindex_get(HashIndex *index, const void *key); static int hashindex_set(HashIndex *index, const void *key, const void *value); @@ -120,9 +125,12 @@ static void hashindex_free(HashIndex *index); static void hashindex_free_buckets(HashIndex *index) { +#ifdef Py_PYTHON_H if(index->buckets_buffer.buf) { PyBuffer_Release(&index->buckets_buffer); - } else { + } else +#endif + { free(index->buckets); } } @@ -263,6 +271,7 @@ count_empty(HashIndex *index) /* Public API */ +#ifdef Py_PYTHON_H static HashIndex * hashindex_read(PyObject *file_py) { @@ -418,6 +427,7 @@ fail_decref_header: fail: return index; } +#endif static HashIndex * hashindex_init(int capacity, int key_size, int value_size) @@ -444,7 +454,9 @@ hashindex_init(int capacity, int key_size, int value_size) index->lower_limit = get_lower_limit(index->num_buckets); index->upper_limit = get_upper_limit(index->num_buckets); index->min_empty = get_min_empty(index->num_buckets); +#ifdef Py_PYTHON_H index->buckets_buffer.buf = NULL; +#endif for(i = 0; i < capacity; i++) { BUCKET_MARK_EMPTY(index, i); } @@ -458,7 +470,7 @@ hashindex_free(HashIndex *index) free(index); } - +#ifdef Py_PYTHON_H static void hashindex_write(HashIndex *index, PyObject *file_py) { @@ -521,6 +533,7 @@ hashindex_write(HashIndex *index, PyObject *file_py) return; } } +#endif static const void * hashindex_get(HashIndex *index, const void *key) diff --git a/src/borg/cache.py b/src/borg/cache.py index e34a3426..934f12a7 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -640,7 +640,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" # Do not make this an else branch; the FileIntegrityError exception handler # above can remove *archive_id* from *cached_ids*. logger.info('Fetching and building archive index for %s ...', archive_name) - archive_chunk_idx = ChunkIndex(master_index_capacity) + archive_chunk_idx = ChunkIndex() fetch_and_build_idx(archive_id, repository, self.key, archive_chunk_idx) logger.info("Merging into master chunks index ...") if chunk_idx is None: diff --git a/src/borg/cache_sync/cache_sync.c b/src/borg/cache_sync/cache_sync.c new file mode 100644 index 00000000..174e4b90 --- /dev/null +++ b/src/borg/cache_sync/cache_sync.c @@ -0,0 +1,108 @@ + +#include "unpack.h" + +typedef struct { + unpack_context ctx; + + char *buf; + size_t head; + size_t tail; + size_t size; +} CacheSyncCtx; + +static CacheSyncCtx * +cache_sync_init(HashIndex *chunks) +{ + CacheSyncCtx *ctx; + if (!(ctx = (CacheSyncCtx*)malloc(sizeof(CacheSyncCtx)))) { + return NULL; + } + + unpack_init(&ctx->ctx); + + ctx->ctx.user.chunks = chunks; + ctx->ctx.user.last_error = NULL; + ctx->ctx.user.level = 0; + ctx->ctx.user.inside_chunks = false; + ctx->buf = NULL; + ctx->head = 0; + ctx->tail = 0; + ctx->size = 0; + + return ctx; +} + +static void +cache_sync_free(CacheSyncCtx *ctx) +{ + if(ctx->buf) { + free(ctx->buf); + } + free(ctx); +} + +static const char * +cache_sync_error(CacheSyncCtx *ctx) +{ + return ctx->ctx.user.last_error; +} + +/** + * feed data to the cache synchronizer + * 0 = abort, 1 = continue + * abort is a regular condition, check cache_sync_error + */ +static int +cache_sync_feed(CacheSyncCtx *ctx, void *data, uint32_t length) +{ + size_t new_size; + int ret; + char *new_buf; + + if(ctx->tail + length > ctx->size) { + if((ctx->tail - ctx->head) + length <= ctx->size) { + /* | XXXXX| -> move data in buffer backwards -> |XXXXX | */ + memmove(ctx->buf, ctx->buf + ctx->head, ctx->tail - ctx->head); + ctx->tail -= ctx->head; + ctx->head = 0; + } else { + /* must expand buffer to fit all data */ + new_size = (ctx->tail - ctx->head) + length; + new_buf = (char*) malloc(new_size); + if(!new_buf) { + ctx->ctx.user.last_error = "cache_sync_feed: unable to allocate buffer"; + return 0; + } + memcpy(new_buf, ctx->buf + ctx->head, ctx->tail - ctx->head); + free(ctx->buf); + ctx->buf = new_buf; + ctx->tail -= ctx->head; + ctx->head = 0; + ctx->size = new_size; + } + } + + memcpy(ctx->buf + ctx->tail, data, length); + ctx->tail += length; + + while(1) { + if(ctx->head >= ctx->tail) { + return 1; /* request more bytes */ + } + + ret = unpack_execute(&ctx->ctx, ctx->buf, ctx->tail, &ctx->head); + if(ret == 1) { + unpack_init(&ctx->ctx); + continue; + } else if(ret == 0) { + return 1; + } else { + if(!ctx->ctx.user.last_error) { + ctx->ctx.user.last_error = "Unknown error"; + } + return 0; + } + } + /* unreachable */ + return 1; +} diff --git a/src/borg/cache_sync/sysdep.h b/src/borg/cache_sync/sysdep.h new file mode 100644 index 00000000..ed9c1bc0 --- /dev/null +++ b/src/borg/cache_sync/sysdep.h @@ -0,0 +1,194 @@ +/* + * MessagePack system dependencies + * + * Copyright (C) 2008-2010 FURUHASHI Sadayuki + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MSGPACK_SYSDEP_H__ +#define MSGPACK_SYSDEP_H__ + +#include +#include +#if defined(_MSC_VER) && _MSC_VER < 1600 +typedef __int8 int8_t; +typedef unsigned __int8 uint8_t; +typedef __int16 int16_t; +typedef unsigned __int16 uint16_t; +typedef __int32 int32_t; +typedef unsigned __int32 uint32_t; +typedef __int64 int64_t; +typedef unsigned __int64 uint64_t; +#elif defined(_MSC_VER) // && _MSC_VER >= 1600 +#include +#else +#include +#include +#endif + +#ifdef _WIN32 +#define _msgpack_atomic_counter_header +typedef long _msgpack_atomic_counter_t; +#define _msgpack_sync_decr_and_fetch(ptr) InterlockedDecrement(ptr) +#define _msgpack_sync_incr_and_fetch(ptr) InterlockedIncrement(ptr) +#elif defined(__GNUC__) && ((__GNUC__*10 + __GNUC_MINOR__) < 41) +#define _msgpack_atomic_counter_header "gcc_atomic.h" +#else +typedef unsigned int _msgpack_atomic_counter_t; +#define _msgpack_sync_decr_and_fetch(ptr) __sync_sub_and_fetch(ptr, 1) +#define _msgpack_sync_incr_and_fetch(ptr) __sync_add_and_fetch(ptr, 1) +#endif + +#ifdef _WIN32 + +#ifdef __cplusplus +/* numeric_limits::min,max */ +#ifdef max +#undef max +#endif +#ifdef min +#undef min +#endif +#endif + +#else +#include /* __BYTE_ORDER */ +#endif + +#if !defined(__LITTLE_ENDIAN__) && !defined(__BIG_ENDIAN__) +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define __LITTLE_ENDIAN__ +#elif __BYTE_ORDER == __BIG_ENDIAN +#define __BIG_ENDIAN__ +#elif _WIN32 +#define __LITTLE_ENDIAN__ +#endif +#endif + + +#ifdef __LITTLE_ENDIAN__ + +#ifdef _WIN32 +# if defined(ntohs) +# define _msgpack_be16(x) ntohs(x) +# elif defined(_byteswap_ushort) || (defined(_MSC_VER) && _MSC_VER >= 1400) +# define _msgpack_be16(x) ((uint16_t)_byteswap_ushort((unsigned short)x)) +# else +# define _msgpack_be16(x) ( \ + ((((uint16_t)x) << 8) ) | \ + ((((uint16_t)x) >> 8) ) ) +# endif +#else +# define _msgpack_be16(x) ntohs(x) +#endif + +#ifdef _WIN32 +# if defined(ntohl) +# define _msgpack_be32(x) ntohl(x) +# elif defined(_byteswap_ulong) || (defined(_MSC_VER) && _MSC_VER >= 1400) +# define _msgpack_be32(x) ((uint32_t)_byteswap_ulong((unsigned long)x)) +# else +# define _msgpack_be32(x) \ + ( ((((uint32_t)x) << 24) ) | \ + ((((uint32_t)x) << 8) & 0x00ff0000U ) | \ + ((((uint32_t)x) >> 8) & 0x0000ff00U ) | \ + ((((uint32_t)x) >> 24) ) ) +# endif +#else +# define _msgpack_be32(x) ntohl(x) +#endif + +#if defined(_byteswap_uint64) || (defined(_MSC_VER) && _MSC_VER >= 1400) +# define _msgpack_be64(x) (_byteswap_uint64(x)) +#elif defined(bswap_64) +# define _msgpack_be64(x) bswap_64(x) +#elif defined(__DARWIN_OSSwapInt64) +# define _msgpack_be64(x) __DARWIN_OSSwapInt64(x) +#else +#define _msgpack_be64(x) \ + ( ((((uint64_t)x) << 56) ) | \ + ((((uint64_t)x) << 40) & 0x00ff000000000000ULL ) | \ + ((((uint64_t)x) << 24) & 0x0000ff0000000000ULL ) | \ + ((((uint64_t)x) << 8) & 0x000000ff00000000ULL ) | \ + ((((uint64_t)x) >> 8) & 0x00000000ff000000ULL ) | \ + ((((uint64_t)x) >> 24) & 0x0000000000ff0000ULL ) | \ + ((((uint64_t)x) >> 40) & 0x000000000000ff00ULL ) | \ + ((((uint64_t)x) >> 56) ) ) +#endif + +#define _msgpack_load16(cast, from) ((cast)( \ + (((uint16_t)((uint8_t*)(from))[0]) << 8) | \ + (((uint16_t)((uint8_t*)(from))[1]) ) )) + +#define _msgpack_load32(cast, from) ((cast)( \ + (((uint32_t)((uint8_t*)(from))[0]) << 24) | \ + (((uint32_t)((uint8_t*)(from))[1]) << 16) | \ + (((uint32_t)((uint8_t*)(from))[2]) << 8) | \ + (((uint32_t)((uint8_t*)(from))[3]) ) )) + +#define _msgpack_load64(cast, from) ((cast)( \ + (((uint64_t)((uint8_t*)(from))[0]) << 56) | \ + (((uint64_t)((uint8_t*)(from))[1]) << 48) | \ + (((uint64_t)((uint8_t*)(from))[2]) << 40) | \ + (((uint64_t)((uint8_t*)(from))[3]) << 32) | \ + (((uint64_t)((uint8_t*)(from))[4]) << 24) | \ + (((uint64_t)((uint8_t*)(from))[5]) << 16) | \ + (((uint64_t)((uint8_t*)(from))[6]) << 8) | \ + (((uint64_t)((uint8_t*)(from))[7]) ) )) + +#else + +#define _msgpack_be16(x) (x) +#define _msgpack_be32(x) (x) +#define _msgpack_be64(x) (x) + +#define _msgpack_load16(cast, from) ((cast)( \ + (((uint16_t)((uint8_t*)from)[0]) << 8) | \ + (((uint16_t)((uint8_t*)from)[1]) ) )) + +#define _msgpack_load32(cast, from) ((cast)( \ + (((uint32_t)((uint8_t*)from)[0]) << 24) | \ + (((uint32_t)((uint8_t*)from)[1]) << 16) | \ + (((uint32_t)((uint8_t*)from)[2]) << 8) | \ + (((uint32_t)((uint8_t*)from)[3]) ) )) + +#define _msgpack_load64(cast, from) ((cast)( \ + (((uint64_t)((uint8_t*)from)[0]) << 56) | \ + (((uint64_t)((uint8_t*)from)[1]) << 48) | \ + (((uint64_t)((uint8_t*)from)[2]) << 40) | \ + (((uint64_t)((uint8_t*)from)[3]) << 32) | \ + (((uint64_t)((uint8_t*)from)[4]) << 24) | \ + (((uint64_t)((uint8_t*)from)[5]) << 16) | \ + (((uint64_t)((uint8_t*)from)[6]) << 8) | \ + (((uint64_t)((uint8_t*)from)[7]) ) )) +#endif + + +#define _msgpack_store16(to, num) \ + do { uint16_t val = _msgpack_be16(num); memcpy(to, &val, 2); } while(0) +#define _msgpack_store32(to, num) \ + do { uint32_t val = _msgpack_be32(num); memcpy(to, &val, 4); } while(0) +#define _msgpack_store64(to, num) \ + do { uint64_t val = _msgpack_be64(num); memcpy(to, &val, 8); } while(0) + +/* +#define _msgpack_load16(cast, from) \ + ({ cast val; memcpy(&val, (char*)from, 2); _msgpack_be16(val); }) +#define _msgpack_load32(cast, from) \ + ({ cast val; memcpy(&val, (char*)from, 4); _msgpack_be32(val); }) +#define _msgpack_load64(cast, from) \ + ({ cast val; memcpy(&val, (char*)from, 8); _msgpack_be64(val); }) +*/ + + +#endif /* msgpack/sysdep.h */ diff --git a/src/borg/cache_sync/unpack.h b/src/borg/cache_sync/unpack.h new file mode 100644 index 00000000..0d71a940 --- /dev/null +++ b/src/borg/cache_sync/unpack.h @@ -0,0 +1,374 @@ +/* + * Borg cache synchronizer, + * based on a MessagePack for Python unpacking routine + * + * Copyright (C) 2009 Naoki INADA + * Copyright (c) 2017 Marian Beermann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * This limits the depth of the structures we can unpack, i.e. how many containers + * are nestable. + */ +#define MSGPACK_EMBED_STACK_SIZE (16) +#include "unpack_define.h" + +// 2**32 - 1025 +#define _MAX_VALUE ( (uint32_t) 4294966271 ) + +#define MIN(x, y) ((x) < (y) ? (x): (y)) + +#ifdef DEBUG +#define set_last_error(msg) \ + fprintf(stderr, "cache_sync parse error: %s\n", (msg)); \ + u->last_error = (msg); +#else +#define set_last_error(msg) \ + u->last_error = (msg); +#endif + +typedef struct unpack_user { + /* Item.chunks is at the top level; we don't care about anything else, + * only need to track the current level to navigate arbitrary and unknown structure. + * To discern keys from everything else on the top level we use expect_map_item_end. + */ + int level; + + const char *last_error; + + HashIndex *chunks; + + /* + * We don't care about most stuff. This flag tells us whether we're at the chunks structure, + * meaning: + * {'foo': 'bar', 'chunks': [...], 'stuff': ... } + * ^-HERE-^ + */ + int inside_chunks; + enum { + /* the next thing is a map key at the Item root level, + * and it might be the "chunks" key we're looking for */ + expect_chunks_map_key, + + /* blocking state to expect_chunks_map_key + * { 'stuff': , 'chunks': [ + * ecmk -> emie -> -> -> -> ecmk ecb eeboce + * (nested containers are tracked via level) + * ecmk=expect_chunks_map_key, emie=expect_map_item_end, ecb=expect_chunks_begin, + * eeboce=expect_entry_begin_or_chunks_end + */ + expect_map_item_end, + + /* next thing must be the chunks array (array) */ + expect_chunks_begin, + + /* next thing must either be another CLE (array) or end of Item.chunks (array_end) */ + expect_entry_begin_or_chunks_end, + + /* + * processing ChunkListEntry tuple: + * expect_key, expect_size, expect_csize, expect_entry_end + */ + /* next thing must be the key (raw, l=32) */ + expect_key, + /* next thing must be the size (int) */ + expect_size, + /* next thing must be the csize (int) */ + expect_csize, + /* next thing must be the end of the CLE (array_end) */ + expect_entry_end, + } expect; + + struct { + char key[32]; + uint32_t csize; + uint32_t size; + } current; +} unpack_user; + +struct unpack_context; +typedef struct unpack_context unpack_context; +typedef int (*execute_fn)(unpack_context *ctx, const char* data, size_t len, size_t* off); + +#define unexpected(what) \ + if(u->inside_chunks || u->expect == expect_chunks_map_key) { \ + set_last_error("Unexpected object: " what); \ + return -1; \ + } + +static inline int unpack_callback_int64(unpack_user* u, int64_t d) +{ + switch(u->expect) { + case expect_size: + u->current.size = d; + u->expect = expect_csize; + break; + case expect_csize: + u->current.csize = d; + u->expect = expect_entry_end; + break; + default: + unexpected("integer"); + } + return 0; +} + +static inline int unpack_callback_uint16(unpack_user* u, uint16_t d) +{ + return unpack_callback_int64(u, d); +} + +static inline int unpack_callback_uint8(unpack_user* u, uint8_t d) +{ + return unpack_callback_int64(u, d); +} + + +static inline int unpack_callback_uint32(unpack_user* u, uint32_t d) +{ + return unpack_callback_int64(u, d); +} + +static inline int unpack_callback_uint64(unpack_user* u, uint64_t d) +{ + return unpack_callback_int64(u, d); +} + +static inline int unpack_callback_int32(unpack_user* u, int32_t d) +{ + return unpack_callback_int64(u, d); +} + +static inline int unpack_callback_int16(unpack_user* u, int16_t d) +{ + return unpack_callback_int64(u, d); +} + +static inline int unpack_callback_int8(unpack_user* u, int8_t d) +{ + return unpack_callback_int64(u, d); +} + +/* Ain't got anything to do with those floats */ +static inline int unpack_callback_double(unpack_user* u, double d) +{ + (void)d; + unexpected("double"); + return 0; +} + +static inline int unpack_callback_float(unpack_user* u, float d) +{ + (void)d; + unexpected("float"); + return 0; +} + +/* nil/true/false — I/don't/care */ +static inline int unpack_callback_nil(unpack_user* u) +{ + unexpected("nil"); + return 0; +} + +static inline int unpack_callback_true(unpack_user* u) +{ + unexpected("true"); + return 0; +} + +static inline int unpack_callback_false(unpack_user* u) +{ + unexpected("false"); + return 0; +} + +static inline int unpack_callback_array(unpack_user* u, unsigned int n) +{ + switch(u->expect) { + case expect_chunks_begin: + /* b'chunks': [ + * ^ */ + u->expect = expect_entry_begin_or_chunks_end; + break; + case expect_entry_begin_or_chunks_end: + /* b'chunks': [ ( + * ^ */ + if(n != 3) { + set_last_error("Invalid chunk list entry length"); + return -1; + } + u->expect = expect_key; + break; + default: + if(u->inside_chunks) { + set_last_error("Unexpected array start"); + return -1; + } else { + u->level++; + return 0; + } + } + return 0; +} + +static inline int unpack_callback_array_item(unpack_user* u, unsigned int current) +{ + (void)u; (void)current; + return 0; +} + +static inline int unpack_callback_array_end(unpack_user* u) +{ + uint32_t *cache_entry; + uint32_t cache_values[3]; + uint64_t refcount; + + switch(u->expect) { + case expect_entry_end: + /* b'chunks': [ ( b'1234...', 123, 345 ) + * ^ */ + cache_entry = (uint32_t*) hashindex_get(u->chunks, u->current.key); + if (cache_entry) { + refcount = _le32toh(cache_entry[0]); + refcount += 1; + cache_entry[0] = _htole32(MIN(refcount, _MAX_VALUE)); + } else { + /* refcount, size, csize */ + cache_values[0] = _htole32(1); + cache_values[1] = _htole32(u->current.size); + cache_values[2] = _htole32(u->current.csize); + if (!hashindex_set(u->chunks, u->current.key, cache_values)) { + set_last_error("hashindex_set failed"); + return -1; + } + } + + u->expect = expect_entry_begin_or_chunks_end; + break; + case expect_entry_begin_or_chunks_end: + /* b'chunks': [ ] + * ^ */ + /* end of Item.chunks */ + u->inside_chunks = 0; + u->expect = expect_map_item_end; + break; + default: + if(u->inside_chunks) { + set_last_error("Invalid state transition (unexpected array end)"); + return -1; + } else { + u->level--; + return 0; + } + } + + return 0; +} + +static inline int unpack_callback_map(unpack_user* u, unsigned int n) +{ + (void)n; + + + if(u->level == 0) { + /* This begins a new Item */ + u->expect = expect_chunks_map_key; + } + + if(u->inside_chunks) { + unexpected("map"); + } + + u->level++; + + return 0; +} + +static inline int unpack_callback_map_item(unpack_user* u, unsigned int current) +{ + (void)u; (void)current; + if(u->level == 1) { + switch(u->expect) { + case expect_map_item_end: + u->expect = expect_chunks_map_key; + break; + default: + set_last_error("Unexpected map item"); + return -1; + } + } + return 0; +} + +static inline int unpack_callback_map_end(unpack_user* u) +{ + u->level--; + if(u->inside_chunks) { + set_last_error("Unexpected map end"); + return -1; + } + return 0; +} + +static inline int unpack_callback_raw(unpack_user* u, const char* b, const char* p, unsigned int l) +{ + /* raw = what Borg uses for binary stuff and strings as well */ + /* Note: p points to an internal buffer which contains l bytes. */ + (void)b; + + switch(u->expect) { + case expect_key: + if(l != 32) { + set_last_error("Incorrect key length"); + return -1; + } + memcpy(u->current.key, p, 32); + u->expect = expect_size; + break; + case expect_chunks_map_key: + if(l == 6 && !memcmp("chunks", p, 6)) { + u->expect = expect_chunks_begin; + u->inside_chunks = 1; + } else { + u->expect = expect_map_item_end; + } + break; + default: + if(u->inside_chunks) { + set_last_error("Unexpected bytes in chunks structure"); + return -1; + } + } + + return 0; +} + +static inline int unpack_callback_bin(unpack_user* u, const char* b, const char* p, unsigned int l) +{ + (void)u; (void)b; (void)p; (void)l; + unexpected("bin"); + return 0; +} + +static inline int unpack_callback_ext(unpack_user* u, const char* base, const char* pos, + unsigned int length) +{ + (void)u; (void)base; (void)pos; (void)length; + unexpected("ext"); + return 0; +} + +#include "unpack_template.h" diff --git a/src/borg/cache_sync/unpack_define.h b/src/borg/cache_sync/unpack_define.h new file mode 100644 index 00000000..d681277b --- /dev/null +++ b/src/borg/cache_sync/unpack_define.h @@ -0,0 +1,95 @@ +/* + * MessagePack unpacking routine template + * + * Copyright (C) 2008-2010 FURUHASHI Sadayuki + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MSGPACK_UNPACK_DEFINE_H__ +#define MSGPACK_UNPACK_DEFINE_H__ + +#include "sysdep.h" +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + + +#ifndef MSGPACK_EMBED_STACK_SIZE +#define MSGPACK_EMBED_STACK_SIZE 32 +#endif + + +// CS is first byte & 0x1f +typedef enum { + CS_HEADER = 0x00, // nil + + //CS_ = 0x01, + //CS_ = 0x02, // false + //CS_ = 0x03, // true + + CS_BIN_8 = 0x04, + CS_BIN_16 = 0x05, + CS_BIN_32 = 0x06, + + CS_EXT_8 = 0x07, + CS_EXT_16 = 0x08, + CS_EXT_32 = 0x09, + + CS_FLOAT = 0x0a, + CS_DOUBLE = 0x0b, + CS_UINT_8 = 0x0c, + CS_UINT_16 = 0x0d, + CS_UINT_32 = 0x0e, + CS_UINT_64 = 0x0f, + CS_INT_8 = 0x10, + CS_INT_16 = 0x11, + CS_INT_32 = 0x12, + CS_INT_64 = 0x13, + + //CS_FIXEXT1 = 0x14, + //CS_FIXEXT2 = 0x15, + //CS_FIXEXT4 = 0x16, + //CS_FIXEXT8 = 0x17, + //CS_FIXEXT16 = 0x18, + + CS_RAW_8 = 0x19, + CS_RAW_16 = 0x1a, + CS_RAW_32 = 0x1b, + CS_ARRAY_16 = 0x1c, + CS_ARRAY_32 = 0x1d, + CS_MAP_16 = 0x1e, + CS_MAP_32 = 0x1f, + + ACS_RAW_VALUE, + ACS_BIN_VALUE, + ACS_EXT_VALUE, +} msgpack_unpack_state; + + +typedef enum { + CT_ARRAY_ITEM, + CT_MAP_KEY, + CT_MAP_VALUE, +} msgpack_container_type; + + +#ifdef __cplusplus +} +#endif + +#endif /* msgpack/unpack_define.h */ diff --git a/src/borg/cache_sync/unpack_template.h b/src/borg/cache_sync/unpack_template.h new file mode 100644 index 00000000..aa7a4c0b --- /dev/null +++ b/src/borg/cache_sync/unpack_template.h @@ -0,0 +1,359 @@ +/* + * MessagePack unpacking routine template + * + * Copyright (C) 2008-2010 FURUHASHI Sadayuki + * Copyright (c) 2017 Marian Beermann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef USE_CASE_RANGE +#if !defined(_MSC_VER) +#define USE_CASE_RANGE +#endif +#endif + +typedef struct unpack_stack { + size_t size; + size_t count; + unsigned int ct; +} unpack_stack; + +struct unpack_context { + unpack_user user; + unsigned int cs; + unsigned int trail; + unsigned int top; + unpack_stack stack[MSGPACK_EMBED_STACK_SIZE]; +}; + +static inline void unpack_init(unpack_context* ctx) +{ + ctx->cs = CS_HEADER; + ctx->trail = 0; + ctx->top = 0; +} + +#define construct 1 + +static inline int unpack_execute(unpack_context* ctx, const char* data, size_t len, size_t* off) +{ + assert(len >= *off); + + const unsigned char* p = (unsigned char*)data + *off; + const unsigned char* const pe = (unsigned char*)data + len; + const void* n = NULL; + + unsigned int trail = ctx->trail; + unsigned int cs = ctx->cs; + unsigned int top = ctx->top; + unpack_stack* stack = ctx->stack; + unpack_user* user = &ctx->user; + + unpack_stack* c = NULL; + + int ret; + +#define construct_cb(name) \ + construct && unpack_callback ## name + +#define push_simple_value(func) \ + if(construct_cb(func)(user) < 0) { goto _failed; } \ + goto _push +#define push_fixed_value(func, arg) \ + if(construct_cb(func)(user, arg) < 0) { goto _failed; } \ + goto _push +#define push_variable_value(func, base, pos, len) \ + if(construct_cb(func)(user, \ + (const char*)base, (const char*)pos, len) < 0) { goto _failed; } \ + goto _push + +#define again_fixed_trail(_cs, trail_len) \ + trail = trail_len; \ + cs = _cs; \ + goto _fixed_trail_again +#define again_fixed_trail_if_zero(_cs, trail_len, ifzero) \ + trail = trail_len; \ + if(trail == 0) { goto ifzero; } \ + cs = _cs; \ + goto _fixed_trail_again + +#define start_container(func, count_, ct_) \ + if(top >= MSGPACK_EMBED_STACK_SIZE) { goto _failed; } /* FIXME */ \ + if(construct_cb(func)(user, count_) < 0) { goto _failed; } \ + if((count_) == 0) { \ + if (construct_cb(func##_end)(user) < 0) { goto _failed; } \ + goto _push; } \ + stack[top].ct = ct_; \ + stack[top].size = count_; \ + stack[top].count = 0; \ + ++top; \ + goto _header_again + +#define NEXT_CS(p) ((unsigned int)*p & 0x1f) + +#ifdef USE_CASE_RANGE +#define SWITCH_RANGE_BEGIN switch(*p) { +#define SWITCH_RANGE(FROM, TO) case FROM ... TO: +#define SWITCH_RANGE_DEFAULT default: +#define SWITCH_RANGE_END } +#else +#define SWITCH_RANGE_BEGIN { if(0) { +#define SWITCH_RANGE(FROM, TO) } else if(FROM <= *p && *p <= TO) { +#define SWITCH_RANGE_DEFAULT } else { +#define SWITCH_RANGE_END } } +#endif + + if(p == pe) { goto _out; } + do { + switch(cs) { + case CS_HEADER: + SWITCH_RANGE_BEGIN + SWITCH_RANGE(0x00, 0x7f) // Positive Fixnum + push_fixed_value(_uint8, *(uint8_t*)p); + SWITCH_RANGE(0xe0, 0xff) // Negative Fixnum + push_fixed_value(_int8, *(int8_t*)p); + SWITCH_RANGE(0xc0, 0xdf) // Variable + switch(*p) { + case 0xc0: // nil + push_simple_value(_nil); + //case 0xc1: // never used + case 0xc2: // false + push_simple_value(_false); + case 0xc3: // true + push_simple_value(_true); + case 0xc4: // bin 8 + again_fixed_trail(NEXT_CS(p), 1); + case 0xc5: // bin 16 + again_fixed_trail(NEXT_CS(p), 2); + case 0xc6: // bin 32 + again_fixed_trail(NEXT_CS(p), 4); + case 0xc7: // ext 8 + again_fixed_trail(NEXT_CS(p), 1); + case 0xc8: // ext 16 + again_fixed_trail(NEXT_CS(p), 2); + case 0xc9: // ext 32 + again_fixed_trail(NEXT_CS(p), 4); + case 0xca: // float + case 0xcb: // double + case 0xcc: // unsigned int 8 + case 0xcd: // unsigned int 16 + case 0xce: // unsigned int 32 + case 0xcf: // unsigned int 64 + case 0xd0: // signed int 8 + case 0xd1: // signed int 16 + case 0xd2: // signed int 32 + case 0xd3: // signed int 64 + again_fixed_trail(NEXT_CS(p), 1 << (((unsigned int)*p) & 0x03)); + case 0xd4: // fixext 1 + case 0xd5: // fixext 2 + case 0xd6: // fixext 4 + case 0xd7: // fixext 8 + again_fixed_trail_if_zero(ACS_EXT_VALUE, + (1 << (((unsigned int)*p) & 0x03))+1, + _ext_zero); + case 0xd8: // fixext 16 + again_fixed_trail_if_zero(ACS_EXT_VALUE, 16+1, _ext_zero); + case 0xd9: // str 8 + again_fixed_trail(NEXT_CS(p), 1); + case 0xda: // raw 16 + case 0xdb: // raw 32 + case 0xdc: // array 16 + case 0xdd: // array 32 + case 0xde: // map 16 + case 0xdf: // map 32 + again_fixed_trail(NEXT_CS(p), 2 << (((unsigned int)*p) & 0x01)); + default: + goto _failed; + } + SWITCH_RANGE(0xa0, 0xbf) // FixRaw + again_fixed_trail_if_zero(ACS_RAW_VALUE, ((unsigned int)*p & 0x1f), _raw_zero); + SWITCH_RANGE(0x90, 0x9f) // FixArray + start_container(_array, ((unsigned int)*p) & 0x0f, CT_ARRAY_ITEM); + SWITCH_RANGE(0x80, 0x8f) // FixMap + start_container(_map, ((unsigned int)*p) & 0x0f, CT_MAP_KEY); + + SWITCH_RANGE_DEFAULT + goto _failed; + SWITCH_RANGE_END + // end CS_HEADER + + + _fixed_trail_again: + ++p; + + default: + if((size_t)(pe - p) < trail) { goto _out; } + n = p; p += trail - 1; + switch(cs) { + case CS_EXT_8: + again_fixed_trail_if_zero(ACS_EXT_VALUE, *(uint8_t*)n+1, _ext_zero); + case CS_EXT_16: + again_fixed_trail_if_zero(ACS_EXT_VALUE, + _msgpack_load16(uint16_t,n)+1, + _ext_zero); + case CS_EXT_32: + again_fixed_trail_if_zero(ACS_EXT_VALUE, + _msgpack_load32(uint32_t,n)+1, + _ext_zero); + case CS_FLOAT: { + union { uint32_t i; float f; } mem; + mem.i = _msgpack_load32(uint32_t,n); + push_fixed_value(_float, mem.f); } + case CS_DOUBLE: { + union { uint64_t i; double f; } mem; + mem.i = _msgpack_load64(uint64_t,n); +#if defined(__arm__) && !(__ARM_EABI__) // arm-oabi + // https://github.com/msgpack/msgpack-perl/pull/1 + mem.i = (mem.i & 0xFFFFFFFFUL) << 32UL | (mem.i >> 32UL); +#endif + push_fixed_value(_double, mem.f); } + case CS_UINT_8: + push_fixed_value(_uint8, *(uint8_t*)n); + case CS_UINT_16: + push_fixed_value(_uint16, _msgpack_load16(uint16_t,n)); + case CS_UINT_32: + push_fixed_value(_uint32, _msgpack_load32(uint32_t,n)); + case CS_UINT_64: + push_fixed_value(_uint64, _msgpack_load64(uint64_t,n)); + + case CS_INT_8: + push_fixed_value(_int8, *(int8_t*)n); + case CS_INT_16: + push_fixed_value(_int16, _msgpack_load16(int16_t,n)); + case CS_INT_32: + push_fixed_value(_int32, _msgpack_load32(int32_t,n)); + case CS_INT_64: + push_fixed_value(_int64, _msgpack_load64(int64_t,n)); + + case CS_BIN_8: + again_fixed_trail_if_zero(ACS_BIN_VALUE, *(uint8_t*)n, _bin_zero); + case CS_BIN_16: + again_fixed_trail_if_zero(ACS_BIN_VALUE, _msgpack_load16(uint16_t,n), _bin_zero); + case CS_BIN_32: + again_fixed_trail_if_zero(ACS_BIN_VALUE, _msgpack_load32(uint32_t,n), _bin_zero); + case ACS_BIN_VALUE: + _bin_zero: + push_variable_value(_bin, data, n, trail); + + case CS_RAW_8: + again_fixed_trail_if_zero(ACS_RAW_VALUE, *(uint8_t*)n, _raw_zero); + case CS_RAW_16: + again_fixed_trail_if_zero(ACS_RAW_VALUE, _msgpack_load16(uint16_t,n), _raw_zero); + case CS_RAW_32: + again_fixed_trail_if_zero(ACS_RAW_VALUE, _msgpack_load32(uint32_t,n), _raw_zero); + case ACS_RAW_VALUE: + _raw_zero: + push_variable_value(_raw, data, n, trail); + + case ACS_EXT_VALUE: + _ext_zero: + push_variable_value(_ext, data, n, trail); + + case CS_ARRAY_16: + start_container(_array, _msgpack_load16(uint16_t,n), CT_ARRAY_ITEM); + case CS_ARRAY_32: + /* FIXME security guard */ + start_container(_array, _msgpack_load32(uint32_t,n), CT_ARRAY_ITEM); + + case CS_MAP_16: + start_container(_map, _msgpack_load16(uint16_t,n), CT_MAP_KEY); + case CS_MAP_32: + /* FIXME security guard */ + start_container(_map, _msgpack_load32(uint32_t,n), CT_MAP_KEY); + + default: + goto _failed; + } + } + +_push: + if(top == 0) { goto _finish; } + c = &stack[top-1]; + switch(c->ct) { + case CT_ARRAY_ITEM: + if(construct_cb(_array_item)(user, c->count) < 0) { goto _failed; } + if(++c->count == c->size) { + if (construct_cb(_array_end)(user) < 0) { goto _failed; } + --top; + /*printf("stack pop %d\n", top);*/ + goto _push; + } + goto _header_again; + case CT_MAP_KEY: + c->ct = CT_MAP_VALUE; + goto _header_again; + case CT_MAP_VALUE: + if(construct_cb(_map_item)(user, c->count) < 0) { goto _failed; } + if(++c->count == c->size) { + if (construct_cb(_map_end)(user) < 0) { goto _failed; } + --top; + /*printf("stack pop %d\n", top);*/ + goto _push; + } + c->ct = CT_MAP_KEY; + goto _header_again; + + default: + goto _failed; + } + +_header_again: + cs = CS_HEADER; + ++p; + } while(p != pe); + goto _out; + + +_finish: + if (!construct) + unpack_callback_nil(user); + ++p; + ret = 1; + /* printf("-- finish --\n"); */ + goto _end; + +_failed: + /* printf("** FAILED **\n"); */ + ret = -1; + goto _end; + +_out: + ret = 0; + goto _end; + +_end: + ctx->cs = cs; + ctx->trail = trail; + ctx->top = top; + *off = p - (const unsigned char*)data; + + return ret; +#undef construct_cb +} + +#undef SWITCH_RANGE_BEGIN +#undef SWITCH_RANGE +#undef SWITCH_RANGE_DEFAULT +#undef SWITCH_RANGE_END +#undef push_simple_value +#undef push_fixed_value +#undef push_variable_value +#undef again_fixed_trail +#undef again_fixed_trail_if_zero +#undef start_container +#undef construct + +#undef NEXT_CS + +/* vim: set ts=4 sw=4 sts=4 expandtab */ diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 75ba1df3..33868344 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -31,7 +31,7 @@ cdef extern from "_hashindex.c": double HASH_MAX_LOAD -cdef extern from "_cache.c": +cdef extern from "cache_sync/cache_sync.c": ctypedef struct CacheSyncCtx: pass @@ -403,5 +403,5 @@ cdef class CacheSynchronizer: def feed(self, chunk): if not cache_sync_feed(self.sync, chunk, len(chunk)): error = cache_sync_error(self.sync) - if error is not None: - raise Exception('cache_sync_feed failed: ' + error.decode('ascii')) + if error != NULL: + raise ValueError('cache_sync_feed failed: ' + error.decode('ascii')) diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py new file mode 100644 index 00000000..690e50e3 --- /dev/null +++ b/src/borg/testsuite/cache.py @@ -0,0 +1,139 @@ + +from msgpack import packb + +import pytest + +from ..hashindex import ChunkIndex, CacheSynchronizer +from .hashindex import H + + +class TestCacheSynchronizer: + @pytest.fixture + def index(self): + return ChunkIndex() + + @pytest.fixture + def sync(self, index): + return CacheSynchronizer(index) + + def test_no_chunks(self, index, sync): + data = packb({ + 'foo': 'bar', + 'baz': 1234, + 'bar': 5678, + 'user': 'chunks', + 'chunks': [] + }) + sync.feed(data) + assert not len(index) + + def test_simple(self, index, sync): + data = packb({ + 'foo': 'bar', + 'baz': 1234, + 'bar': 5678, + 'user': 'chunks', + 'chunks': [ + (H(1), 1, 2), + (H(2), 2, 3), + ] + }) + sync.feed(data) + assert len(index) == 2 + assert index[H(1)] == (1, 1, 2) + assert index[H(2)] == (1, 2, 3) + + def test_multiple(self, index, sync): + data = packb({ + 'foo': 'bar', + 'baz': 1234, + 'bar': 5678, + 'user': 'chunks', + 'chunks': [ + (H(1), 1, 2), + (H(2), 2, 3), + ] + }) + data += packb({ + 'xattrs': { + 'security.foo': 'bar', + 'chunks': '123456', + }, + 'stuff': [ + (1, 2, 3), + ] + }) + data += packb({ + 'xattrs': { + 'security.foo': 'bar', + 'chunks': '123456', + }, + 'chunks': [ + (H(1), 1, 2), + (H(2), 2, 3), + ], + 'stuff': [ + (1, 2, 3), + ] + }) + data += packb({ + 'chunks': [ + (H(3), 1, 2), + ], + }) + data += packb({ + 'chunks': [ + (H(1), 1, 2), + ], + }) + + part1 = data[:70] + part2 = data[70:120] + part3 = data[120:] + sync.feed(part1) + sync.feed(part2) + sync.feed(part3) + assert len(index) == 3 + assert index[H(1)] == (3, 1, 2) + assert index[H(2)] == (2, 2, 3) + assert index[H(3)] == (1, 1, 2) + + @pytest.mark.parametrize('elem,error', ( + ({1: 2}, 'Unexpected object: map'), + (bytes(213), [ + 'Unexpected bytes in chunks structure', # structure 2/3 + 'Incorrect key length']), # structure 3/3 + (1, 'Unexpected object: integer'), + (1.0, 'Unexpected object: double'), + (True, 'Unexpected object: true'), + (False, 'Unexpected object: false'), + (None, 'Unexpected object: nil'), + )) + @pytest.mark.parametrize('structure', ( + lambda elem: {'chunks': elem}, + lambda elem: {'chunks': [elem]}, + lambda elem: {'chunks': [(elem, 1, 2)]}, + )) + def test_corrupted(self, sync, structure, elem, error): + packed = packb(structure(elem)) + with pytest.raises(ValueError) as excinfo: + sync.feed(packed) + if isinstance(error, str): + error = [error] + possible_errors = ['cache_sync_feed failed: ' + error for error in error] + assert str(excinfo.value) in possible_errors + + @pytest.mark.parametrize('data,error', ( + # Incorrect tuple length + ({'chunks': [(bytes(32), 2, 3, 4)]}, 'Invalid chunk list entry length'), + ({'chunks': [(bytes(32), 2)]}, 'Invalid chunk list entry length'), + # Incorrect types + ({'chunks': [(1, 2, 3)]}, 'Unexpected object: integer'), + ({'chunks': [(1, bytes(32), 2)]}, 'Unexpected object: integer'), + ({'chunks': [(bytes(32), 1.0, 2)]}, 'Unexpected object: double'), + )) + def test_corrupted_ancillary(self, index, sync, data, error): + packed = packb(data) + with pytest.raises(ValueError) as excinfo: + sync.feed(packed) + assert str(excinfo.value) == 'cache_sync_feed failed: ' + error From 835b0e5ee057e07f416817238039b7d29c4de62d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 28 May 2017 13:16:52 +0200 Subject: [PATCH 0937/1387] cache sync/remote: compressed, decrypted cache --- src/borg/cache.py | 18 +++++------ src/borg/remote.py | 81 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 70 insertions(+), 29 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 934f12a7..9fb0c540 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -564,17 +564,15 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" except FileNotFoundError: pass - def fetch_and_build_idx(archive_id, repository, key, chunk_idx): - cdata = repository.get(archive_id) - data = key.decrypt(archive_id, cdata) - chunk_idx.add(archive_id, 1, len(data), len(cdata)) + def fetch_and_build_idx(archive_id, decrypted_repository, key, chunk_idx): + csize, data = decrypted_repository.get(archive_id) + chunk_idx.add(archive_id, 1, len(data), csize) archive = ArchiveItem(internal_dict=msgpack.unpackb(data)) if archive.version != 1: raise Exception('Unknown archive metadata version') sync = CacheSynchronizer(chunk_idx) - for item_id, chunk in zip(archive.items, repository.get_many(archive.items)): - data = key.decrypt(item_id, chunk) - chunk_idx.add(item_id, 1, len(data), len(chunk)) + for item_id, (csize, data) in zip(archive.items, decrypted_repository.get_many(archive.items)): + chunk_idx.add(item_id, 1, len(data), csize) sync.feed(data) if self.do_cache: fn = mkpath(archive_id) @@ -641,7 +639,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" # above can remove *archive_id* from *cached_ids*. logger.info('Fetching and building archive index for %s ...', archive_name) archive_chunk_idx = ChunkIndex() - fetch_and_build_idx(archive_id, repository, self.key, archive_chunk_idx) + fetch_and_build_idx(archive_id, decrypted_repository, self.key, archive_chunk_idx) logger.info("Merging into master chunks index ...") if chunk_idx is None: # we just use the first archive's idx as starting point, @@ -653,7 +651,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" else: chunk_idx = chunk_idx or ChunkIndex(master_index_capacity) logger.info('Fetching archive index for %s ...', archive_name) - fetch_and_build_idx(archive_id, repository, self.key, chunk_idx) + fetch_and_build_idx(archive_id, decrypted_repository, self.key, chunk_idx) if self.progress: pi.finish() logger.info('Done.') @@ -675,7 +673,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" pass self.begin_txn() - with cache_if_remote(self.repository) as repository: + with cache_if_remote(self.repository, decrypted_cache=self.key) as decrypted_repository: legacy_cleanup() # TEMPORARY HACK: to avoid archive index caching, create a FILE named ~/.cache/borg/REPOID/chunks.archive.d - # this is only recommended if you have a fast, low latency connection to your repo (e.g. if repo is local disk) diff --git a/src/borg/remote.py b/src/borg/remote.py index c991dbef..daad2c25 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -8,6 +8,7 @@ import os import select import shlex import shutil +import struct import sys import tempfile import textwrap @@ -18,6 +19,7 @@ from subprocess import Popen, PIPE import msgpack from . import __version__ +from .compress import LZ4 from .helpers import Error, IntegrityError from .helpers import bin_to_hex from .helpers import get_home_dir @@ -1046,9 +1048,14 @@ class RepositoryNoCache: """A not caching Repository wrapper, passes through to repository. Just to have same API (including the context manager) as RepositoryCache. + + *transform* is a callable taking two arguments, key and raw repository data. + The return value is returned from get()/get_many(). By default, the raw + repository data is returned. """ - def __init__(self, repository): + def __init__(self, repository, transform=None): self.repository = repository + self.transform = transform or (lambda key, data: data) def close(self): pass @@ -1063,8 +1070,8 @@ class RepositoryNoCache: return next(self.get_many([key], cache=False)) def get_many(self, keys, cache=True): - for data in self.repository.get_many(keys): - yield data + for key, data in zip(keys, self.repository.get_many(keys)): + yield self.transform(key, data) class RepositoryCache(RepositoryNoCache): @@ -1072,10 +1079,17 @@ class RepositoryCache(RepositoryNoCache): A caching Repository wrapper. Caches Repository GET operations locally. + + *pack* and *unpack* complement *transform* of the base class. + *pack* receives the output of *transform* and should return bytes, + which are stored in the cache. *unpack* receives these bytes and + should return the initial data (as returned by *transform*). """ - def __init__(self, repository): - super().__init__(repository) + def __init__(self, repository, pack=None, unpack=None, transform=None): + super().__init__(repository, transform) + self.pack = pack or (lambda data: data) + self.unpack = unpack or (lambda data: data) self.cache = set() self.basedir = tempfile.mkdtemp(prefix='borg-cache-') self.query_size_limit() @@ -1106,11 +1120,15 @@ class RepositoryCache(RepositoryNoCache): os.unlink(file) self.evictions += 1 - def add_entry(self, key, data): + def add_entry(self, key, data, cache): + transformed = self.transform(key, data) + if not cache: + return transformed + packed = self.pack(transformed) file = self.key_filename(key) try: with open(file, 'wb') as fd: - fd.write(data) + fd.write(packed) except OSError as os_error: if os_error.errno == errno.ENOSPC: self.enospc += 1 @@ -1118,11 +1136,11 @@ class RepositoryCache(RepositoryNoCache): else: raise else: - self.size += len(data) + self.size += len(packed) self.cache.add(key) if self.size > self.size_limit: self.backoff() - return data + return transformed def close(self): logger.debug('RepositoryCache: current items %d, size %s / %s, %d hits, %d misses, %d slow misses (+%.1fs), ' @@ -1141,31 +1159,56 @@ class RepositoryCache(RepositoryNoCache): file = self.key_filename(key) with open(file, 'rb') as fd: self.hits += 1 - yield fd.read() + yield self.unpack(fd.read()) else: for key_, data in repository_iterator: if key_ == key: - if cache: - self.add_entry(key, data) + transformed = self.add_entry(key, data, cache) self.misses += 1 - yield data + yield transformed break else: # slow path: eviction during this get_many removed this key from the cache t0 = time.perf_counter() data = self.repository.get(key) self.slow_lat += time.perf_counter() - t0 - if cache: - self.add_entry(key, data) + transformed = self.add_entry(key, data, cache) self.slow_misses += 1 - yield data + yield transformed # Consume any pending requests for _ in repository_iterator: pass -def cache_if_remote(repository): +def cache_if_remote(repository, *, decrypted_cache=False, pack=None, unpack=None, transform=None): + """ + Return a Repository(No)Cache for *repository*. + + If *decrypted_cache* is a key object, then get and get_many will return a tuple + (csize, plaintext) instead of the actual data in the repository. The cache will + store decrypted data, which increases CPU efficiency (by avoiding repeatedly decrypting + and more importantly MAC and ID checking cached objects). + Internally, objects are compressed with LZ4. + """ + if decrypted_cache and (pack or unpack or transform): + raise ValueError('decrypted_cache and pack/unpack/transform are incompatible') + elif decrypted_cache: + key = decrypted_cache + cache_struct = struct.Struct('=I') + compressor = LZ4() + + def pack(data): + return cache_struct.pack(data[0]) + compressor.compress(data[1]) + + def unpack(data): + return cache_struct.unpack(data[:cache_struct.size])[0], compressor.decompress(data[cache_struct.size:]) + + def transform(id_, data): + csize = len(data) + decrypted = key.decrypt(id_, data) + return csize, decrypted + if isinstance(repository, RemoteRepository): - return RepositoryCache(repository) + return RepositoryCache(repository, pack, unpack, transform) else: - return RepositoryNoCache(repository) + return RepositoryNoCache(repository, transform) From 7f04e00ba23157dee8910cc35362098dc98e63df Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 28 May 2017 15:33:36 +0200 Subject: [PATCH 0938/1387] testsuite: add TestRepositoryCache --- src/borg/testsuite/remote.py | 91 +++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/src/borg/testsuite/remote.py b/src/borg/testsuite/remote.py index b9eddabd..419463be 100644 --- a/src/borg/testsuite/remote.py +++ b/src/borg/testsuite/remote.py @@ -1,9 +1,13 @@ +import errno import os import time +from unittest.mock import patch import pytest -from ..remote import SleepingBandwidthLimiter +from ..remote import SleepingBandwidthLimiter, RepositoryCache +from ..repository import Repository +from .hashindex import H class TestSleepingBandwidthLimiter: @@ -58,3 +62,88 @@ class TestSleepingBandwidthLimiter: now += 10 self.expect_write(5, b"1") it.write(5, b"1") + + +class TestRepositoryCache: + @pytest.yield_fixture + def repository(self, tmpdir): + self.repository_location = os.path.join(str(tmpdir), 'repository') + with Repository(self.repository_location, exclusive=True, create=True) as repository: + repository.put(H(1), b'1234') + repository.put(H(2), b'5678') + repository.put(H(3), bytes(100)) + yield repository + + @pytest.fixture + def cache(self, repository): + return RepositoryCache(repository) + + def test_simple(self, cache: RepositoryCache): + # Single get()s are not cached, since they are used for unique objects like archives. + assert cache.get(H(1)) == b'1234' + assert cache.misses == 1 + assert cache.hits == 0 + + assert list(cache.get_many([H(1)])) == [b'1234'] + assert cache.misses == 2 + assert cache.hits == 0 + + assert list(cache.get_many([H(1)])) == [b'1234'] + assert cache.misses == 2 + assert cache.hits == 1 + + assert cache.get(H(1)) == b'1234' + assert cache.misses == 2 + assert cache.hits == 2 + + def test_backoff(self, cache: RepositoryCache): + def query_size_limit(): + cache.size_limit = 0 + + assert list(cache.get_many([H(1), H(2)])) == [b'1234', b'5678'] + assert cache.misses == 2 + assert cache.evictions == 0 + iterator = cache.get_many([H(1), H(3), H(2)]) + assert next(iterator) == b'1234' + + # Force cache to back off + qsl = cache.query_size_limit + cache.query_size_limit = query_size_limit + cache.backoff() + cache.query_size_limit = qsl + # Evicted H(1) and H(2) + assert cache.evictions == 2 + assert H(1) not in cache.cache + assert H(2) not in cache.cache + assert next(iterator) == bytes(100) + assert cache.slow_misses == 0 + # Since H(2) was in the cache when we called get_many(), but has + # been evicted during iterating the generator, it will be a slow miss. + assert next(iterator) == b'5678' + assert cache.slow_misses == 1 + + def test_enospc(self, cache: RepositoryCache): + class enospc_open: + def __init__(self, *args): + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def write(self, data): + raise OSError(errno.ENOSPC, 'foo') + + iterator = cache.get_many([H(1), H(2), H(3)]) + assert next(iterator) == b'1234' + + with patch('builtins.open', enospc_open): + assert next(iterator) == b'5678' + assert cache.enospc == 1 + # We didn't patch query_size_limit which would set size_limit to some low + # value, so nothing was actually evicted. + assert cache.evictions == 0 + + assert next(iterator) == bytes(100) From 67b97f22231638f1a0276c54b14fab0319742c54 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 31 May 2017 20:46:57 +0200 Subject: [PATCH 0939/1387] cache sync: cleanup progress handling, unused parameters --- src/borg/cache.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 9fb0c540..7a9fce02 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -564,7 +564,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" except FileNotFoundError: pass - def fetch_and_build_idx(archive_id, decrypted_repository, key, chunk_idx): + def fetch_and_build_idx(archive_id, decrypted_repository, chunk_idx): csize, data = decrypted_repository.get(archive_id) chunk_idx.add(archive_id, 1, len(data), csize) archive = ArchiveItem(internal_dict=msgpack.unpackb(data)) @@ -595,6 +595,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" for info in self.manifest.archives.list(): if info.id in archive_ids: archive_names[info.id] = info.name + assert len(archive_names) == len(archive_ids) return archive_names def create_master_idx(chunk_idx): @@ -612,15 +613,12 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" master_index_capacity = int(len(self.repository) / ChunkIndex.MAX_LOAD_FACTOR) if archive_ids: chunk_idx = None - if self.progress: - pi = ProgressIndicatorPercent(total=len(archive_ids), step=0.1, - msg='%3.0f%% Syncing chunks cache. Processing archive %s', - msgid='cache.sync') + pi = ProgressIndicatorPercent(total=len(archive_ids), step=0.1, + msg='%3.0f%% Syncing chunks cache. Processing archive %s', + msgid='cache.sync') archive_ids_to_names = get_archive_ids_to_names(archive_ids) - for archive_id in archive_ids: - archive_name = archive_ids_to_names.pop(archive_id) - if self.progress: - pi.show(info=[remove_surrogates(archive_name)]) + for archive_id, archive_name in archive_ids_to_names.items(): + pi.show(info=[remove_surrogates(archive_name)]) if self.do_cache: if archive_id in cached_ids: archive_chunk_idx_path = mkpath(archive_id) @@ -639,7 +637,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" # above can remove *archive_id* from *cached_ids*. logger.info('Fetching and building archive index for %s ...', archive_name) archive_chunk_idx = ChunkIndex() - fetch_and_build_idx(archive_id, decrypted_repository, self.key, archive_chunk_idx) + fetch_and_build_idx(archive_id, decrypted_repository, archive_chunk_idx) logger.info("Merging into master chunks index ...") if chunk_idx is None: # we just use the first archive's idx as starting point, @@ -651,9 +649,8 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" else: chunk_idx = chunk_idx or ChunkIndex(master_index_capacity) logger.info('Fetching archive index for %s ...', archive_name) - fetch_and_build_idx(archive_id, decrypted_repository, self.key, chunk_idx) - if self.progress: - pi.finish() + fetch_and_build_idx(archive_id, decrypted_repository, chunk_idx) + pi.finish() logger.info('Done.') return chunk_idx From cb98cb838d2b73387fa4a311734a2b3c842f6e17 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 31 May 2017 20:47:16 +0200 Subject: [PATCH 0940/1387] fuse: fix read(2) caching data in metadata cache The OS page cache is responsible for handling this and is much more empowered to do a good job at that than Borg. --- src/borg/fuse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 28e09689..4441c406 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -340,7 +340,7 @@ class FuseOperations(llfuse.Operations): # evict fully read chunk from cache del self.data_cache[id] else: - data = self.key.decrypt(id, self.repository.get(id)) + data = self.key.decrypt(id, self.repository_uncached.get(id)) if offset + n < len(data): # chunk was only partially read, cache it self.data_cache[id] = data From 795cdfc9abadb7ec4a93e8a01dae7bec1832dcc7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Jun 2017 19:28:50 +0200 Subject: [PATCH 0941/1387] cache sync: move stat initialization to main unpack --- src/borg/cache_sync/cache_sync.c | 5 +---- src/borg/cache_sync/unpack.h | 15 ++++++++++++++- src/borg/cache_sync/unpack_template.h | 1 + 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/borg/cache_sync/cache_sync.c b/src/borg/cache_sync/cache_sync.c index 174e4b90..e0c2e0fb 100644 --- a/src/borg/cache_sync/cache_sync.c +++ b/src/borg/cache_sync/cache_sync.c @@ -19,11 +19,8 @@ cache_sync_init(HashIndex *chunks) } unpack_init(&ctx->ctx); - + /* needs to be set only once */ ctx->ctx.user.chunks = chunks; - ctx->ctx.user.last_error = NULL; - ctx->ctx.user.level = 0; - ctx->ctx.user.inside_chunks = false; ctx->buf = NULL; ctx->head = 0; ctx->tail = 0; diff --git a/src/borg/cache_sync/unpack.h b/src/borg/cache_sync/unpack.h index 0d71a940..62e775f4 100644 --- a/src/borg/cache_sync/unpack.h +++ b/src/borg/cache_sync/unpack.h @@ -89,6 +89,8 @@ typedef struct unpack_user { expect_csize, /* next thing must be the end of the CLE (array_end) */ expect_entry_end, + + expect_item_begin } expect; struct { @@ -108,6 +110,14 @@ typedef int (*execute_fn)(unpack_context *ctx, const char* data, size_t len, siz return -1; \ } +static inline void unpack_init_user_state(unpack_user *u) +{ + u->last_error = NULL; + u->level = 0; + u->inside_chunks = false; + u->expect = expect_item_begin; +} + static inline int unpack_callback_int64(unpack_user* u, int64_t d) { switch(u->expect) { @@ -282,8 +292,11 @@ static inline int unpack_callback_map(unpack_user* u, unsigned int n) { (void)n; - if(u->level == 0) { + if(u->expect != expect_item_begin) { + set_last_error("Invalid state transition"); /* unreachable */ + return -1; + } /* This begins a new Item */ u->expect = expect_chunks_map_key; } diff --git a/src/borg/cache_sync/unpack_template.h b/src/borg/cache_sync/unpack_template.h index aa7a4c0b..1ac26274 100644 --- a/src/borg/cache_sync/unpack_template.h +++ b/src/borg/cache_sync/unpack_template.h @@ -42,6 +42,7 @@ static inline void unpack_init(unpack_context* ctx) ctx->cs = CS_HEADER; ctx->trail = 0; ctx->top = 0; + unpack_init_user_state(&ctx->user); } #define construct 1 From 5b3667b61760696f5cca985dc556bbb551997aeb Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Jun 2017 19:31:56 +0200 Subject: [PATCH 0942/1387] cache sync: macros in all-caps --- src/borg/cache_sync/unpack.h | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/borg/cache_sync/unpack.h b/src/borg/cache_sync/unpack.h index 62e775f4..a48559ff 100644 --- a/src/borg/cache_sync/unpack.h +++ b/src/borg/cache_sync/unpack.h @@ -31,11 +31,11 @@ #define MIN(x, y) ((x) < (y) ? (x): (y)) #ifdef DEBUG -#define set_last_error(msg) \ +#define SET_LAST_ERROR(msg) \ fprintf(stderr, "cache_sync parse error: %s\n", (msg)); \ u->last_error = (msg); #else -#define set_last_error(msg) \ +#define SET_LAST_ERROR(msg) \ u->last_error = (msg); #endif @@ -104,9 +104,9 @@ struct unpack_context; typedef struct unpack_context unpack_context; typedef int (*execute_fn)(unpack_context *ctx, const char* data, size_t len, size_t* off); -#define unexpected(what) \ +#define UNEXPECTED(what) \ if(u->inside_chunks || u->expect == expect_chunks_map_key) { \ - set_last_error("Unexpected object: " what); \ + SET_LAST_ERROR("Unexpected object: " what); \ return -1; \ } @@ -130,7 +130,7 @@ static inline int unpack_callback_int64(unpack_user* u, int64_t d) u->expect = expect_entry_end; break; default: - unexpected("integer"); + UNEXPECTED("integer"); } return 0; } @@ -175,33 +175,33 @@ static inline int unpack_callback_int8(unpack_user* u, int8_t d) static inline int unpack_callback_double(unpack_user* u, double d) { (void)d; - unexpected("double"); + UNEXPECTED("double"); return 0; } static inline int unpack_callback_float(unpack_user* u, float d) { (void)d; - unexpected("float"); + UNEXPECTED("float"); return 0; } /* nil/true/false — I/don't/care */ static inline int unpack_callback_nil(unpack_user* u) { - unexpected("nil"); + UNEXPECTED("nil"); return 0; } static inline int unpack_callback_true(unpack_user* u) { - unexpected("true"); + UNEXPECTED("true"); return 0; } static inline int unpack_callback_false(unpack_user* u) { - unexpected("false"); + UNEXPECTED("false"); return 0; } @@ -217,14 +217,14 @@ static inline int unpack_callback_array(unpack_user* u, unsigned int n) /* b'chunks': [ ( * ^ */ if(n != 3) { - set_last_error("Invalid chunk list entry length"); + SET_LAST_ERROR("Invalid chunk list entry length"); return -1; } u->expect = expect_key; break; default: if(u->inside_chunks) { - set_last_error("Unexpected array start"); + SET_LAST_ERROR("Unexpected array start"); return -1; } else { u->level++; @@ -261,7 +261,7 @@ static inline int unpack_callback_array_end(unpack_user* u) cache_values[1] = _htole32(u->current.size); cache_values[2] = _htole32(u->current.csize); if (!hashindex_set(u->chunks, u->current.key, cache_values)) { - set_last_error("hashindex_set failed"); + SET_LAST_ERROR("hashindex_set failed"); return -1; } } @@ -277,7 +277,7 @@ static inline int unpack_callback_array_end(unpack_user* u) break; default: if(u->inside_chunks) { - set_last_error("Invalid state transition (unexpected array end)"); + SET_LAST_ERROR("Invalid state transition (unexpected array end)"); return -1; } else { u->level--; @@ -294,7 +294,7 @@ static inline int unpack_callback_map(unpack_user* u, unsigned int n) if(u->level == 0) { if(u->expect != expect_item_begin) { - set_last_error("Invalid state transition"); /* unreachable */ + SET_LAST_ERROR("Invalid state transition"); /* unreachable */ return -1; } /* This begins a new Item */ @@ -302,7 +302,7 @@ static inline int unpack_callback_map(unpack_user* u, unsigned int n) } if(u->inside_chunks) { - unexpected("map"); + UNEXPECTED("map"); } u->level++; @@ -319,7 +319,7 @@ static inline int unpack_callback_map_item(unpack_user* u, unsigned int current) u->expect = expect_chunks_map_key; break; default: - set_last_error("Unexpected map item"); + SET_LAST_ERROR("Unexpected map item"); return -1; } } @@ -330,7 +330,7 @@ static inline int unpack_callback_map_end(unpack_user* u) { u->level--; if(u->inside_chunks) { - set_last_error("Unexpected map end"); + SET_LAST_ERROR("Unexpected map end"); return -1; } return 0; @@ -345,7 +345,7 @@ static inline int unpack_callback_raw(unpack_user* u, const char* b, const char* switch(u->expect) { case expect_key: if(l != 32) { - set_last_error("Incorrect key length"); + SET_LAST_ERROR("Incorrect key length"); return -1; } memcpy(u->current.key, p, 32); @@ -361,7 +361,7 @@ static inline int unpack_callback_raw(unpack_user* u, const char* b, const char* break; default: if(u->inside_chunks) { - set_last_error("Unexpected bytes in chunks structure"); + SET_LAST_ERROR("Unexpected bytes in chunks structure"); return -1; } } @@ -372,7 +372,7 @@ static inline int unpack_callback_raw(unpack_user* u, const char* b, const char* static inline int unpack_callback_bin(unpack_user* u, const char* b, const char* p, unsigned int l) { (void)u; (void)b; (void)p; (void)l; - unexpected("bin"); + UNEXPECTED("bin"); return 0; } @@ -380,7 +380,7 @@ static inline int unpack_callback_ext(unpack_user* u, const char* base, const ch unsigned int length) { (void)u; (void)base; (void)pos; (void)length; - unexpected("ext"); + UNEXPECTED("ext"); return 0; } From f61ee038d08b735c05fde1b851823f853ed1cfae Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 1 Jun 2017 23:28:11 +0200 Subject: [PATCH 0943/1387] repository: checksum index and hints --- src/borg/repository.py | 100 ++++++++++++++++++++++++------- src/borg/testsuite/repository.py | 11 ++++ 2 files changed, 88 insertions(+), 23 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index a4fbb7cc..43183223 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -24,6 +24,7 @@ from .logger import create_logger from .lrucache import LRUCache from .platform import SaveFile, SyncFile, sync_dir, safe_fadvise from .algorithms.checksums import crc32 +from .crypto.file_integrity import IntegrityCheckedFile, FileIntegrityError logger = create_logger(__name__) @@ -372,13 +373,27 @@ class Repository: self.write_index() self.rollback() + def _read_integrity(self, transaction_id, key=None): + integrity_path = os.path.join(self.path, 'integrity.%d' % transaction_id) + try: + with open(integrity_path, 'rb') as fd: + integrity = msgpack.unpack(fd) + except FileNotFoundError: + return + if key: + return integrity[key].decode() + else: + return integrity + def open_index(self, transaction_id, auto_recover=True): if transaction_id is None: return NSIndex() - index_path = os.path.join(self.path, 'index.%d' % transaction_id).encode('utf-8') + index_path = os.path.join(self.path, 'index.%d' % transaction_id) + integrity_data = self._read_integrity(transaction_id, b'index') try: - return NSIndex.read(index_path) - except (ValueError, OSError) as exc: + with IntegrityCheckedFile(index_path, write=False, integrity_data=integrity_data) as fd: + return NSIndex.read(fd) + except (ValueError, OSError, FileIntegrityError) as exc: logger.warning('Repository index missing or corrupted, trying to recover from: %s', exc) os.unlink(index_path) if not auto_recover: @@ -409,11 +424,11 @@ class Repository: raise if not self.index or transaction_id is None: try: - self.index = self.open_index(transaction_id, False) - except (ValueError, OSError) as exc: + self.index = self.open_index(transaction_id, auto_recover=False) + except (ValueError, OSError, FileIntegrityError) as exc: logger.warning('Checking repository transaction due to previous error: %s', exc) self.check_transaction() - self.index = self.open_index(transaction_id, False) + self.index = self.open_index(transaction_id, auto_recover=False) if transaction_id is None: self.segments = {} # XXX bad name: usage_count_of_segment_x = self.segments[x] self.compact = FreeSpace() # XXX bad name: freeable_space_of_segment_x = self.compact[x] @@ -424,11 +439,12 @@ class Repository: self.io.cleanup(transaction_id) hints_path = os.path.join(self.path, 'hints.%d' % transaction_id) index_path = os.path.join(self.path, 'index.%d' % transaction_id) + integrity_data = self._read_integrity(transaction_id, b'hints') try: - with open(hints_path, 'rb') as fd: + with IntegrityCheckedFile(hints_path, write=False, integrity_data=integrity_data) as fd: hints = msgpack.unpack(fd) - except (msgpack.UnpackException, msgpack.ExtraData, FileNotFoundError) as e: - logger.warning('Repository hints file missing or corrupted, trying to recover') + except (msgpack.UnpackException, msgpack.ExtraData, FileNotFoundError, FileIntegrityError) as e: + logger.warning('Repository hints file missing or corrupted, trying to recover: %s', e) if not isinstance(e, FileNotFoundError): os.unlink(hints_path) # index must exist at this point @@ -459,28 +475,66 @@ class Repository: shadowed_segments.remove(segment) def write_index(self): - hints = {b'version': 2, - b'segments': self.segments, - b'compact': self.compact, - b'storage_quota_use': self.storage_quota_use, } - transaction_id = self.io.get_segments_transaction_id() - assert transaction_id is not None - hints_file = os.path.join(self.path, 'hints.%d' % transaction_id) - with open(hints_file + '.tmp', 'wb') as fd: - msgpack.pack(hints, fd) + def flush_and_sync(fd): fd.flush() os.fsync(fd.fileno()) - os.rename(hints_file + '.tmp', hints_file) - self.index.write(os.path.join(self.path, 'index.tmp')) - os.rename(os.path.join(self.path, 'index.tmp'), - os.path.join(self.path, 'index.%d' % transaction_id)) + + def rename_tmp(file): + os.rename(file + '.tmp', file) + + hints = { + b'version': 2, + b'segments': self.segments, + b'compact': self.compact, + b'storage_quota_use': self.storage_quota_use, + } + integrity = { + b'version': 2, + } + transaction_id = self.io.get_segments_transaction_id() + assert transaction_id is not None + + # Log transaction in append-only mode if self.append_only: with open(os.path.join(self.path, 'transactions'), 'a') as log: print('transaction %d, UTC time %s' % (transaction_id, datetime.utcnow().isoformat()), file=log) + + # Write hints file + hints_name = 'hints.%d' % transaction_id + hints_file = os.path.join(self.path, hints_name) + with IntegrityCheckedFile(hints_file + '.tmp', filename=hints_name, write=True) as fd: + msgpack.pack(hints, fd) + flush_and_sync(fd) + integrity[b'hints'] = fd.integrity_data + + # Write repository index + index_name = 'index.%d' % transaction_id + index_file = os.path.join(self.path, index_name) + with IntegrityCheckedFile(index_file + '.tmp', filename=index_name, write=True) as fd: + # XXX: Consider using SyncFile for index write-outs. + self.index.write(fd) + flush_and_sync(fd) + integrity[b'index'] = fd.integrity_data + + # Write integrity file, containing checksums of the hints and index files + integrity_name = 'integrity.%d' % transaction_id + integrity_file = os.path.join(self.path, integrity_name) + with open(integrity_file + '.tmp', 'wb') as fd: + msgpack.pack(integrity, fd) + flush_and_sync(fd) + + # Rename the integrity file first + rename_tmp(integrity_file) + sync_dir(self.path) + # Rename the others after the integrity file is hypothetically on disk + rename_tmp(hints_file) + rename_tmp(index_file) + sync_dir(self.path) + # Remove old auxiliary files current = '.%d' % transaction_id for name in os.listdir(self.path): - if not name.startswith(('index.', 'hints.')): + if not name.startswith(('index.', 'hints.', 'integrity.')): continue if name.endswith(current): continue diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index f9c270ec..f507504e 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -501,6 +501,11 @@ class RepositoryAuxiliaryCorruptionTestCase(RepositoryTestCaseBase): self.repository.commit() self.repository.close() + def corrupt(self, file): + with open(file, 'r+b') as fd: + fd.seek(-1, io.SEEK_END) + fd.write(b'1') + def do_commit(self): with self.repository: self.repository.put(H(0), b'fox') @@ -537,6 +542,12 @@ class RepositoryAuxiliaryCorruptionTestCase(RepositoryTestCaseBase): with self.repository: assert len(self.repository) == 1 + def test_index_corrupted(self): + self.corrupt(os.path.join(self.repository.path, 'index.1')) + with self.repository: + assert len(self.repository) == 1 + assert self.repository.get(H(0)) == b'foo' + def test_unreadable_index(self): index = os.path.join(self.repository.path, 'index.1') os.unlink(index) From 2e067a7ae8120e822a97561f02b1db1228ad62ed Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Jun 2017 11:36:58 +0200 Subject: [PATCH 0944/1387] repository: add refcount corruption test --- src/borg/repository.py | 17 ++++----- src/borg/testsuite/repository.py | 59 ++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 43183223..a3460031 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -373,17 +373,18 @@ class Repository: self.write_index() self.rollback() - def _read_integrity(self, transaction_id, key=None): - integrity_path = os.path.join(self.path, 'integrity.%d' % transaction_id) + def _read_integrity(self, transaction_id, key): + integrity_file = 'integrity.%d' % transaction_id + integrity_path = os.path.join(self.path, integrity_file) try: with open(integrity_path, 'rb') as fd: integrity = msgpack.unpack(fd) except FileNotFoundError: return - if key: - return integrity[key].decode() - else: - return integrity + if integrity.get(b'version') != 2: + logger.warning('Unknown integrity data version %r in %s', integrity.get(b'version'), integrity_file) + return + return integrity[key].decode() def open_index(self, transaction_id, auto_recover=True): if transaction_id is None: @@ -617,7 +618,7 @@ class Repository: # get rid of the old, sparse, unused segments. free space. for segment in unused: logger.debug('complete_xfer: deleting unused segment %d', segment) - assert self.segments.pop(segment) == 0 + assert self.segments.pop(segment) == 0, 'Corrupted segment reference count - corrupted index or hints' self.io.delete_segment(segment) del self.compact[segment] unused = [] @@ -711,7 +712,7 @@ class Repository: new_segment, size = self.io.write_delete(key) self.compact[new_segment] += size segments.setdefault(new_segment, 0) - assert segments[segment] == 0 + assert segments[segment] == 0, 'Corrupted segment reference count - corrupted index or hints' unused.append(segment) pi.show() pi.finish() diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index f507504e..cfbb3283 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -6,6 +6,8 @@ import sys import tempfile from unittest.mock import patch +import msgpack + import pytest from ..hashindex import NSIndex @@ -555,6 +557,63 @@ class RepositoryAuxiliaryCorruptionTestCase(RepositoryTestCaseBase): with self.assert_raises(OSError): self.do_commit() + def test_unknown_integrity_version(self): + integrity_path = os.path.join(self.repository.path, 'integrity.1') + with open(integrity_path, 'r+b') as fd: + msgpack.pack({ + b'version': 4.7, + }, fd) + fd.truncate() + with self.repository: + assert len(self.repository) == 1 + assert self.repository.get(H(0)) == b'foo' + + def _subtly_corrupted_hints_setup(self): + with self.repository: + self.repository.append_only = True + assert len(self.repository) == 1 + assert self.repository.get(H(0)) == b'foo' + self.repository.put(H(1), b'bar') + self.repository.put(H(2), b'baz') + self.repository.commit() + self.repository.put(H(2), b'bazz') + self.repository.commit() + + hints_path = os.path.join(self.repository.path, 'hints.5') + with open(hints_path, 'r+b') as fd: + hints = msgpack.unpack(fd) + fd.seek(0) + # Corrupt segment refcount + assert hints[b'segments'][2] == 1 + hints[b'segments'][2] = 0 + msgpack.pack(hints, fd) + fd.truncate() + + def test_subtly_corrupted_hints(self): + self._subtly_corrupted_hints_setup() + with self.repository: + self.repository.append_only = False + self.repository.put(H(3), b'1234') + # Do a compaction run. Succeeds, since the failed checksum prompted a rebuild of the index+hints. + self.repository.commit() + + assert len(self.repository) == 4 + assert self.repository.get(H(0)) == b'foo' + assert self.repository.get(H(1)) == b'bar' + assert self.repository.get(H(2)) == b'bazz' + + def test_subtly_corrupted_hints_without_integrity(self): + self._subtly_corrupted_hints_setup() + integrity_path = os.path.join(self.repository.path, 'integrity.5') + os.unlink(integrity_path) + with self.repository: + self.repository.append_only = False + self.repository.put(H(3), b'1234') + # Do a compaction run. Fails, since the corrupted refcount was not detected and leads to an assertion failure. + with pytest.raises(AssertionError) as exc_info: + self.repository.commit() + assert 'Corrupted segment reference count' in str(exc_info.value) + class RepositoryCheckTestCase(RepositoryTestCaseBase): From 54e023c75a0a87ee1365a4c50de1983d1159314b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Jun 2017 16:22:02 +0200 Subject: [PATCH 0945/1387] repository: add complementary index corruption test --- src/borg/repository.py | 2 ++ src/borg/testsuite/repository.py | 40 +++++++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index a3460031..597d3ca5 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -490,6 +490,8 @@ class Repository: b'storage_quota_use': self.storage_quota_use, } integrity = { + # Integrity version started at 2, the current hints version. + # Thus, integrity version == hints version, for now. b'version': 2, } transaction_id = self.io.get_segments_transaction_id() diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index cfbb3283..4efd0d21 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -503,11 +503,6 @@ class RepositoryAuxiliaryCorruptionTestCase(RepositoryTestCaseBase): self.repository.commit() self.repository.close() - def corrupt(self, file): - with open(file, 'r+b') as fd: - fd.seek(-1, io.SEEK_END) - fd.write(b'1') - def do_commit(self): with self.repository: self.repository.put(H(0), b'fox') @@ -544,12 +539,42 @@ class RepositoryAuxiliaryCorruptionTestCase(RepositoryTestCaseBase): with self.repository: assert len(self.repository) == 1 + def _corrupt_index(self): + # HashIndex is able to detect incorrect headers and file lengths, + # but on its own it can't tell if the data is correct. + index_path = os.path.join(self.repository.path, 'index.1') + with open(index_path, 'r+b') as fd: + index_data = fd.read() + # Flip one bit in a key stored in the index + corrupted_key = (int.from_bytes(H(0), 'little') ^ 1).to_bytes(32, 'little') + corrupted_index_data = index_data.replace(H(0), corrupted_key) + assert corrupted_index_data != index_data + assert len(corrupted_index_data) == len(index_data) + fd.seek(0) + fd.write(corrupted_index_data) + def test_index_corrupted(self): - self.corrupt(os.path.join(self.repository.path, 'index.1')) + # HashIndex is able to detect incorrect headers and file lengths, + # but on its own it can't tell if the data itself is correct. + self._corrupt_index() with self.repository: + # Data corruption is detected due to mismatching checksums + # and fixed by rebuilding the index. assert len(self.repository) == 1 assert self.repository.get(H(0)) == b'foo' + def test_index_corrupted_without_integrity(self): + self._corrupt_index() + integrity_path = os.path.join(self.repository.path, 'integrity.1') + os.unlink(integrity_path) + with self.repository: + # Since the corrupted key is not noticed, the repository still thinks + # it contains one key... + assert len(self.repository) == 1 + with pytest.raises(Repository.ObjectNotFound): + # ... but the real, uncorrupted key is not found in the corrupted index. + self.repository.get(H(0)) + def test_unreadable_index(self): index = os.path.join(self.repository.path, 'index.1') os.unlink(index) @@ -558,13 +583,16 @@ class RepositoryAuxiliaryCorruptionTestCase(RepositoryTestCaseBase): self.do_commit() def test_unknown_integrity_version(self): + # For now an unknown integrity data version is ignored and not an error. integrity_path = os.path.join(self.repository.path, 'integrity.1') with open(integrity_path, 'r+b') as fd: msgpack.pack({ + # Borg only understands version 2 b'version': 4.7, }, fd) fd.truncate() with self.repository: + # No issues accessing the repository assert len(self.repository) == 1 assert self.repository.get(H(0)) == b'foo' From 45ee62e5eaf4219f571dfb6ebe14d9fb01309278 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 3 Jun 2017 00:43:39 +0200 Subject: [PATCH 0946/1387] docs: file integrity --- docs/internals/data-structures.rst | 167 +++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 2a688f8b..4dd296c4 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -715,3 +715,170 @@ In case you run into troubles with the locks, you can use the ``borg break-lock` command after you first have made sure that no |project_name| process is running on any machine that accesses this resource. Be very careful, the cache or repository might get damaged if multiple processes use it at the same time. + +Checksumming data structures +---------------------------- + +As detailed in the previous sections, Borg generates and stores various files +containing important meta data, such as the repository index, repository hints, +chunks caches and files cache. + +Data corruption in these files can damage the archive data in a repository, +e.g. due to wrong reference counts in the chunks cache. Only some parts of Borg +were designed to handle corrupted data structures, so a corrupted files cache +may cause crashes or write incorrect archives. + +Therefore, Borg calculates checksums when writing these files and tests checksums +when reading them. Checksums are generally 64-bit XXH64 checksums. +XXH64 has been chosen for its high speed on all platforms, which avoids performance +degradation in CPU-limited parts (e.g. cache synchronization). Unlike CRC32, +it does neither require hardware support (crc32c or CLMUL) nor vectorized code +nor large, cache-unfriendly lookup tables to achieve good performance. +This simplifies deployment of it considerably (cf. src/borg/algorithms/crc32...). + +Further, XXH64 is a non-linear hash function and thus has a "more or less" good +chance to detect larger burst errors, unlike linear CRCs where the probability +of detection decreases with error size. + +The 64-bit checksum length is considered sufficient for the file sizes typically +checksummed (individual files up to a few GB, usually less). + +The canonical xxHash representation is used, i.e. big-endian. +Checksums are generally stored as hexadecimal ASCII strings. + +Lower layer — file_integrity +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To accommodate the different transaction models used for the cache and repository, +there is a lower layer (borg.crypto.file_integrity.IntegrityCheckedFile) which +wraps a file-like object and performs streaming calculation and comparison of checksums. +Checksum errors are signalled by raising an exception (borg.crypto.file_integrity.FileIntegrityError) +at the earliest possible moment. + +.. rubric:: Calculating checksums + +The various indices used by Borg have separate header and main data parts. +IntegrityCheckedFile allows to checksum them independently, which avoids +even reading the data when the header is corrupted. When a part is signalled, +the length of the pathname is mixed into the checksum state first (encoded +as an ASCII string via `%10d` printf format), then the name of the part +is mixed in as an UTF-8 string. Lastly, the current position (length) +in the file is mixed in as well. + +The checksum state is not reset at part boundaries. + +A final checksum is always calculated from the entire state. + +.. rubric:: Serializing checksums + +All checksums are compiled into a simple JSON structure called *integrity data*: + +.. code-block:: json + + { + "algorithm": "XXH64", + "digests": { + "HashHeader": "eab6802590ba39e3", + "final": "e2a7f132fc2e8b24" + } + } + +The *algorithm* key notes the used algorithm. When reading, integrity data containing +an unknown algorithm is not inspected further. + +The *digests* key contains a mapping of part names to their digests. + +Integrity data is generally stored by the upper layers, introduced below. An exception +is the DetachedIntegrityCheckedFile, which automatically writes and reads it from +a ".integrity" file next to the data file. It is used for archive chunks in chunks.archive.d. + +Upper layer +~~~~~~~~~~~ + +Storage of integrity data depends on the component using it, since they have +different transaction mechanisms, and integrity data needs to be +transacted with the data it is supposed to protect. + +.. rubric:: Main cache files: chunks and files cache + +The integrity data of the ``chunks`` and ``files`` caches is stored in the +cache ``config``, since all three are transacted together. + +The ``[integrity]`` section is used: + +.. code-block:: ini + + [cache] + version = 1 + repository = 3c4...e59 + manifest = 10e...21c + timestamp = 2017-06-01T21:31:39.699514 + key_type = 2 + previous_location = /path/to/repo + + [integrity] + manifest = 10e...21c + chunks = {"algorithm": "XXH64", "digests": {"HashHeader": "eab...39e3", "final": "e2a...b24"}} + +The manifest ID is duplicated in the integrity section due to the way all Borg +versions handle the config file. Instead of creating a "new" config file from +an internal representation containing only the data understood by Borg, +the config file is read in entirety (using the Python ConfigParser) and modified. +This preserves all sections and values not understood by the Borg version +modifying it. + +Thus, if an older versions uses a cache with integrity data, it would preserve +the integrity section and its contents. If a integrity-aware Borg version +would read this cache, it would incorrectly report checksum errors, since +the older version did not update the checksums. + +However, by duplicating the manifest ID in the integrity section, it is +easy to tell whether the checksums concern the current state of the cache. + +Integrity errors are fatal in these files, terminating the program, +and are not automatically corrected at this time. + +.. rubric:: chunks.archive.d + +Indices in chunks.archive.d are not transacted and use DetachedIntegrityCheckedFile, which +writes the integrity data to a separate ".integrity" file. + +Integrity errors result in deleting the affected index and rebuilding it. +This logs a warning and increases the exit code to WARNING (1). + +.. rubric:: Repository index and hints + +The repository associates index and hints files with a transaction by including the +transaction ID in the file names. Integrity data is stored in a third file +("integrity."). Like the hints file, it is msgpacked: + +.. code-block:: python + + { + b'version': 2, + b'hints': b'{"algorithm": "XXH64", "digests": {"final": "411208db2aa13f1a"}}', + b'index': b'{"algorithm": "XXH64", "digests": {"HashHeader": "846b7315f91b8e48", "final": "cb3e26cadc173e40"}}' + } + +The *version* key started at 2, the same version used for the hints. Since Borg has +many versioned file formats, this keeps the number of different versions in use +a bit lower. + +The other keys map an auxiliary file, like *index* or *hints* to their integrity data. +Note that the JSON is stored as-is, and not as part of the msgpack structure. + +Integrity errors result in deleting the affected file(s) (index/hints) and rebuilding the index, +which is the same action taken when corruption is noticed in other ways (e.g. HashIndex can +detect most corrupted headers, but not data corruption). A warning is logged as well. +The exit code is not influenced, since remote repositories cannot perform that action. +Raising the exit code would be possible for local repositories, but is not implemented. + +Unlike the cache design this mechanism can have false positives whenever an older version +*rewrites* the auxiliary files for a transaction created by a newer version, +since that might result in a different index (due to hash-table resizing) or hints file +(hash ordering, or the older version 1 format), while not invalidating the integrity file. + +For example, using 1.1 on a repository, noticing corruption or similar issues and then running +``borg-1.0 check --repair``, which rewrites the index and hints, results in this situation. +Borg 1.1 would erroneously report checksum errors in the hints and/or index files and trigger +an automatic rebuild of these files. From b544af2af15318d3a7473dd2d4168e32abf4779f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 3 Jun 2017 12:14:17 +0200 Subject: [PATCH 0947/1387] RepositoryCache: checksum decrypted cache --- src/borg/remote.py | 67 +++++++++++++++++++++++------------- src/borg/testsuite/remote.py | 54 ++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 24 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index daad2c25..d2cf2a43 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -30,6 +30,7 @@ from .helpers import format_file_size from .logger import create_logger, setup_logging from .repository import Repository, MAX_OBJECT_SIZE, LIST_SCAN_LIMIT from .version import parse_version, format_version +from .algorithms.checksums import xxh64 logger = create_logger(__name__) @@ -1086,6 +1087,9 @@ class RepositoryCache(RepositoryNoCache): should return the initial data (as returned by *transform*). """ + class InvalidateCacheEntry(Exception): + pass + def __init__(self, repository, pack=None, unpack=None, transform=None): super().__init__(repository, transform) self.pack = pack or (lambda data: data) @@ -1100,6 +1104,7 @@ class RepositoryCache(RepositoryNoCache): self.slow_misses = 0 self.slow_lat = 0.0 self.evictions = 0 + self.checksum_errors = 0 self.enospc = 0 def query_size_limit(self): @@ -1144,10 +1149,10 @@ class RepositoryCache(RepositoryNoCache): def close(self): logger.debug('RepositoryCache: current items %d, size %s / %s, %d hits, %d misses, %d slow misses (+%.1fs), ' - '%d evictions, %d ENOSPC hit', + '%d evictions, %d ENOSPC hit, %d checksum errors', len(self.cache), format_file_size(self.size), format_file_size(self.size_limit), self.hits, self.misses, self.slow_misses, self.slow_lat, - self.evictions, self.enospc) + self.evictions, self.enospc, self.checksum_errors) self.cache.clear() shutil.rmtree(self.basedir) @@ -1157,30 +1162,37 @@ class RepositoryCache(RepositoryNoCache): for key in keys: if key in self.cache: file = self.key_filename(key) - with open(file, 'rb') as fd: - self.hits += 1 - yield self.unpack(fd.read()) - else: - for key_, data in repository_iterator: - if key_ == key: - transformed = self.add_entry(key, data, cache) - self.misses += 1 - yield transformed - break - else: - # slow path: eviction during this get_many removed this key from the cache - t0 = time.perf_counter() - data = self.repository.get(key) - self.slow_lat += time.perf_counter() - t0 + try: + with open(file, 'rb') as fd: + self.hits += 1 + yield self.unpack(fd.read()) + continue # go to the next key + except self.InvalidateCacheEntry: + self.cache.remove(key) + self.size -= os.stat(file).st_size + self.checksum_errors += 1 + os.unlink(file) + # fall through to fetch the object again + for key_, data in repository_iterator: + if key_ == key: transformed = self.add_entry(key, data, cache) - self.slow_misses += 1 + self.misses += 1 yield transformed + break + else: + # slow path: eviction during this get_many removed this key from the cache + t0 = time.perf_counter() + data = self.repository.get(key) + self.slow_lat += time.perf_counter() - t0 + transformed = self.add_entry(key, data, cache) + self.slow_misses += 1 + yield transformed # Consume any pending requests for _ in repository_iterator: pass -def cache_if_remote(repository, *, decrypted_cache=False, pack=None, unpack=None, transform=None): +def cache_if_remote(repository, *, decrypted_cache=False, pack=None, unpack=None, transform=None, force_cache=False): """ Return a Repository(No)Cache for *repository*. @@ -1194,21 +1206,30 @@ def cache_if_remote(repository, *, decrypted_cache=False, pack=None, unpack=None raise ValueError('decrypted_cache and pack/unpack/transform are incompatible') elif decrypted_cache: key = decrypted_cache - cache_struct = struct.Struct('=I') + # 32 bit csize, 64 bit (8 byte) xxh64 + cache_struct = struct.Struct('=I8s') compressor = LZ4() def pack(data): - return cache_struct.pack(data[0]) + compressor.compress(data[1]) + csize, decrypted = data + compressed = compressor.compress(decrypted) + return cache_struct.pack(csize, xxh64(compressed)) + compressed def unpack(data): - return cache_struct.unpack(data[:cache_struct.size])[0], compressor.decompress(data[cache_struct.size:]) + data = memoryview(data) + csize, checksum = cache_struct.unpack(data[:cache_struct.size]) + compressed = data[cache_struct.size:] + if checksum != xxh64(compressed): + logger.warning('Repository metadata cache: detected corrupted data in cache!') + raise RepositoryCache.InvalidateCacheEntry + return csize, compressor.decompress(compressed) def transform(id_, data): csize = len(data) decrypted = key.decrypt(id_, data) return csize, decrypted - if isinstance(repository, RemoteRepository): + if isinstance(repository, RemoteRepository) or force_cache: return RepositoryCache(repository, pack, unpack, transform) else: return RepositoryNoCache(repository, transform) diff --git a/src/borg/testsuite/remote.py b/src/borg/testsuite/remote.py index 419463be..681e82cf 100644 --- a/src/borg/testsuite/remote.py +++ b/src/borg/testsuite/remote.py @@ -1,13 +1,17 @@ import errno import os +import io import time from unittest.mock import patch import pytest -from ..remote import SleepingBandwidthLimiter, RepositoryCache +from ..remote import SleepingBandwidthLimiter, RepositoryCache, cache_if_remote from ..repository import Repository +from ..crypto.key import PlaintextKey +from ..compress import CompressionSpec from .hashindex import H +from .key import TestKey class TestSleepingBandwidthLimiter: @@ -147,3 +151,51 @@ class TestRepositoryCache: assert cache.evictions == 0 assert next(iterator) == bytes(100) + + @pytest.fixture + def key(self, repository, monkeypatch): + monkeypatch.setenv('BORG_PASSPHRASE', 'test') + key = PlaintextKey.create(repository, TestKey.MockArgs()) + key.compressor = CompressionSpec('none').compressor + return key + + def _put_encrypted_object(self, key, repository, data): + id_ = key.id_hash(data) + repository.put(id_, key.encrypt(data)) + return id_ + + @pytest.fixture + def H1(self, key, repository): + return self._put_encrypted_object(key, repository, b'1234') + + @pytest.fixture + def H2(self, key, repository): + return self._put_encrypted_object(key, repository, b'5678') + + @pytest.fixture + def H3(self, key, repository): + return self._put_encrypted_object(key, repository, bytes(100)) + + @pytest.fixture + def decrypted_cache(self, key, repository): + return cache_if_remote(repository, decrypted_cache=key, force_cache=True) + + def test_cache_corruption(self, decrypted_cache: RepositoryCache, H1, H2, H3): + list(decrypted_cache.get_many([H1, H2, H3])) + + iterator = decrypted_cache.get_many([H1, H2, H3]) + assert next(iterator) == (7, b'1234') + + with open(decrypted_cache.key_filename(H2), 'a+b') as fd: + fd.seek(-1, io.SEEK_END) + corrupted = (int.from_bytes(fd.read(), 'little') ^ 2).to_bytes(1, 'little') + fd.seek(-1, io.SEEK_END) + fd.write(corrupted) + fd.truncate() + + assert next(iterator) == (7, b'5678') + assert decrypted_cache.checksum_errors == 1 + assert decrypted_cache.slow_misses == 1 + assert next(iterator) == (103, bytes(100)) + assert decrypted_cache.hits == 3 + assert decrypted_cache.misses == 3 From 4faaa7d1fa798556a780cb6e9a8f41b8ec106b5a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 3 Jun 2017 12:27:35 +0200 Subject: [PATCH 0948/1387] RepositoryCache: abort on data corruption --- src/borg/remote.py | 52 ++++++++++++++---------------------- src/borg/testsuite/remote.py | 9 +++---- 2 files changed, 23 insertions(+), 38 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index d2cf2a43..63b5e817 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -1087,9 +1087,6 @@ class RepositoryCache(RepositoryNoCache): should return the initial data (as returned by *transform*). """ - class InvalidateCacheEntry(Exception): - pass - def __init__(self, repository, pack=None, unpack=None, transform=None): super().__init__(repository, transform) self.pack = pack or (lambda data: data) @@ -1104,7 +1101,6 @@ class RepositoryCache(RepositoryNoCache): self.slow_misses = 0 self.slow_lat = 0.0 self.evictions = 0 - self.checksum_errors = 0 self.enospc = 0 def query_size_limit(self): @@ -1149,10 +1145,10 @@ class RepositoryCache(RepositoryNoCache): def close(self): logger.debug('RepositoryCache: current items %d, size %s / %s, %d hits, %d misses, %d slow misses (+%.1fs), ' - '%d evictions, %d ENOSPC hit, %d checksum errors', + '%d evictions, %d ENOSPC hit', len(self.cache), format_file_size(self.size), format_file_size(self.size_limit), self.hits, self.misses, self.slow_misses, self.slow_lat, - self.evictions, self.enospc, self.checksum_errors) + self.evictions, self.enospc) self.cache.clear() shutil.rmtree(self.basedir) @@ -1162,31 +1158,24 @@ class RepositoryCache(RepositoryNoCache): for key in keys: if key in self.cache: file = self.key_filename(key) - try: - with open(file, 'rb') as fd: - self.hits += 1 - yield self.unpack(fd.read()) - continue # go to the next key - except self.InvalidateCacheEntry: - self.cache.remove(key) - self.size -= os.stat(file).st_size - self.checksum_errors += 1 - os.unlink(file) - # fall through to fetch the object again - for key_, data in repository_iterator: - if key_ == key: - transformed = self.add_entry(key, data, cache) - self.misses += 1 - yield transformed - break + with open(file, 'rb') as fd: + self.hits += 1 + yield self.unpack(fd.read()) else: - # slow path: eviction during this get_many removed this key from the cache - t0 = time.perf_counter() - data = self.repository.get(key) - self.slow_lat += time.perf_counter() - t0 - transformed = self.add_entry(key, data, cache) - self.slow_misses += 1 - yield transformed + for key_, data in repository_iterator: + if key_ == key: + transformed = self.add_entry(key, data, cache) + self.misses += 1 + yield transformed + break + else: + # slow path: eviction during this get_many removed this key from the cache + t0 = time.perf_counter() + data = self.repository.get(key) + self.slow_lat += time.perf_counter() - t0 + transformed = self.add_entry(key, data, cache) + self.slow_misses += 1 + yield transformed # Consume any pending requests for _ in repository_iterator: pass @@ -1220,8 +1209,7 @@ def cache_if_remote(repository, *, decrypted_cache=False, pack=None, unpack=None csize, checksum = cache_struct.unpack(data[:cache_struct.size]) compressed = data[cache_struct.size:] if checksum != xxh64(compressed): - logger.warning('Repository metadata cache: detected corrupted data in cache!') - raise RepositoryCache.InvalidateCacheEntry + raise IntegrityError('detected corrupted data in metadata cache') return csize, compressor.decompress(compressed) def transform(id_, data): diff --git a/src/borg/testsuite/remote.py b/src/borg/testsuite/remote.py index 681e82cf..dccfdaff 100644 --- a/src/borg/testsuite/remote.py +++ b/src/borg/testsuite/remote.py @@ -10,6 +10,7 @@ from ..remote import SleepingBandwidthLimiter, RepositoryCache, cache_if_remote from ..repository import Repository from ..crypto.key import PlaintextKey from ..compress import CompressionSpec +from ..helpers import IntegrityError from .hashindex import H from .key import TestKey @@ -193,9 +194,5 @@ class TestRepositoryCache: fd.write(corrupted) fd.truncate() - assert next(iterator) == (7, b'5678') - assert decrypted_cache.checksum_errors == 1 - assert decrypted_cache.slow_misses == 1 - assert next(iterator) == (103, bytes(100)) - assert decrypted_cache.hits == 3 - assert decrypted_cache.misses == 3 + with pytest.raises(IntegrityError): + assert next(iterator) == (7, b'5678') From b8e40fdce609c012b3197f6b6644a1498019282f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 3 Jun 2017 13:04:05 +0200 Subject: [PATCH 0949/1387] editing --- docs/internals/data-structures.rst | 44 ++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 4dd296c4..14c870d3 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -729,11 +729,22 @@ were designed to handle corrupted data structures, so a corrupted files cache may cause crashes or write incorrect archives. Therefore, Borg calculates checksums when writing these files and tests checksums -when reading them. Checksums are generally 64-bit XXH64 checksums. +when reading them. Checksums are generally 64-bit XXH64 hashes. +The canonical xxHash representation is used, i.e. big-endian. +Checksums are stored as hexadecimal ASCII strings. + +For compatibility, checksums are not required and absent checksums do not trigger errors. +The mechanisms have been designed to avoid false-positives when various Borg +versions are used alternately on the same repositories. + +Checksums are a data safety mechanism. They are not a security mechanism. + +.. rubric:: Choice of algorithm + XXH64 has been chosen for its high speed on all platforms, which avoids performance -degradation in CPU-limited parts (e.g. cache synchronization). Unlike CRC32, -it does neither require hardware support (crc32c or CLMUL) nor vectorized code -nor large, cache-unfriendly lookup tables to achieve good performance. +degradation in CPU-limited parts (e.g. cache synchronization). +Unlike CRC32, it neither requires hardware support (crc32c or CLMUL) +nor vectorized code nor large, cache-unfriendly lookup tables to achieve good performance. This simplifies deployment of it considerably (cf. src/borg/algorithms/crc32...). Further, XXH64 is a non-linear hash function and thus has a "more or less" good @@ -742,32 +753,36 @@ of detection decreases with error size. The 64-bit checksum length is considered sufficient for the file sizes typically checksummed (individual files up to a few GB, usually less). - -The canonical xxHash representation is used, i.e. big-endian. -Checksums are generally stored as hexadecimal ASCII strings. +xxHash was expressly designed for data blocks of these sizes. Lower layer — file_integrity ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To accommodate the different transaction models used for the cache and repository, -there is a lower layer (borg.crypto.file_integrity.IntegrityCheckedFile) which -wraps a file-like object and performs streaming calculation and comparison of checksums. +there is a lower layer (borg.crypto.file_integrity.IntegrityCheckedFile) +wrapping a file-like object, performing streaming calculation and comparison of checksums. Checksum errors are signalled by raising an exception (borg.crypto.file_integrity.FileIntegrityError) at the earliest possible moment. .. rubric:: Calculating checksums +Before feeding the checksum algorithm any data, the file name (i.e. without any path) +is mixed into the checksum, since the name encodes the context of the data for Borg. + The various indices used by Borg have separate header and main data parts. IntegrityCheckedFile allows to checksum them independently, which avoids even reading the data when the header is corrupted. When a part is signalled, -the length of the pathname is mixed into the checksum state first (encoded +the length of the part name is mixed into the checksum state first (encoded as an ASCII string via `%10d` printf format), then the name of the part is mixed in as an UTF-8 string. Lastly, the current position (length) in the file is mixed in as well. The checksum state is not reset at part boundaries. -A final checksum is always calculated from the entire state. +A final checksum is always calculated in the same way as the parts described above, +after seeking to the end of the file. The final checksum cannot prevent code +from processing corrupted data during reading, however, it prevents use of the +corrupted data. .. rubric:: Serializing checksums @@ -790,7 +805,8 @@ The *digests* key contains a mapping of part names to their digests. Integrity data is generally stored by the upper layers, introduced below. An exception is the DetachedIntegrityCheckedFile, which automatically writes and reads it from -a ".integrity" file next to the data file. It is used for archive chunks in chunks.archive.d. +a ".integrity" file next to the data file. +It is used for archive chunks indexes in chunks.archive.d. Upper layer ~~~~~~~~~~~ @@ -840,8 +856,8 @@ and are not automatically corrected at this time. .. rubric:: chunks.archive.d -Indices in chunks.archive.d are not transacted and use DetachedIntegrityCheckedFile, which -writes the integrity data to a separate ".integrity" file. +Indices in chunks.archive.d are not transacted and use DetachedIntegrityCheckedFile, +which writes the integrity data to a separate ".integrity" file. Integrity errors result in deleting the affected index and rebuilding it. This logs a warning and increases the exit code to WARNING (1). From 2bcbf8144ef2f7532d358552df23d1403e89b550 Mon Sep 17 00:00:00 2001 From: Steve Groesz Date: Sat, 3 Jun 2017 07:40:08 -0500 Subject: [PATCH 0950/1387] Add bountysource badge (#2581) add bountysource badge, fixes #2558 --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9bfdd40d..fbdd9d53 100644 --- a/README.rst +++ b/README.rst @@ -154,7 +154,11 @@ see ``docs/suppport.rst`` in the source distribution). .. start-badges -|doc| |build| |coverage| |bestpractices| +|doc| |build| |coverage| |bestpractices| |bounties| + +.. |bounties| image:: https://api.bountysource.com/badge/team?team_id=78284&style=bounties_posted + :alt: Bounty Source + :target: https://www.bountysource.com/teams/borgbackup .. |doc| image:: https://readthedocs.org/projects/borgbackup/badge/?version=stable :alt: Documentation From 5af66dbb126043e5b861b4f1df21a7762f93332e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 3 Jun 2017 14:57:24 +0200 Subject: [PATCH 0951/1387] cache sync: add more refcount tests --- src/borg/cache_sync/unpack.h | 8 +++-- src/borg/testsuite/cache.py | 59 ++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/borg/cache_sync/unpack.h b/src/borg/cache_sync/unpack.h index a48559ff..d878dda7 100644 --- a/src/borg/cache_sync/unpack.h +++ b/src/borg/cache_sync/unpack.h @@ -251,8 +251,12 @@ static inline int unpack_callback_array_end(unpack_user* u) /* b'chunks': [ ( b'1234...', 123, 345 ) * ^ */ cache_entry = (uint32_t*) hashindex_get(u->chunks, u->current.key); - if (cache_entry) { + if(cache_entry) { refcount = _le32toh(cache_entry[0]); + if(refcount > _MAX_VALUE) { + SET_LAST_ERROR("invalid reference count"); + return -1; + } refcount += 1; cache_entry[0] = _htole32(MIN(refcount, _MAX_VALUE)); } else { @@ -260,7 +264,7 @@ static inline int unpack_callback_array_end(unpack_user* u) cache_values[0] = _htole32(1); cache_values[1] = _htole32(u->current.size); cache_values[2] = _htole32(u->current.csize); - if (!hashindex_set(u->chunks, u->current.key, cache_values)) { + if(!hashindex_set(u->chunks, u->current.key, cache_values)) { SET_LAST_ERROR("hashindex_set failed"); return -1; } diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py index 690e50e3..6f6452a1 100644 --- a/src/borg/testsuite/cache.py +++ b/src/borg/testsuite/cache.py @@ -1,3 +1,4 @@ +import io from msgpack import packb @@ -137,3 +138,61 @@ class TestCacheSynchronizer: with pytest.raises(ValueError) as excinfo: sync.feed(packed) assert str(excinfo.value) == 'cache_sync_feed failed: ' + error + + def make_index_with_refcount(self, refcount): + index_data = io.BytesIO() + index_data.write(b'BORG_IDX') + # num_entries + index_data.write((1).to_bytes(4, 'little')) + # num_buckets + index_data.write((1).to_bytes(4, 'little')) + # key_size + index_data.write((32).to_bytes(1, 'little')) + # value_size + index_data.write((3 * 4).to_bytes(1, 'little')) + + index_data.write(H(0)) + index_data.write(refcount.to_bytes(4, 'little')) + index_data.write((1234).to_bytes(4, 'little')) + index_data.write((5678).to_bytes(4, 'little')) + + index_data.seek(0) + index = ChunkIndex.read(index_data) + return index + + def test_corrupted_refcount(self): + index = self.make_index_with_refcount(ChunkIndex.MAX_VALUE + 1) + sync = CacheSynchronizer(index) + data = packb({ + 'chunks': [ + (H(0), 1, 2), + ] + }) + with pytest.raises(ValueError) as excinfo: + sync.feed(data) + assert str(excinfo.value) == 'cache_sync_feed failed: invalid reference count' + + def test_refcount_max_value(self): + index = self.make_index_with_refcount(ChunkIndex.MAX_VALUE) + sync = CacheSynchronizer(index) + data = packb({ + 'chunks': [ + (H(0), 1, 2), + ] + }) + sync.feed(data) + assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234, 5678) + + def test_refcount_one_below_max_value(self): + index = self.make_index_with_refcount(ChunkIndex.MAX_VALUE - 1) + sync = CacheSynchronizer(index) + data = packb({ + 'chunks': [ + (H(0), 1, 2), + ] + }) + sync.feed(data) + # Incremented to maximum + assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234, 5678) + sync.feed(data) + assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234, 5678) From 07fbba4ee9eb5b7781136e51b57ddc2701b75c45 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Jun 2017 23:38:02 +0200 Subject: [PATCH 0952/1387] serve: add --restrict-to-repository --- src/borg/archiver.py | 9 +++++++++ src/borg/remote.py | 12 ++++++++++-- src/borg/testsuite/archiver.py | 9 +++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 2081b44e..77fdbf14 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -215,6 +215,7 @@ class Archiver: """Start in server mode. This command is usually not used manually.""" return RepositoryServer( restrict_to_paths=args.restrict_to_paths, + restrict_to_repositories=args.restrict_to_repositories, append_only=args.append_only, storage_quota=args.storage_quota, ).serve() @@ -2339,6 +2340,14 @@ class Archiver: metavar='PATH', help='restrict repository access to PATH. ' 'Can be specified multiple times to allow the client access to several directories. ' 'Access to all sub-directories is granted implicitly; PATH doesn\'t need to directly point to a repository.') + subparser.add_argument('--restrict-to-repository', dest='restrict_to_repositories', action='append', + metavar='PATH', help='restrict repository access. Only the repository located at PATH (no sub-directories are considered) ' + 'is accessible. ' + 'Can be specified multiple times to allow the client access to several repositories. ' + 'Unlike --restrict-to-path sub-directories are not accessible; ' + 'PATH needs to directly point at a repository location. ' + 'PATH may be an empty directory or the last element of PATH may not exist, in which case ' + 'the client may initialize a repository there.') subparser.add_argument('--append-only', dest='append_only', action='store_true', help='only allow appending to repository segment files') subparser.add_argument('--storage-quota', dest='storage_quota', default=None, diff --git a/src/borg/remote.py b/src/borg/remote.py index 47c59741..b4810a5b 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -178,9 +178,10 @@ class RepositoryServer: # pragma: no cover 'inject_exception', ) - def __init__(self, restrict_to_paths, append_only, storage_quota): + def __init__(self, restrict_to_paths, restrict_to_repositories, append_only, storage_quota): self.repository = None self.restrict_to_paths = restrict_to_paths + self.restrict_to_repositories = restrict_to_repositories # This flag is parsed from the serve command line via Archiver.do_serve, # i.e. it reflects local system policy and generally ranks higher than # whatever the client wants, except when initializing a new repository @@ -348,17 +349,24 @@ class RepositoryServer: # pragma: no cover logging.debug('Resolving repository path %r', path) path = self._resolve_path(path) logging.debug('Resolved repository path to %r', path) + path_with_sep = os.path.join(path, '') # make sure there is a trailing slash (os.sep) if self.restrict_to_paths: # if --restrict-to-path P is given, we make sure that we only operate in/below path P. # for the prefix check, it is important that the compared pathes both have trailing slashes, # so that a path /foobar will NOT be accepted with --restrict-to-path /foo option. - path_with_sep = os.path.join(path, '') # make sure there is a trailing slash (os.sep) for restrict_to_path in self.restrict_to_paths: restrict_to_path_with_sep = os.path.join(os.path.realpath(restrict_to_path), '') # trailing slash if path_with_sep.startswith(restrict_to_path_with_sep): break else: raise PathNotAllowed(path) + if self.restrict_to_repositories: + for restrict_to_repository in self.restrict_to_repositories: + restrict_to_repository_with_sep = os.path.join(os.path.realpath(restrict_to_repository), '') + if restrict_to_repository_with_sep == path_with_sep: + break + else: + raise PathNotAllowed(path) # "borg init" on "borg serve --append-only" (=self.append_only) does not create an append only repo, # while "borg init --append-only" (=append_only) does, regardless of the --append-only (self.append_only) # flag for serve. diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 9e207f96..5c0c52a1 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2861,6 +2861,15 @@ class RemoteArchiverTestCase(ArchiverTestCase): with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]): self.cmd('init', '--encryption=repokey', self.repository_location + '_3') + def test_remote_repo_restrict_to_repository(self): + # restricted to repo directory itself: + with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-repository', self.repository_path]): + self.cmd('init', '--encryption=repokey', self.repository_location) + parent_path = os.path.join(self.repository_path, '..') + with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-repository', parent_path]): + with pytest.raises(PathNotAllowed): + self.cmd('init', '--encryption=repokey', self.repository_location) + @unittest.skip('only works locally') def test_debug_put_get_delete_obj(self): pass From 8dfe2a8080618f2a0fdc8eaaf49e37f38dae6150 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Jun 2017 23:38:49 +0200 Subject: [PATCH 0953/1387] remote: show path in PathNotAllowed not 100 % sure whether "if old_server" is required, so let's play it safe. 1.0 -> 1.1 server is no problem. --- src/borg/remote.py | 9 ++++++--- src/borg/testsuite/repository.py | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index b4810a5b..5dc2a495 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -87,7 +87,7 @@ class ConnectionClosedWithHint(ConnectionClosed): class PathNotAllowed(Error): - """Repository path not allowed""" + """Repository path not allowed: {}""" class InvalidRPCMethod(Error): @@ -391,7 +391,7 @@ class RepositoryServer: # pragma: no cover elif kind == 'IntegrityError': raise IntegrityError(s1) elif kind == 'PathNotAllowed': - raise PathNotAllowed() + raise PathNotAllowed('foo') elif kind == 'ObjectNotFound': raise Repository.ObjectNotFound(s1, s2) elif kind == 'InvalidRPCMethod': @@ -747,7 +747,10 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. else: raise IntegrityError(args[0].decode()) elif error == 'PathNotAllowed': - raise PathNotAllowed() + if old_server: + raise PathNotAllowed('(unknown)') + else: + raise PathNotAllowed(args[0].decode()) elif error == 'ObjectNotFound': if old_server: raise Repository.ObjectNotFound('(not available)', self.location.orig) diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 4efd0d21..25e112bd 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -815,7 +815,8 @@ class RemoteRepositoryTestCase(RepositoryTestCase): try: self.repository.call('inject_exception', {'kind': 'PathNotAllowed'}) except PathNotAllowed as e: - assert len(e.args) == 0 + assert len(e.args) == 1 + assert e.args[0] == 'foo' try: self.repository.call('inject_exception', {'kind': 'ObjectNotFound'}) From da99ec2fbdc2ef5866537ea04eba7f0cb31a6833 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 2 Jun 2017 23:40:45 +0200 Subject: [PATCH 0954/1387] docs: quotas: refer to --restrict-to-repository --- docs/internals/data-structures.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 2a688f8b..d2921c7a 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -223,7 +223,7 @@ since the quota can be changed in the repository config. The quota is enforcible only if *all* :ref:`borg_serve` versions accessible to clients support quotas (see next section). Further, quota is per repository. Therefore, ensure clients can only access a defined set of repositories -with their quotas set, using ``--restrict-to-path``. +with their quotas set, using ``--restrict-to-repository``. If the client exceeds the storage quota the ``StorageQuotaExceeded`` exception is raised. Normally a client could ignore such an exception and just send a ``commit()`` From 28e4eb4ea3e23377b2cb8c1e11ea576d0d6c74dd Mon Sep 17 00:00:00 2001 From: TW Date: Sat, 3 Jun 2017 15:13:21 +0200 Subject: [PATCH 0955/1387] add a .coafile for coala (#2592) add support for using coala, fixes #1366 ignores / disable are set up so that there are not many faults. we can improve that iteratively. --- .coafile | 38 ++++++++++++++++++++++++++++++++++++++ docs/development.rst | 15 +++++++++++++++ requirements.d/coala.txt | 4 ++++ 3 files changed, 57 insertions(+) create mode 100644 .coafile create mode 100644 requirements.d/coala.txt diff --git a/.coafile b/.coafile new file mode 100644 index 00000000..39494964 --- /dev/null +++ b/.coafile @@ -0,0 +1,38 @@ +[all] +# note: put developer specific settings into ~/.coarc (e.g. editor = ...) +max_line_length = 255 +use_spaces = True + +[all.general] +files = src/borg/**/*.(py|pyx|c) +ignore = src/borg/(compress.c|hashindex.c|item.c), + src/borg/algorithms/(chunker.c|checksums.c|crc32.c), + src/borg/algorithms/blake2/*, + src/borg/algorithms/xxh64/*, + src/borg/crypto/low_level.c, + src/borg/platform/*.c +bears = SpaceConsistencyBear, FilenameBear, InvalidLinkBear, LineLengthBear +file_naming_convention = snake + + +[all.python] +files = src/borg/**/*.py +bears = PEP8Bear, PyDocStyleBear, PyLintBear +pep_ignore = E122,E123,E125,E126,E127,E128,E226,E301,E309,E402,F401,F405,F811,W690 +pylint_disable = C0103, C0111, C0112, C0301, C0302, C0325, C0330, C0411, C0412, C0413, + W0102, W0104, W0106, W0108, W0120, W0201, W0212, W0221, W0231, W0401, W0404, + W0511, W0603, W0611, W0612, W0613, W0614, W0621, W0622, W0702, W0703, + W1201, W1202, W1401, W1503, W1505, + R0101, R0201, R0204, R0902, R0903, R0904, R0911, R0912, R0913, R0914, R0915, R0916, + E0102, E0202, E0211, E0401, E0611, E0702, E1101, E1102, E1120, E1133 +pydocstyle_ignore = D100, D101, D102, D103, D104, D105, D200, D201, D202, D203, D204, D205, D209, D210, + D212, D213, D300, D301, D400, D401, D402, D403, D404 + +[all.c] +files = src/borg/**/*.c +bears = CPPCheckBear + +[all.html] +files = src/borg/**/*.html +bears = HTMLLintBear +htmllint_ignore = * diff --git a/docs/development.rst b/docs/development.rst index 7d4c0774..6156be36 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -195,6 +195,21 @@ Important notes: - When using ``--`` to give options to py.test, you MUST also give ``borg.testsuite[.module]``. +Running more checks using coala +------------------------------- + +First install coala and some checkers ("bears"): + + pip install -r requirements.d/coala.txt + +You can now run coala from the toplevel directory; it will read its settings +from ``.coafile`` there: + + coala + +Some bears have additional requirements and they usually tell you about +them in case they are missing. + Documentation ------------- diff --git a/requirements.d/coala.txt b/requirements.d/coala.txt new file mode 100644 index 00000000..4b5f3678 --- /dev/null +++ b/requirements.d/coala.txt @@ -0,0 +1,4 @@ +# style and other checks for many languages. +# some bears (checkers) have additional requirements. +coala coala-bears + From 8ad309ae2a5479d146f679c561db6b154d719cd7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 3 Jun 2017 15:47:01 +0200 Subject: [PATCH 0956/1387] recreate: if single archive is not processed, exit 2 --- src/borg/archive.py | 4 ++-- src/borg/archiver.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 8337a974..b72bbda4 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1612,11 +1612,11 @@ class ArchiveRecreater: if self.exclude_if_present or self.exclude_caches: self.matcher_add_tagged_dirs(archive) if self.matcher.empty() and not self.recompress and not target.recreate_rechunkify and comment is None: - logger.info("Skipping archive %s, nothing to do", archive_name) - return + return False self.process_items(archive, target) replace_original = target_name is None self.save(archive, target, comment, replace_original=replace_original) + return True def process_items(self, archive, target): matcher = self.matcher diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 2081b44e..82b51638 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1598,7 +1598,9 @@ class Archiver: if recreater.is_temporary_archive(name): self.print_error('Refusing to work on temporary archive of prior recreate: %s', name) return self.exit_code - recreater.recreate(name, args.comment, target) + if not recreater.recreate(name, args.comment, target): + self.print_error('Nothing to do. Archive was not processed.\n' + 'Specify at least one pattern, PATH, --comment, re-compression or re-chunking option.') else: if args.target is not None: self.print_error('--target: Need to specify single archive') @@ -1608,7 +1610,8 @@ class Archiver: if recreater.is_temporary_archive(name): continue print('Processing', name) - recreater.recreate(name, args.comment) + if not recreater.recreate(name, args.comment): + logger.info('Skipped archive %s: Nothing to do. Archive was not processed.', name) if not args.dry_run: manifest.write() repository.commit() From fd0215c3c2e25ae2c4ce0b5f805b7103e473bc0f Mon Sep 17 00:00:00 2001 From: Mark Edgington Date: Mon, 29 May 2017 11:53:31 -0400 Subject: [PATCH 0957/1387] patterns: don't recurse with !/--exclude for path-prefix (pf:) Fixes issue #2509 --- src/borg/patterns.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/borg/patterns.py b/src/borg/patterns.py index 924b51fe..897c75e2 100644 --- a/src/borg/patterns.py +++ b/src/borg/patterns.py @@ -150,9 +150,8 @@ class PatternMatcher: if value is not non_existent: # we have a full path match! - # TODO: get from pattern; don't hard-code - self.recurse_dir = True - return value + self.recurse_dir = command_recurses_dir(value) + return self.is_include_cmd[value] # this is the slow way, if we have many patterns in self._items: for (pattern, cmd) in self._items: @@ -325,6 +324,11 @@ class IECommand(Enum): ExcludeNoRecurse = 5 +def command_recurses_dir(cmd): + # TODO?: raise error or return None if *cmd* is RootPath or PatternStyle + return cmd not in [IECommand.ExcludeNoRecurse] + + def get_pattern_class(prefix): try: return _PATTERN_CLASS_BY_PREFIX[prefix] @@ -386,7 +390,7 @@ def parse_inclexcl_command(cmd_line_str, fallback=ShellPattern): raise argparse.ArgumentTypeError("Invalid pattern style: {}".format(remainder_str)) else: # determine recurse_dir based on command type - recurse_dir = cmd not in [IECommand.ExcludeNoRecurse] + recurse_dir = command_recurses_dir(cmd) val = parse_pattern(remainder_str, fallback, recurse_dir) return CmdTuple(val, cmd) From ffcf6b76b61786e7fc979c79c95c52e8ed6b3c6c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 3 Jun 2017 21:54:41 +0200 Subject: [PATCH 0958/1387] DEFAULT_SEGMENTS_PER_DIR = 1000 prettier increments for the directory names. --- src/borg/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/constants.py b/src/borg/constants.py index b60627f5..c4cf0a9a 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -31,7 +31,7 @@ DEFAULT_MAX_SEGMENT_SIZE = 500 * 1024 * 1024 # the header, and the total size was set to 20 MiB). MAX_DATA_SIZE = 20971479 -DEFAULT_SEGMENTS_PER_DIR = 2000 +DEFAULT_SEGMENTS_PER_DIR = 1000 CHUNK_MIN_EXP = 19 # 2**19 == 512kiB CHUNK_MAX_EXP = 23 # 2**23 == 8MiB From b06ceb6547b5af149de1f97bd97872326396b7ce Mon Sep 17 00:00:00 2001 From: enkore Date: Sat, 3 Jun 2017 22:02:52 +0200 Subject: [PATCH 0959/1387] Revert "Start fakeroot faked in debug mode - fixes EISDIR issues" --- .travis/run.sh | 2 +- Vagrantfile | 2 +- scripts/faked-debug.sh | 6 ------ 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100755 scripts/faked-debug.sh diff --git a/.travis/run.sh b/.travis/run.sh index b32de444..7c1e847c 100755 --- a/.travis/run.sh +++ b/.travis/run.sh @@ -19,5 +19,5 @@ if [[ "$(uname -s)" == "Darwin" ]]; then # no fakeroot on OS X sudo tox -e $TOXENV -r else - fakeroot -f scripts/faked-debug.sh -u tox -r + fakeroot -u tox -r fi diff --git a/Vagrantfile b/Vagrantfile index e7186109..10f8cec2 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -324,7 +324,7 @@ def run_tests(boxname) # otherwise: just use the system python if which fakeroot 2> /dev/null; then echo "Running tox WITH fakeroot -u" - fakeroot -f scripts/faked-debug.sh -u tox --skip-missing-interpreters + fakeroot -u tox --skip-missing-interpreters else echo "Running tox WITHOUT fakeroot -u" tox --skip-missing-interpreters diff --git a/scripts/faked-debug.sh b/scripts/faked-debug.sh deleted file mode 100755 index 924193c6..00000000 --- a/scripts/faked-debug.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -if which faked; then - faked --debug "$@" -else - faked-sysv --debug "$@" -fi From 93e9ca0d23bf9e9afa003c4e9c58fdb4e5aafb4e Mon Sep 17 00:00:00 2001 From: TW Date: Sun, 4 Jun 2017 00:18:09 +0200 Subject: [PATCH 0960/1387] update CHANGES (master) (#2594) update CHANGES (master) --- docs/changes.rst | 119 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 114 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index ffc7603b..6e3394cd 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -136,15 +136,13 @@ Version 1.1.0b6 (unreleased) Compatibility notes: -- Repositories in the "repokey" and "repokey-blake2" modes with an empty passphrase - are now treated as unencrypted repositories for security checks - (e.g. BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK). - Running "borg init" via a "borg serve --append-only" server will *not* create an append-only repository anymore. Use "borg init --append-only" to initialize an append-only repository. - Repositories in the "authenticated" mode are now treated as the unencrypted repositories - they are. +- Repositories in the "repokey" and "repokey-blake2" modes with an empty passphrase + are now treated as unencrypted repositories for security checks (e.g. + BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK). Previously there would be no prompts nor messages if an unknown repository in one of these modes with an empty passphrase was encountered. This would @@ -154,6 +152,117 @@ Compatibility notes: Since the "trick" does not work if BORG_PASSPHRASE is set, this does generally not affect scripts. +- Repositories in the "authenticated" mode are now treated as the unencrypted + repositories they are. + + +New features: + +- integrity checking for important files used by borg: + + - repository: index and hints files + - cache: chunks and files caches, archive.chunks.d +- Verify most operations against SecurityManager. Location, manifest timestamp + and key types are now checked for almost all non-debug commands. #2487 +- implement storage quotas, #2517 +- serve: add --restrict-to-repository, #2589 +- BORG_PASSCOMMAND: use external tool providing the key passphrase, #2573 +- borg export-tar, #2519 +- list: --json-lines instead of --json for archive contents, #2439 +- add --debug-profile option (and also "borg debug convert-profile"), #2473 + +Fixes: +- hashindex: read/write indices >2 GiB on 32bit systems, better error + reporting, #2496 +- repository URLs: implement IPv6 address support and also more informative + error message when parsing fails. +- mount: check whether llfuse is installed before asking for passphrase, #2540 +- mount: do pre-mount checks before opening repository, #2541 +- FUSE: fix crash if empty (None) xattr is read, #2534 +- serve: ignore --append-only when initializing a repository (borg init), #2501 +- fix --exclude and --exclude-from recursing into directories, #2469 +- init: don't allow creating nested repositories, #2563 +- --json: fix encryption[mode] not being the cmdline name +- remote: propagate Error.traceback correctly +- serve: fix incorrect type of exception_short for Errors, #2513 +- fix remote logging and progress, #2241 + + - implement --debug-topic for remote servers + - remote: restore "Remote:" prefix (as used in 1.0.x) + - rpc negotiate: enable v3 log protocol only for supported clients + - fix --progress and logging in general for remote + +Other changes: + +- remote: show path in PathNotAllowed +- consider repokey w/o passphrase == unencrypted, #2169 +- consider authenticated mode == unencrypted, #2503 +- restrict key file names, #2560 +- document follow_symlinks requirements, check libc, use stat and chown + with follow_symlinks=False, #2507 +- support common options on the main command, #2508 +- support common options on mid-level commands (e.g. borg *key* export) +- make --progress a common option +- increase DEFAULT_SEGMENTS_PER_DIR to 1000 + +- docs: + + - init: document --encryption as required + - security: OpenSSL usage + - security: used implementations; note python libraries + - security: security track record of OpenSSL and msgpack + - quotas: local repo disclaimer + - quotas: clarify compatbility; only relevant to serve side + - book: use A4 format, new builder option format. + - book: create appendices + - data structures: explain repository compaction + - data structures: add chunk layout diagram + - data structures: integrity checking + - Attic FAQ: separate section for attic stuff + - FAQ: I get an IntegrityError or similar - what now? + - add systemd warning regarding placeholders, #2543 + - xattr: document API + - add docs/misc/borg-data-flow data flow chart + - debugging facilities + - README: how to help the project, #2550 + - README: add bountysource badge, #2558 + - logo: vectorized (PDF and SVG) versions + - frontends: use headlines - you can link to them + - sphinx: disable smartypants, avoids mangled Unicode options like "—exclude" + +- testing / checking: + + - add support for using coala, #1366 + - testsuite: add ArchiverCorruptionTestCase + - do not test logger name, #2504 + - call setup_logging after destroying logging config + - testsuite.archiver: normalise pytest.raises vs. assert_raises + - add test for preserved intermediate folder permissions, #2477 + - key: add round-trip test + +- vagrant: + + - control VM cpus and pytest workers via env vars VMCPUS and XDISTN + - update cleaning workdir + - fix openbsd shell + +- packaging: + + - binaries: don't bundle libssl + - setup.py clean to remove compiled files + - fail in borg package if version metadata is very broken (setuptools_scm) + +- repo / code structure: + + - create borg.algorithms and borg.crypto packages + - algorithms: rename crc32 to checksums + - move patterns to module, #2469 + - gitignore: complete paths for src/ excludes + - cache: extract CacheConfig class + - implement IntegrityCheckedFile + Detached variant, #2502 #1688 + - introduce popen_with_error_handling to handle common user errors + + Version 1.1.0b5 (2017-04-30) ---------------------------- From f1709df8a9223d72b7632a201350f53037492ea8 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 4 Jun 2017 17:31:22 +0200 Subject: [PATCH 0961/1387] docs/internals: layers image + description --- docs/internals.rst | 10 ++++++++++ docs/internals/structure.png | Bin 0 -> 201363 bytes docs/internals/structure.vsd | Bin 0 -> 99840 bytes 3 files changed, 10 insertions(+) create mode 100644 docs/internals/structure.png create mode 100644 docs/internals/structure.vsd diff --git a/docs/internals.rst b/docs/internals.rst index db5408ad..35ab0018 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -26,6 +26,16 @@ To actually perform the repository-wide deduplication, a hash of each chunk is checked against the :ref:`chunks cache `, which is a hash-table of all chunks that already exist. +.. figure:: internals/structure.png + + Layers in Borg. On the very top commands are implemented, using + a data access layer provided by the Archive and Item classes. + The "key" object provides both compression and authenticated + encryption used by the data access layer. The "key" object represents + the sole trust boundary in Borg. + The lowest layer is the repository, either accessed directly + (Repository) or remotely (RemoteRepository). + .. toctree:: :caption: Contents diff --git a/docs/internals/structure.png b/docs/internals/structure.png new file mode 100644 index 0000000000000000000000000000000000000000..e79e76c429b729aa5ff064d298a5574619e31933 GIT binary patch literal 201363 zcmeEv3p~^N|9_>N5?xOrx6VCop3?vNg&kW&FLN$){`KZQFZ}V`?nr&fu`O991NYU>{0C>7 zkX}JrO>+4Uz9HrGIDh`4K|80zO<{-4(H7u;PsgB}^PNG?pnv~`>9g&WIo{E@DC~%b zx&Fqee^17U$gux09j)INIKDsWNsRmV98W&A+uGC`{_np4@OMT;Qtnckk1cWe52*4u z<$p}axa|Y++bzIo*MH6N$V{pKsfcuBrs<%~DNGhBqw7h6|?@ zc#iFtEk)*n)<`#*;!zXbU4)uc;i!U$xO&CBAN^KwLd0;q)|56SpbKFm%u>2wuuUy& z_*RjjaGY=vdhynvzt-%x;oFB^A5?%v6ghErn$0zQvg79sQU5`WS1 z(HjQNpBH;v|M?}B;oSTb*zh%;aNJPH9Jps{Fq|s-<5Q>)Nr^ZL`mAupgq6G<)FI{4 z54^6O0%rJ>+r3=cuVefg-SS~>B7O4eqxtMegH}u-+%Ue^1y(;zzT+Odt-ULBpPO5= z4W}4}lfP*?3FDS|wP9BLklVZ7?Y^9Ind5*T^bY5@WjJHE6i8=zeH_B_bHGCK|JbiJzho4bX zrbaWp=@X=)3&;vyw09_DAfI5cy2anL9(H1OiyeL<@w!f#$HTH9^LS^<(z1r*_PK%_l3;csZ?exekAfY}j&{ zGr1CC_d|}U51j-S?T>2_FcgGi+;(c zqlQh3lGP8z&JB2yfv9Qq4jV}<3aa-k-ZDP^4j#4vq1z#Nw@#~28$Yw1idcZZqM=&A znr0Y3Gv9Mle^QrUU-+2mH8V-3yJ$L=4a!Sc)jGJbYPE%2(J~`jRt>jj-3&>KD?i_p z{Z}|gH(4Fs^pDgW@ck8D6m2nbWC7GsI6>4R9T5o2 z(SaLMzOLmK9k`d_k}dw1Xs|z){Z%AWHjOCb!QaAX9JNLvj_t}?J;(a*mgi}$M+D*w zfyz-)w5?WC? zM+H!c?1$wqf|*|OGutcweBBsZ5P|ghZko-yKNMwggv<(()U4WSMV@pMm##6eb&3@#aIueVvdPB5LWzXixn2Yt6LzRY`}!F&RDbCO9KHDK-2Z~ ztevVio!N&^^JkIX35c|~k&-CN?fQ>|reo{0(&FSw!b>E;n3Q51iBZh0)tY;I1vMf! zIagjpWS#oE#E*aIuy4eA+8)5$maC-!i+>+xFc&k1Nc(ahjttvs zS9vU;=J_`VA8GMsSNLY>3aRM@KjKTH@s5+7lG)DK<%W~TtqBdmyvA7qMGlIVD0Fyv zzjAy6stlRB4qL=*yfb%MzHw@tjsiSz;j)Mf4&b|>bCVOT%F124Kd~>2d zK313mHzlvFDkoRo3S`$hXPS-!mkhv8M9!CJKDMwhaQK_|{_(N+7N8>6Lal!#F`i#j z=6Btk*{O%bVh;(a<`*xLZ#=&6doD=p2gIEx5~u;N{?zhYl_7QmLaV+sOM)O3k?^r91zOl7Lm_;4wa`(BvlOzt1=EfzwW>lXCA2f7-(N4KL9=3a z4L}4?jzucg4KOes+nXA%xqs1%$?g>j7e&IPoLsSP%MRXq7B&aBY1CFT4a50$ zj#HmjJwdZWnfl%ep+heX1&jM1K?v4X-qVXjF0NNY;fKR6OXy60QUeGny%bb)@!wH0 z(n@P+7_T6#llTSG9`UKjPU2-t2bT8)0I%qviFO(2KGBa&&=u?qIjBRe(;sI zwtZy`x&u=92&MNK71Oay)GWK*)*jxV-_k6)jQ!bm$}hXZDen95#sG8bi`Yim0;_SyGvh|Ndz)@wR5P^DK+ z$>))$BLLYiJ7sMuRRJwOK^xIq9w+VOv_V86d@@g65W&_q((JRV$5;U`aY0=J5L7uK z#V<^AW=Q^+6d?l##Ow4NFAi$l7ISF6JmS}88}M36+BIqiu;7@Z2x=|K7<5L>@}6sD z*$UCAPw}|2>UjCi@P|%MVkz5T1oIbu35_88AxG^a1oVZFrsE z7)l$sdT)y7ZsfEF#I8|D7+mx_!p*N0f@ie^mqa=^V;2uE>6cb8RZyT1)J25nI*64a zzH;EJ?+qtDD*l!*Unq}|X02?#5%K!hRS&0dZ<8Shqf@&J^KLSC1^p_ddk}e=uKo0E z!&Bn05g_b|XPrkB2cF&*G9Z|+N%^>1v=1<(MB**fi;yL(SmF((fEn?bFFFBucQQjg zQMnHEJuYTuj-rE*^^zn$;9*QvduRYQXvwO`E8QNVb;mCyM)3xq3^O{Qp^=eROIhhT zzLsU0UY*zz9WqCOer2e(*C5$yVtyuuk|ziGj_DWuC^fuuJ|}G9=506}&KKSPg;Sje-Ci7I-dUwdcb->zwO|oLyqPWEc@LRp2OSs4$%NGd6`p zE%{#bW(@fBE7_eIHzO^bvCr^<m%MGFU4hVc4Ro1v?-OX>NjkcJd)Hf~zR9}9P_N04zfve|RaRIcal5gi%L%GLI^&lv}kI&PoB!WiQPK z-g6T7D(KT@DwJylAOB2BK+lY({BoHyL4M$(JqMaB1a#{XmWKM{`S!*9}#vDzhFg_E1{TrloZ=GV=<@Krq@=R6M+D zAg1Hhq>_Ah>JzpcL9}PGmJCAM3fT7h_3Uu`7>kNI% zg7u$*pq|G_OvVl_k=5go#5`X=9u&9UiJ&X}&erjWrxwx;90- z93|G@)rewh7rq*{Y3r(~J1=(=g1@8ZZ{Ss!a|O&SD-*q6kZom+>Y6AoE5yr|@oZ)B z#tkCxOxi{ns%QKT zG~iDGXj-YayQj^RB>aTIg}yM?jmR_mTm4L{Q6Xd91qp%Y*6d2C#6k;ScN?Udr9_M`c|#-jmSY^1{%X9S<)0fMJ>nE*kQ@Mul{3 z7398xbl11&^W}#*d$MyK++{W`)yQZ}YBICrh8&U}&~v*QKi+Z#RIb~HxgbD*$alw` zgJE9pQrx20I}Y}RBtEB;0)kCD^3NcSk}qL@%SMimKBtpo;z1*U9`$V4Xh1w8x`F8s8bzLxwJbL z62I4>&F@k z%yO^4q_SW~7QWPrxPHPZ!PJ&2PSq^}ItQ(fT?Xc$ltdvMnvpu2$&Da!i(w2AkLV?a58RLzfHna@}W^D;Rdq;;D6DG**bYT9C1sDZh4YiFHNsaa?A-tLAC zPGEN+#wtH7m8tV%V_R1sGGBVH<}?}Mv1QlwG0 z7)K4$;jbT|R=w|Bc?t659@5s7sll=E;+rW3>fC{}kr)F#+7G=p9nAe*vSf3+V{ISU zoZeFzg1NB2?MR!(qA-$ITKr)4nSfNJ(S*rJ&187dg7eyu@yXiJ7f1_@GQw2l48aU- zi?;U~NnoOyHnY00iCQjB8!Z>jY|PV9FFD7c1SP<;?y_1fvHs53gf(vpD?=d4+Yj

hW)+KuMARDXUnm*DA%R^;9TANInaJ~Gjb1+k}%R4pfRO%Pg`(&XQoP#@i+=O z9-87tc$lRQRz0e~@R6TLXoPugd}?>wBO#mASFTfIZ1n+qD%1$n^P!5W?JoZ`(OW?n z&K_^DQnqKs_r9x8u~cf;zhGPDP3Up4+*ieZU2OxX zZlFhq`<+Ac#xNcSXcs=A_OI&&~c&#plMQK408=yPcOLXv1X2# z1S2_Pe5YnmDW}B21vi@HSv2+VfJcA41Gt&Q+ymIYyFg+f85*?q`;5aECoyu`@~O(F zO0~$4ZY5gyrg{#X$N}q?dF<8PsGP8kS9D=DDot3pcdNFp2orA%vQ*}x*W7-`Q_b&xJJ0qC0&)uW(;v96W&Vgi*MdR(?WU6F+Z~X zVMRsAzyvKO{6srAvpLj!XO3}=_2EY-dAhGWc7c9Yu4Qk4D+^S)dVw-#*sSwZ*c1j| z7MFATVPc>^><;u-NUyew2Y-z-7F3?Wt${%J8R<=_FF$0dG->CYUPO0r31=tu2M8x0 z?)M-zDb>R!-nkiztG}fH{+Kz^qz8>NY5Gp!&@*FucInrZn|Nf(#7; zAJ1OWhivaq*M~ON2e;}k zV=`mKZp0(@$7Y-KP1v1s_og>G2A&SZhlMP>V2gaN`^e3sen7&q@Yy)Yz;A?jC-`@Z zruV#O>2UW3`)uy67ySBMhhEOPYaXOWA_-i22uNi**+UBJdP)*zM}Gn?wCjGmTEKB^R7g?D`3`)adWH#fyDdkfR3|{ z1V?m*W(5mEdI;f{&^2*6?Z2)#0Y$1^p?cfEJlkhZ|r<%o|uQ|7_Jk33S0nMzaAo z+0nxhFqh^sA;=v>mtEWzw;R!^g3x%4@(Veb*cHZo{?$65%EwuHxjn|~F~q5iH5>i? z`)RxbpqeU+CxvGFppE0t*Pl-^XXQECDz(MJEbp*VN-vvpTx<-V-`lD2cR8q8=Boxi zuq%kHQWo#33|XmposJZ|((!H53P|;ok5R^GQ2ex0UFH1Ynr&hWt@(iwi4P*S#$zO$gmt4R_C_?3PQOgu#x&DrSD@nn?J2M4aL=3 zrX)2fAs3ujx$8}Pag=%usqe6g$SPxfs5ey>Ab=LNvuAh>yokj)M6r8xQ1_kYA>Xc^ z;XCAviJWd16(02!9XfDb-K_c4L@U3Tyd#X2Fo?eU+_BdqQ+vefu#Y-79&E3cvoa4Z zHDZMdYtnQI!5 zX~`GxKi*&*0s>N{65<)JFWFrwLIjWs-J_z+iq<`DQa*7%Qd0z38Dft0hbKiRK7{-h z6*vEeE1hY%kYell#LAl8WNK()5LdK4?bDQi-__8t*kts4_=Ta<=T}2Ttg{RNJvH_k zlCqX`OGC$T<@Te5a5H#avJmY4Ha)!`e`MH+OAl}^vZmSIWIMOi3^1DjZfruYwRNOJ;NE-=&ie}I&D0e0(0`%@ z3YM`B$eAlYJPO}gPsH$|i$Kap8ryWukN4|RCzVhly5|sbAx~zC@pTkwhQ*BxbciQ}DdeFvxX;K~5>a1MaO#yKs z0VPE@UB-%SbOy73u@xFf=JtMou(#d=k61>x*$G1Gr9s?>9zv2>n>GxigeWRTS+&WG>T+23D z%m)4FX)7Pz&9-fX34W#GigT^8Wr zFQwq`P%UJBp^>$kNAl9xZh6kMK0BGh#@MuOy?TvW*^^%g(c)3_jYuYt0ERjM@nF&tmM>>vCgt@^m6sti6Xauuf61w} zQP8@r^Hv!V-{w6iYDaLg^h(De6C*)YA-+{X?3;>pYcjc_PdxDZ`>uLZ524ykd%3BJ zD@bkSssj?zUGi2^{9E)`Y5_=d3OvntS8oBS=Hs0b#DH#3@7>tFhf zbHmJYX^GZTENtyd1r4yqr+l)pNE924AK?%1ZjP+$UXW&Gmpy84PtS@E(RfRW$aQE7 z`84hd0tyM5YiDA&h)Df8%*V8KdwF0`Oho0DgFI8>i7{v*u;L(;B0ia8}|taK^>AH$iELmv3R?(_cp2yWF<(aTNG04`< zHZShdEwP1R^ofb`fL%BgU#6%#wVzm?!~JZw>HxEaop8B7{2O4hH^)8trS6MQzY9of z&43^@456n;;e0;IYW;H^L_GA|J5pcZMp%ki=zZ!!_L?I$vV%2CUwUtU+k3X z$$YnSv7`IE67{3lXTdBDRj0cSS;#IdWu++?WpbtRsYw$*&+hCwc5%yUySAOzz79~ylsOfvf^282$ z1!PmV0$KA^SaBdaPX5XLW|Q)Kt4$OzAt}~D*XXk<+lYu?4pe@gvTfE308SZ9D;yO| zeb8SvRzh%5dTppVQ{m6OecVa1ITV3B#}|X}7!Lu8gx$#hZu)Y+@01 zVkf7M6F6l6b?GnwGA@Hnl8Vtw0kwVbQ@>r%mnu>RX13@Mad;ea??t&jDG+$GuZwoT zh$rOAeJUh|`V919XWP5P@$?VT#d`z`@t7eyf{|IwxzDB-Wq`2aFQz^l2JYs>nJEWl z%)Ga>-TMYZ^0bQ-9?{K4Dv}LUJn^&$o}bXF{^7f;aeETR^|~!rwyi$0b>No6Zx0m# z9c`_~edtH-T)GtM0jq`p&X$Ga+0AlBY#P?s;BEi`nhJV}Mwa*G5#CeIp;egWX$0Qn zqZ99wk)D97Q53`E1%xvTOTx0D_U%MttW+^Dbpp%h)GlW12OO`JviiTo%#_v3=gR6+ z727d=`$W0YA6JstJ}ttgwdH{i71_ri5V~ z)B&&Zn>>FgtArVV;niSxJ23!8`H|-%AqmPvj$~tFyav>C@%@-(T{+4d1B{WM){#@O zjuAI}*P>|?7mvg93{m6Tdpg65C}!`iwA)?{XE4ZRUJ0vt5IYo| zr+2v|OGZR64gAU;P%d2q1Z%MZ{lKX&lq-hM#q)QugbmqhjRUtxfW2wRFODn>sM0I? z-LdQfL1HLX*Axm|>qdn zy1z>CL#`sq#HK_OAvthZ4tHTAhUy|=T(uYp33+wI>sZc`h*zP%j`$2_rl~@EhCUMH zM}|nU51_J463Yx{2*mV@+_{KPu{%>z$wZKL60T<%1L|SQiXHM(uB-NKT$Y_afjQII z(ni1x`_<%S>M&`62(+{3f!>72E_IKG5}Hy!q_2fgx$lTpyPnFd1M_3EP{& zSm`l0z(uOr57z*+=eG!puYo8BHQsIo3VcO=^W%$sE!V}>BGcCLH)EY+OMJIqIeKL8 zMsqq>>;_`dI`RUI5&!EW*Hc=oYNM;;9Q>6J91zCuO3 zwPBkqa~0Y;5r2dUCuxIDxvIR))m9_yLmO3wG}*1=WG@-04(5p+Ks4OItT8`&W zNtd>~dhFH(;zF9AG8G-FcBjoM2X1T$G9J)6T5V^wQ5XTR0Bt zKtFCcA5F7$*Pc)hz|zE8sLr9L8GZ~8F)DS+~$+FEBT&*?fiFLbvq2@weUao~u5$sfrMdf4RhaT>``UNF7D7j;;s&AO#<Y$sKC)AG z$^GW$XB$Fa5Dm3q=IdJ8cJ`cPQijenXQbaqosS51lv_fT3-)|dCs!F_gnT7IFLrj6 zIg;4-t|UVRA&ab?yc+0ff28VHuuIXV?dDqX>4gm-v60tdgaj?ZVjy_8O(MJ?7+=&h zQc;kj`-`BpH*3p2LEUwFM|$v3SiZgBZX&J`K*Zz}X*0p49=17ZscIfIu)%kTbasOW zJ>HvR4BWw6dh2QC>1D*r99!G0@zQio3jmJl2)AtB_DCt7q3Md@mx2cYGq zr*&CEtpS_$_h`#hDQhUTH97IAZ|VjwCr{7SPtA&~7t z>fH4dmw_)uw!fx_9CbJ>FfQB@eEw|XyX11|+WoiYS>E!oW;wM21UeCqjZ7YIn4lcD zy)|_xy42FrZ2l``u)&1CbW)3XyrO^gyP?2ZyF%(FZBrwCu7hcYKp|^iZ&I+OwOZqr ziz`(2y5_+Xj#tMrpzf}o>9}U-T{F0|isU3^T~Xbd>LUY3{oOo-P?7EF%tT7gXkHeW z10-|&l+$BHWtpCiP(a+E`3byotE-&OG6-cS$J{!>+@CtS>9ps-DV#4OWBUsAMWXORH|f3C1##3+TYX%yd$V8nHLA&b zknRHr-<4wEbu^x9nq4maa{rr32gfX*-R`-Yi81fV8TH#>_;5DXCPC~9F~{%Zs+gNP zH4khWrH#qyf~JxTj9@@6KV7?a5h~SkBuo|aM8Q)cbA)AbxFW-13~GwBZ6wyIn-cW= zF>K`H_sNuPj;qOaXd`=7en^P-N>7j5CxJy0Vf+}NmU_o(|5*80M`jMylFowYZcpNTX8PZ@NlZP+X&C?Z>Lb+R$b zZpNg`KeC^or4?Eh^7H_F`>vNj`6&~WB=#K~Xf}tme;VqWEG?R&u|oF_>s` z4$lVj8VVBZ2PD`h_06)o`fc`{Drd{XAyjacop7`-@hr2+7q^)~1Q3djlTb2dik zUvVJij5oPh@X8z$;SW=_lg=F_M<}vQ%ty`-?C=PXYfq4QdNnH;L@BjsvSds5u;u(* zx(|hSdH9^~Pw~aJ_Te!1C>rNqP23?LW8N1d$6uBx^62D|C$Xr(zQzh)z{kQvUVp%m zj$6Z(85d_!(8ZT#-Bh3HyqjH@lvG>A9U_5rZZyPB4ekPD2qxkq14LG;A4V?D^r~rm zCh7=tEC;0N9@ymz)6uay{Rm zw!UQ(05`f_Ds5Go(#-_`$@1D=9C3}?I=5|+nWww=aJFY8pIwiB?!Y2$NU@gm)JcY} zJA`~L$sT~p`=`N{l4Md8gDz3?lT7?5-X-X}(^mMEbJwiUtg zyKDqE@ToCn$-jwC5qHCl;=Izd=`Q5;bc_6ueDOKYc-;s*eX6aTok}i^&m9-1P!UY*(C>>^=_Ym7*i*L)fws{hHACN#En$+nBWAjp z8L;A)2IY$WR}9W{*ooBXSv}~bPK?lsXvTCS%SoaYjDG}%f5!H+0KsucHo5uD4>1n7+zE*kIn~@QM^DlO+Y9fX(3Ha z!7X0JlTT;&5y|hUsb|Ea0FbjcwH)h51r%*Sx*@)2b_C`oK$nQn7E6+}Doj2B0s+Jn zojrC4u{+ST0n4*$4wAotbjMpjZ8;1L-c6u2fMwIrY#mgc*O7#Ai-=T~*k-jb8h7LO zdBLuUvTY9l;J^Mf#6Itu(LY#G0HCiTvA;w0;`+R;`zK$SWOfrqOIwcZhU4O<`fn+T zz7c=5_4LZOw^GK0eU_HG0cJ|mLK7J$ngQ{h^hOkr{pj54t$+b(2MkCRNc?*|>35oG z4h}z?v;BhRD+Lm_E_v4u{PTuq?#nlwN%}*XKfuR9U??fPT7K<6G+F#I1EZU3 zkec03h5fW}wNyR*UbtpUu3zfyUz(r4-+{=%DMsoiB8Ul z+4Wp;);9Qjo@^1`RjnX1Y=rpxMKjlpg$vojB`k>`1=jP#QXs1E+UiL3j>)Fa=t!sS z)9%64oslA7u8E2zIsbXG#iB1c@xR<{#|_abworh(2>29LC7%vv=O)VttS0>DMiDpx zo;`CpkhS>Bug_T~DFBt2EYr1j)vSf7kP{adWbl_QB8iGTnll=6&;NO~NC@C<+JnQV zh$*uVj!lD~y${bop=T||cc!G`EntHd0>5T=2NZRsCQNv8Vh9P*owF4F#{AEIBm>@M zY2nmlw&Co9POhLm2k0y2x=3eNo!!-!O?NB;jQ5hoixv}2Z_kohRsgV#t6@#BYYQNkB7fXS z{du*D>1LfAq@l2Hu8BAl2oVWIX@7mhKiK4S7Lca}z^~ce{dauLeUbe5srU7N;Oo$W z@7X$p$1j~6_Wk87#i{b|8B`23P0K^S!D;`S+2>bv08!tBdAx8|9(;Zg*@x>b-xrB~ zx%*4N_XlCV`LB-Y{#r6AC7d(&VEJ}nIk4F*;eNnYb@c*3Am#(V{&)ClZDV@;o$jlD z;A_zQ@7W5-@1m)(Vv+5h>L31|L3RMV0FKS>*HeYgoNl_6323nS(HUp2+&&Zdzl^hFU5pWZ84>|%jctmB(EaARk$=t`!x+x+e zVs3Z;9bXX$rDq>yPJOq*uXkP+@jY87hldx0hf?P!=sXzudj{o9=Tm4FT-o^9AL{S` zAl*OzJc-CBfGWitR^9x$oqBpl)Rz@+y-V1~ojldw{*4`&CRQ`&gP;51 z$K!sTy%1j?aZdkafd%oFQp)qyxGpDNuNC)(%{^*P|r~Pbls2e2j&snfRC#R3P?SY$fb)-K2 z{X4~H`sYVbQ?n0N!~?1>)OdE_!VjflS~n@JU%!5CcmEY%=PR?8Y@N6&^$&bi1G><^ zVe9Y@iY>p%Yhmd_@~>+aGcbMh6au0EPL zsQ;_AjQIJ#edm199AE@Vpr>HmDtz|A4r4&om2aGFTb(8RoHCfD=(KKHy<)|R{~f+Y z#Bvs{f$jU@P@*q4B~nuTJGMp`8aM#%g#PX((bcn}IY)`FcK>ClOzE3s#*10a>kph2 z=)V5z=jV?6+2rWfF4{ONZvUOzG?WggI{MwYQ3127&RMjX!%pj_Z~UhJg0B^CLN4Ac z|E*^3RI`ly>hu*$r%`@ivlWnEHNc&ad+dH`$*gGp^`5`X*|B#e__ogY9FZ8{tpEIz zc0QY&#IB1!-QoX_++ptu@58__$Z#lMIE53k|4oo5J%$oapu(wmzkTKs6d;ibu`*UA z-kp{F7NFnAyTWiZ3>b|#rFeBb^6$et{JJq<0G(2F8o)gXwMfy+F_@LU zmB1B}7&B@lgmLrLriP|~(S{Sn-fwIq-g;dBO76e96=j8ysL*a@M=Eoy>z*kvqN@xZ zK$Ng@2I=xq-vlDGi14nX&RF3XTsUOZA0dFO(!DX~Z+-iQsB;~_R53a*$h(xs7Dn-KU|jmp{m zQ-N|LErNb@6-f(2b@qEF`lpF}-`rU)aMKxQ;E6~wP=i9dzG6$yWKSctrUSO=$rF)U zo0j8!?)*?weRKvKTc8*JpE~P+!opvx*uP2a{=YhjX^sUm+<9?jj z{k9bN?Yn0T#gDT%GkqvCBR#5SrR3YK{7r`ctEoZXNYEc2f1Kd|dKS!2K>)^6d`tZ~ zL;O<^fb!grOFcg|!Z$Ny<~ow*;>Z~V@iz(npBmw>hxDONb*KRoncJqvB^3dqZGowW z-jlrSq|%Y?bMp@8izc7y0Gjm#ognCBD3GM4 zD#4zp%^Edv5l23SJk^VZhW6GxZ|Q&JOwbqvMxL?_U>fg>0ms2(1EKMX+m|Ji|Clg# zKSh5FOCL5JUyW+z+F7Vgfw6pIX<;EI*I^PmP;PrpLH82l4x}lSOqDet9~tG37JEcC z_(SNYCS_ZVMg_q1%E@Yi1w(jgWA)KWJs{w#TSr!}s-H@vKTW)u%U1r6dFgrG0{|W~ffyYyW zAC2*GyUZJ93=bDuRy8x8GY-O{6wz!jU%xpJbJJ`Ti;soV*@3y_QVH`hXh?l2YY7?K z5d9+Du~5VEU|#<&uFcvXx@BK_Yl^G7o=ON*{d`^A#v?hO+Ik>DIaG8?vGh9KTL{Z0 z6e>yt?Fuw>nCVW0AnwPBEjs4^qN3Ix7?M_T`|VpjhMw+K#O2^)0#un-_kvRSYoIW? zWA9*;OQz*7OUafor}^UHriKR*X+@Kv`y%MT6g&vfL9h*B<)EDN2D0(@D-=t#Lu&rsOiw<#FaLA8CbGmNabzNN&$ zYJh!Anbkd^krQu4WLm_?xM*?04qAIpLiUwOHeln|c&x(-jglFD;W*z0&g((&BXB6j zC)jf+xuV6ryDn_d`>-{83O4%6cLUIdw7T6xtLTf=Mt%9go4_E#2MBy3f(c6i54d?B z_ns;ld#k@!MN#@@qLbxvS?JNIE+S?$FF{N=`haz+y9tRYCZ`4(7wdI}b&8&t+<*IY@I9p>v6JU`U2A&H;ObH9 za$5-*l;^;eO7ptSE5Q_T7gNXWARBLwT4R9S=7cwK{KP^ye*So0|A54&B=dGgIDqGs zmkd@iwjzIW$HRB`m$4yMFcOb;i~24UsE6Ua$I4ULT9orW@!pL)6!7YC(wl*F_rI0E z-M~nsw!)z17SdLj@4k<~bbGjXG3KFb87XE-8{B=9w&ZBgLa)BzXpGPKg_*&8EAPEJ zE-^2a5&5GfaianjSFpz4q*nugnruU#DlLm`c9R6rnph%?*hnZ4G?iL~M`;=W0n57T z7b8v&KPyGxrxScm76a2UH}$xg)`JD)tq5H?C4&wJu*CD8Jb$pK;Rp|yUJJjZrJ_gj zS4o%8lJegNH!uO|X!nqtaXgQG@R6PVDSLGYauBzjTlCf%qacUrI4^p9y@ZvgA_CuS z!9BQh@L{$q{G=}P)KqW@kQTWLmHLZuE`J^%E6A^(?w1zgU0g|)t+$Nsf*Nk*((>&L z8{)YJRaSi&4)A%OD5)4T<-ux2U*y75uAq@axZwRx*^ZvLdn;Jn+DGY3sy_n`3* z!0^1vsJh z##~Mb`PbN(McnLgOu~<@7Rm|bar9DQ zHw!?)S4V$18&2K)AjN7@IL3R{dxwI4e%Qk>@GP`>lgF><1hSeuPF6V%?LZvvXB=qv z4_KzboIp@0OPa6i_&;7oid%s_=bb)So`T>$YE*0hI zWH#z1LZ);$jEqZQF5bdOg`8Fy@VNuW)$)J;#*OmjmHjbwv73;(u-?V5FK6tLOM@=H z*jwXPSw3yOju9=W=M0(>(OPB6(!kN|$=Tiu?8}UfBI_-W--S^u+uHQveF75+v{KcX z;h{bYiV18AvnfnhfJy5+e|A63IvIsQQT@yjl(Oh)uV9I;Th-))oW7@!t(JzKt!W0- z*#kEEyKU4(mbjZ-_+YmJ4L=cl@pabAp39>o{qDYiMib}?Y<5Ai2IOgMbj^ld6Z; zq_jV2CWa9!cy6J`P!_leGu9FxK6(-wa}O^`UwJ?OSKU+=9d*;wW|RZ0;w#=`Wc4}b zt{iu7WNCuOCKdqv?BylrXnMHln=>0F*GwMkrE5bMUN)a`B{ny_ z71{dsLaH9jzT)sH@4$LLfk8S9Np&l>etyk%r@n9}qb-T;b!R_iI@f2L*jAzGR_)0X zx3}tsSqx5K3t=59>IWteM#k61fKsX;maGDj``fY1l4*zELbvyZ z3Km*WXTq$KpC}ow$k{wYb*iMK$-&9EavyI}Vh+M4 zCZ2&em5|P-rG)3WQbd2~IFUxQ%~_}#FLX*^4W+k3kVIe3akde*lECDqZiOG4{FQNo z@@*KGvLST)32`Z{76{lAI&$RW-S>xvV|&|@FG2N7kXLWDiG8uhb$Qu$-&zd&!ROCI zVHrhSy#8x{{%+F6D}r$WV>bod)a6z=ujQml&xzXL$cTFQ3uPi9KQhxlplK1otNu0fH@oQS9=e&O1MgNf z%aK^v(c}2w8G(l>1-k=Ia=yG!6;bZ0E%gp+*fTzrbT^-kqvIlaA@gB4tBE4a<$B=E{!=U2EcyY{VHfkyL`^0LC51Im%~z){7x$v zhfbg@-|HU0b?U_7Ln-%B3%x8?L7S{lg43I*jf?J@OS2yO+V3YbxW?X23os!o&1}L4 zu7zaw{X#IRQ23;51e?I6b2}m`I(-VtYpe0?0wZtpN<`8WZX@-nmGWc`qVxhS)IcYVb!_$?BF-fC~uHxS}P@Rl%aJ9ntO%oFnse?0Ejz^veqcWYorv40C zg?$f|o&-W3wu+|zp~qNZIUKEmbRM34kbEzDdauyr0KF^)PTNhE5SSC*k=ca$bEDxt zVI2UxBb;>-?Z*#L6$-hZLefI4l$2@JkJcd3V<8o#3c4;(H10wH(`ZRZkHEul7cQ`d zFihiy*{SNj0ilXp26+|?Jbb@@{Fhp7dce2=03}(W=G|kPQs&F44|oF`J2vF_;1_&) zl3lQQeeHph_y8L{d<`QuT}QbKd4s_Ad2%#dOQj|u*M%{Z#&hFDHFc32vA^SWVT0_X z6%XPU##r}d4Mz3;tk&bG-JjogKc)eXu>L^S(7}^W@@Z6U|Ga~plbyw>&^)H4y3)YR z6^-KwNauC+4h6T$0LV_LQ*eYibw*u3!E1gS3IHI4a8ubfv?2|4d&*IK-_9~|D|{+V zqJB}tw7bC;nar4EpI=1M-GQph*v7wDWQlO_4Q_&=(^qu*#x?|t{CSJYu6=4ZF`o%j zA0iN}yY_CtcNS#Kd)YDVjSq)W%GckoGV}WIX~;y7pC4Gy()Wuoa+0}5?^t}28Ka&6 z4-b#1v;7#(K{HS9fVl{WC&$ndDoc9=3ULXr&aRXRy7Y@9v9d?@vk`)QdjTA|(HHz}>xjQrSBJVTLcISFTj|T^ILm3NGM)oWUr;Y&g4WLDT zAg5I~_cZk^w|`pYOS*u;0_&ZPn4qO-TV>6Ja*p|zYTMPR>W7#4T;ll|bQT}ovr8a{ zeB052H{2&*{Loah^zx$2jK>d1SJNTl#= zX~0H7tZT5fkcI=c{wa9`A{-#_2I<+=G1UIp#)ol4Q$5t#J~U@Nr(z$9^+`;ziWh1^Wie z0o(LnKYAbrB1$lcm9C}u8T%IK$o3g30E{7T6?_b(`o$4QuM~?rqijv%AG8>I74+kC z^sqe-On&NUTtvX+pDPVm7od?Z&(}N?iOfAc=9ihN+qFsXCUPOXt^}og6?KE?31{4w zNa9>ckgEyL4taHEwF&Lvhw`$Q@So_Jv3Vr7z~&2$b}9Zyc!Ta{nsA&4o#4#Q5+IxN zrq7vcMi2r_a>CS7(=FI{j1YmeLZ)-UCOFUC{CY?$ix?k}{WiQ=v1??-`9OaIbEyAB zXfs3CTqA{CV$#^|BAT&q@O9}4g5r`vA?r_`7%*)$*EI1bey$cNRv!kK^%rtTR|6-H zg|Dv5NjAyUXQN89FQl8%!_F^yoTPWEI}KgcVaam@%ZiLXQ@JOc@tx0P%EV&hPHZ%2 z+tDacTt@Fdd>(b=5*)KHf0^%YN>RYG!lzb9eHZxko$W<&PFljnVU9fv);c<=&dIio zvaMT@6vgA#ICcB?36AeM>-%0`{+*X08Nf{85MvvtaC|P4&M1|8gP{%1R8v3DRa@ZK zPPb+`OPm<%egqn82m6gynMEESV zN+-lQ#ifSwzC%ElMNLrYX|XJ7%0;kW>3ykyQow}Q*F5YcCFu{ih;w}Ek-#Bm(_xQ@ zMt}7O9i=t==KLf#HUXie?Ae;th8{(r#-c89V+Y!<0NF5_Csg&L1q)RvpTa=AFET1~ z<`u%clr@(*<}cC}v|x#w7-lPv8BwaJCj%Oqbx`&>2mCdpw#_^a`}P^l0({QPQ>Cx>=aMT=Ftk3pJBR(|zoIJEN3w7$gP$V2FBT0{1FflS4{*ahwpGD1Xm{R*TbK&? zxbSxvh+A3${HvC_kHib?CMY|f+m+=c&a!FOI7p0y^6h)puYtg zdxxrj8;Z|6Z;I*;%c2=zN-$-@C=sODCXD6-%cigWfSc;7966O6>!AePIJ|}uIcyHe zS2Og9p3WJ_xCCY1PiryNX9^>hS$I_hFi6j1RWWB6M(kI_{`qS%-TWwgjwbT7WjGE| znZ0;ItTbHHCOl%szTe?w{b^@+2L58F8A*V=d-BT^Sg}n@uQ*y5xv_Zhh8sA z@Pu;vf>6QUc$QHVf9!J4JNV8{Qw=gm4nwgkyC|mN7m%IopJzUtNlzWdY0E4`OjuQS z5MVnQ@!5{QapFtC9o9aFFi09at9P^JR86|vu{V(?%zb{Lg~YhMiftU;QMTW)ISk5- z$TuAG@171GxONJ4eWw}S?h}1I!c=tZ4e;$fFQdavKiL4lU`Z1;M89V$&ad;_(Ylg2 zn{)f{mYUD}_BrS%<#n1X6Nyy>j3e`XTFZDYF*o4#1>eovM=q6Wpm3c;Gl~zfyxplZ za1;QoPAY_zN)y<(y{z&Hf#=g$m5!`ddg)Mln#f>(HhxURa4=~TqF8LgsECAdJ;fI@ zO&Y~mZFJXWfswf<57=QNXMKT!c`##Bgl3O@gW3aQ_HmKq6EeL7AUX+Y(^Zx(EF2Dy zlF)Xj#;gOb`PijVMT4&c6w_bkd$kBN}VXEuzVZ%vq|c2k32@o>vIsjY$%-=`oGU;t(SF|4wgx{Bk_ ztR5hl=zGai-RyrpvJ^0DK^7L!t54{}P=x5NaK9P6{YiMpSl>_zwU~#z zdOHDa!lF^}Pu*FpEx>y@LyH1)EmH!U{GbyaR*7c9o{Ezx^0IDEx zBL6}_ZuxU1{cD8zzi!hvw(vPifipi{faGdr{HW2wxdl=-1g~dxW3{0u2%gszUtZ9a z)xSM}F{FjC>BzlsfC5KT{RB-J0OJF7-Ih4v>6cLiWPvY)aTOWGJi+259FW$0n5!pN zZ`ZnQALwmMP!&Y$Zu__Zc*Yk(rGHG?T!NRLs!yW zLjsTnAk5GW6OK8LqeeUf8&VuN1wujKN>0ykh2LdBxItoiXlXEy2$h8bX^192INd~^ z%CVu@3)Mi2ArCW;?fW&0xwQJr+#ew6T!_LfBiNg!$N@&8C+a?GiOkK>w6f(TmYDJU zoQsW{k|z4!4Q|zM`!Pt>Jr#A_~~JRc+bb4ne!Wh`J?N9YCS@CguXi>LMB;_6JjOodvGc#1E zEuzuBfqFLv13qoDcg#BA<{Bn7D81gm#e5?h z4Vg_51a~$M1CitbAT2pB zJlYQ+*Ro3!KrX;Js#022pBR?&2cNy-F^&ytx4 zL9HAsG3kD1q-QhX+ALy|$e=L%F*BXlNdW{vBbd|mH^6ntaJZtp`)l{s`~9*ljF0L8ag*=^u0TF&e!V9!lzDlZ=1-ytc z=7|s}3-f78vP*;ZS(%uBjg|q^5J?JLNx?nmB+*0!JfQAwjYi0%S+fc6vn%c!Do+ zSe1#5&jEeZ#Hz^U zCvvPlup#4(dAC2+&zd{@;jH@ZxBJWO6-+#XucLC^^L{&&6MwOiitGnqESauvr@pm}j+9Cif}Ih6O} zo-Vq3&Twjw9~Q!DC|kUmgbY1>3a!d&sLMp8%z9*EQih4-w_t^NQ?h!KUA~bO^=rMU z>|4F5mn3|NDh-$E!r@)e4|kk)KHmOJo`2DA;@LoRKI6^hOm$`c@+bElDk``hL8-J4 zqkZ^_X~(uijk24YSVVH4PozJb?nm`YKzIeW>kCp*DHoe`G{US8)wG3qq6_>0D~Tk* zB&($Ub!a!Z8AhOFfp>yg+0ZD~3y7Qx<+&uu7`Kv&GYVPdi^wtiA;`Bmj(UOTM+xZx zwjzL)F0MX@Z2C7pfPlbbODl~S zEE$_P7+rKurB6g9OX=hnAq?RL`M)P||6|)3m-J=bN(rPd0WEzA3dq~#f0qnpU?k%lk;Iawwt@D|f$~5! zVf3S8Y%3$tGi_#3BqX3HeVkk41+>4Yr>+`Ugzq^b0n^+N^~zu=sW9#4S)39M6_uK% zAJeyBjHC|f_nGR5Shf9nAoPru-92OdMNpaMm)MsX2O^7UZDr}lZbj*D5dnTsK2iyT>bqAxUENa`abICvo&R`IL ztlt6;o?KgQgN7~|v=HU70shA>tj56L{p=-#5Wxo~TIM!|XiY^b2i@e@6BidfU7ZfF zsgMR<=RvauSLx=!JH>N_3UUgWIwtL&#%brms(O<``wvuI3C)`+x0GG{994|eNd|Hd z^Vbc!zoJWJvQK2hF;`2*8KMi_DpS1%15-aE|K9=b=IznJWhSap}LgFU4EhezB zEI;0F?P>Iyw(U_RaXR>%PRsFkE@P+LT(Mb;;oXCfr8n2GsM<2?r=l*unX#^x zx&Own5X*k8I_5~&rmtn`nr@2~MjS$sOm}i?5z1czNvki%ZuD)EyRKHDm>c86ucDANJL*i;%j@rPff-Wc?T z_t;O=?&YGVGu8bSm6rp`h&xA2#omUnw8qwh=9-%~>Z)zT5@|g!_4^pz2R@{Ag{BX%piL>D}6w3cz+5k|S2D2Ki}-9H-@;jJp$z7ma-bE_)Ef zGagswrYAhVGxh#}{N`lkEoavGTwJ@-BGsidQ}19$@hJ@f9>NB{YgB3+c)eKQcXaI^ z4@SxbbeU{=Bf8V_Zd)QxEL`VRKOFj_dC$j|$Rc1YS`Ga}(9s7iG$)HZI!mwuV6MCyBFt?r{8mdn(?ZLz>&ZQJWbd{ELBEOxPG$9LN|cy0Gvr(d?U z(N-qc%2HzwjhU+c*}74G6*$fagzod6V+-wk3NXmH8SkFsY4w(~zRk@wj5imFg0z4X z2(9}5Rr(rRwX<@4S17q2P)v=Gh~(WjAqz4VZ+o6+yE&8_3Z#2f!(Y9`l6Gx4 z954dIuNE5!nBs(%qxaGo0UmJ8?uxZ8{>`8>RQk9->E0$B5pLnc7A{{L+52xK?m7TbnDXut0t~<`WR8*%aXPcVB1L4BF-U4> z`n9R$(?0Q%@t3+M%V=k4vFrO}#XE-R^F2IGyy0jL_a-t!oA@a*|Z>@N4XlC9$u6TQQ0=ii6IC$2FyTfERYx$7U zvK6DNc4tB=9HQjCd_I|xkcXbW^Hub%$kAV|HRyAo|L{KO)p*-l?_d~7nz*a8N z?O7RdcD}>R%jk;;999`xW>Zb13t4tdJz@U;k3!a zLn*s6^x<+yM~f=4)+|p?Q+O{qot9r#^*nVyPM0=bxdA@$!^x>zMF$q$PCzmpce?i% zGO3E%E=3othTzOV`{tt{m5*~#NC3$5jw@ExD;jnh*3&LyyJqel&fk8&BF>6dy!5Ph zWYm+zMLor*pyafX?k9Sr!NT<-gmyN6NaXbXieynD_AQM;uTrtVS%LsU)0K2_^d;UE z8#Z3)boSHncYZUKk0N{U=u&VAW<`TzwAi+T?~2C)9nbJQ-*NwFgwcAFv|0z4v{xnq zf$x}aHx|yj;~2tymyR1|iG-l9^xse>lulkSA6x9-T*GtT^0a?DnsGixOt!pVen&xS zDj#)5f*Ou(3dRzy-E8~_y3bT)H=|U0MrgWEEjuo?!p~X(_RdJ2k;#Q(oYH(G_an?L z4uE_fyJ>o20vhF2)a|=r~ zK4OW#`nG1F4oU0TLwO5P^-+XlbBVD4*cVQx5boL>{QgRp6f^Z{X|8QSL z1&FR$WJphj*zcZxz=GkJ&hJ92-9F;b#F z_~b6YHlMmEXP%o{W?r`ij`@SBu!P7;GDifzNkqmMG85Ay^J9xtX}u72K>dJflvt+Hm}uvK zR3rlcCAZgbMS#o`ZE1)WffA#7-?yo004p~a)_z`^i3%G{g=kvt=|+O?IT7N90N zGh`gLSY3U1+2PYreG`BE_kD|BOqP@KUW5dCp&HyU7G7u0X%j$o#ILE4v6W$4(dHgI z;f}~i%|zCoA@AaLW4YGoLOFA%6-Vzkt%V}!Gw2q}v3jpOAfw7S#!C!RZP`{Jp=br% zqA}Feew3zTH~d+91+=arMBP5MxR8!!q$L≈6X#_AI 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/internals/structure.vsd b/docs/internals/structure.vsd new file mode 100644 index 0000000000000000000000000000000000000000..b03a4f28f545c6a1bf0b16f1b4931088205a0370 GIT binary patch literal 99840 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�wj0bObl^gpOK387_Va6#O?1 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 Date: Sun, 4 Jun 2017 16:16:56 +0200 Subject: [PATCH 0962/1387] docs: data structures: demingle cache and repo index --- docs/internals/data-structures.rst | 109 +++++++++++++++++------------ 1 file changed, 65 insertions(+), 44 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 61614c32..040fc512 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -104,12 +104,37 @@ to the file containing the object id and data. If an object is deleted a ``DELETE`` entry is appended with the object id. A ``COMMIT`` tag is written when a repository transaction is -committed. +committed. The segment number of the segment containing +a commit is the **transaction ID**. When a repository is opened any ``PUT`` or ``DELETE`` operations not followed by a ``COMMIT`` tag are discarded since they are part of a partial/uncommitted transaction. +Index, hints and integrity +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The **repository index** is stored in ``index.`` and is used to +determine an object's location in the repository. It is a HashIndex_, +a hash table using open addressing. It maps object keys_ to two +unsigned 32-bit integers; the first integer gives the segment number, +the second indicates the offset of the object's entry within the segment. + +The **hints file** is a msgpacked file named ``hints.``. +It contains: + +* version +* list of segments +* compact + +The **integrity file** is a msgpacked file named ``integrity.``. +It contains checksums of the index and hints files and is described in the +:ref:`Checksumming data structures ` section below. + +If the index or hints are corrupted, they are re-generated automatically. +If they are outdated, segments are replayed from the index state to the currently +committed transaction. + Compaction ~~~~~~~~~~ @@ -384,13 +409,13 @@ For some more general usage hints see also ``--chunker-params``. .. _cache: -Indexes / Caches ----------------- +The cache +--------- The **files cache** is stored in ``cache/files`` and is used at backup time to quickly determine whether a given file is unchanged and we have all its chunks. -The files cache is a key -> value mapping and contains: +The files cache is in memory a key -> value mapping (a Python *dict*) and contains: * key: @@ -438,6 +463,10 @@ Borg can also work without using the files cache (saves memory if you have a lot of files or not much RAM free), then all files are assumed to have changed. This is usually much slower than with files cache. +The on-disk format of the files cache is a stream of msgpacked tuples (key, value). +Loading the files cache involves reading the file, one msgpack object at a time, +unpacking it, and msgpacking the value (in an effort to save memory). + The **chunks cache** is stored in ``cache/chunks`` and is used to determine whether we already have a specific chunk, to count references to it and also for statistics. @@ -453,46 +482,7 @@ The chunks cache is a key -> value mapping and contains: - size - encrypted/compressed size -The chunks cache is a hashindex, a hash table implemented in C and tuned for -memory efficiency. - -The **repository index** is stored in ``repo/index.%d`` and is used to -determine a chunk's location in the repository. - -The repo index is a key -> value mapping and contains: - -* key: - - - chunk id_hash -* value: - - - segment (that contains the chunk) - - offset (where the chunk is located in the segment) - -The repo index is a hashindex, a hash table implemented in C and tuned for -memory efficiency. - - -Hints are stored in a file (``repo/hints.%d``). - -It contains: - -* version -* list of segments -* compact - -hints and index can be recreated if damaged or lost using ``check --repair``. - -The chunks cache and the repository index are stored as hash tables, with -only one slot per bucket, but that spreads the collisions to the following -buckets. As a consequence the hash is just a start position for a linear -search, and if the element is not in the table the index is linearly crossed -until an empty bucket is found. - -When the hash table is filled to 75%, its size is grown. When it's -emptied to 25%, its size is shrinked. So operations on it have a variable -complexity between constant and linear with low factor, and memory overhead -varies between 33% and 300%. +The chunks cache is a HashIndex_. .. _cache-memory-usage: @@ -556,6 +546,35 @@ b) with ``create --chunker-params 19,23,21,4095`` (default): You'll save some memory, but it will need to read / chunk all the files as it can not skip unmodified files then. +HashIndex +--------- + +The chunks cache and the repository index are stored as hash tables, with +only one slot per bucket, spreading hash collisions to the following +buckets. As a consequence the hash is just a start position for a linear +search, and if the element is not in the table the index is linearly crossed +until an empty bucket is found. + +This particular mode of operation is open addressing with linear probing. + +When the hash table is filled to 75%, its size is grown. When it's +emptied to 25%, its size is shrinked. Operations on it have a variable +complexity between constant and linear with low factor, and memory overhead +varies between 33% and 300%. + +Further, if the number of empty slots becomes too low (recall that linear probing +for an element not in the index stops at the first empty slot), the hash table +is rebuilt. The maximum *effective* load factor is 93%. + +Data in a HashIndex is always stored in little-endian format, which increases +efficiency for almost everyone, since basically no one uses big-endian processors +any more. + +The format is easy to read and write, because the buckets array has the same layout +in memory and on disk. Only the header formats differ. + +.. todo:: Describe HashHeader + Encryption ---------- @@ -862,6 +881,8 @@ which writes the integrity data to a separate ".integrity" file. Integrity errors result in deleting the affected index and rebuilding it. This logs a warning and increases the exit code to WARNING (1). +.. _integrity_repo: + .. rubric:: Repository index and hints The repository associates index and hints files with a transaction by including the From 89d8f54afb649124553fba3d9e1c34ca78a033f9 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 4 Jun 2017 18:17:38 +0200 Subject: [PATCH 0963/1387] docs: internals: edited obj graph related sections a bit --- docs/internals/data-structures.rst | 62 +++++++++++++++++++---------- docs/internals/object-graph.png | Bin 0 -> 320386 bytes docs/internals/object-graph.vsd | Bin 0 -> 150528 bytes docs/internals/security.rst | 2 + 4 files changed, 43 insertions(+), 21 deletions(-) create mode 100644 docs/internals/object-graph.png create mode 100644 docs/internals/object-graph.vsd diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 040fc512..7096614b 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -278,10 +278,21 @@ If the quota shall be enforced accurately in these cases, either - edit the msgpacked ``hints.N`` file (not recommended and thus not documented further). +The object graph +---------------- + +On top of the simple key-value store offered by the Repository_, +Borg builds a much more sophisticated data structure that is essentially +a completely encrypted object graph. Objects, such as archives_, are referenced +by their chunk ID, which is cryptographically derived from their contents. +More on how this helps security in :ref:`security_structural_auth`. + +.. figure:: object-graph.png + .. _manifest: The manifest ------------- +~~~~~~~~~~~~ The manifest is an object with an all-zero key that references all the archives. It contains: @@ -303,24 +314,33 @@ each time an archive is added, modified or deleted. .. _archive: Archives --------- +~~~~~~~~ -The archive metadata does not contain the file items directly. Only -references to other objects that contain that data. An archive is an -object that contains: +Each archive is an object referenced by the manifest. The archive object +itself does not store any of the data contained in the archive it describes. -* version -* name -* list of chunks containing item metadata (size: count * ~40B) -* cmdline -* hostname -* username -* time +Instead, it contains a list of chunks which form a msgpacked stream of items_. +The archive object itself further contains some metadata: + +* *version* +* *name*, which might differ from the name set in the manifest. + When :ref:`borg_check` rebuilds the manifest (e.g. if it was corrupted) and finds + more than one archive object with the same name, it adds a counter to the name + in the manifest, but leaves the *name* field of the archives as it was. +* *items*, a list of chunk IDs containing item metadata (size: count * ~31B) +* *cmdline*, the command line which was used to create the archive +* *hostname* +* *username* +* *time* and *time_end* are the start and end timestamps, respectively +* *comment*, a user-specified archive comment +* *chunker_params* are the :ref:`chunker-params ` used for creating the archive. + This is used by :ref:`borg_recreate` to determine whether a given archive needs rechunking. +* Some other pieces of information related to recreate. .. _archive_limitation: Note about archive limitations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +++++++++++++++++++++++++++++++ The archive is currently stored as a single object in the repository and thus limited in size to MAX_OBJECT_SIZE (20MiB). @@ -349,10 +369,10 @@ also :issue:`1452`. .. _item: Items ------ +~~~~~ -Each item represents a file, directory or other fs item and is stored as an -``item`` dictionary that contains: +Each item represents a file, directory or other file system item and is stored as a +dictionary created by the ``Item`` class that contains: * path * list of data chunks (size: count * ~40B) @@ -361,12 +381,12 @@ Each item represents a file, directory or other fs item and is stored as an * uid * gid * mode (item type + permissions) -* source (for links) -* rdev (for devices) +* source (for symlinks, and for hardlinks within one archive) +* rdev (for device files) * mtime, atime, ctime in nanoseconds * xattrs -* acl -* bsdfiles +* acl (various OS-dependent fields) +* bsdflags All items are serialized using msgpack and the resulting byte stream is fed into the same chunker algorithm as used for regular file data @@ -381,7 +401,7 @@ A chunk is stored as an object as well, of course. .. _chunker_details: Chunks ------- +~~~~~~ The |project_name| chunker uses a rolling hash computed by the Buzhash_ algorithm. It triggers (chunks) when the last HASH_MASK_BITS bits of the hash are zero, diff --git a/docs/internals/object-graph.png b/docs/internals/object-graph.png new file mode 100644 index 0000000000000000000000000000000000000000..6abfa4d345612e4bd72f81f004974f0a139c18f4 GIT binary patch literal 320386 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 z%0A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

RCao3Ve1Pp8uA8L3clv7?1LbU&&fmS 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

Tn2R`4`+dL!w^!@^$qnkZH;Wpx~8;l=zYxLHK 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 aJ@m^z=fC?n@Gbb?>Xp7L7+-Ju{{I7t#qEm# literal 0 HcmV?d00001 diff --git a/docs/internals/object-graph.vsd b/docs/internals/object-graph.vsd new file mode 100644 index 0000000000000000000000000000000000000000..3d74cbb8d228a000fa2d381a0b8663937340a624 GIT binary patch literal 150528 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�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 literal 0 HcmV?d00001 diff --git a/docs/internals/security.rst b/docs/internals/security.rst index 34621938..6535ef5b 100644 --- a/docs/internals/security.rst +++ b/docs/internals/security.rst @@ -37,6 +37,8 @@ Under these circumstances Borg guarantees that the attacker cannot The attacker can always impose a denial of service per definition (he could forbid connections to the repository, or delete it entirely). +.. _security_structural_auth: + Structural Authentication ------------------------- From 9174f846826a768a73c0c8f4e060152ab8f1ae58 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 4 Jun 2017 18:26:01 +0200 Subject: [PATCH 0964/1387] docs: internals: delete non-sequitor from repo/segments --- docs/internals/data-structures.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 7096614b..7c546cc8 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -79,10 +79,6 @@ strong hash or MAC. Segments ~~~~~~~~ -A |project_name| repository is a filesystem based transactional key/value -store. It makes extensive use of msgpack_ to store data and, unless -otherwise noted, data is stored in msgpack_ encoded files. - Objects referenced by a key are stored inline in files (`segments`) of approx. 500 MB size in numbered subdirectories of ``repo/data``. From a3815034e11144e56a40f0c7a9b6bce5f719882f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 4 Jun 2017 18:28:23 +0200 Subject: [PATCH 0965/1387] docs: internals: terms/glossary TODO --- docs/internals/data-structures.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 7c546cc8..b355efa3 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -6,6 +6,10 @@ Data structures and file formats ================================ +.. todo:: Clarify terms, perhaps create a glossary. + ID (client?) vs. key (repository?), + chunks (blob of data in repo?) vs. object (blob of data in repo, referred to from another object?), + .. _repository: Repository @@ -323,7 +327,7 @@ The archive object itself further contains some metadata: When :ref:`borg_check` rebuilds the manifest (e.g. if it was corrupted) and finds more than one archive object with the same name, it adds a counter to the name in the manifest, but leaves the *name* field of the archives as it was. -* *items*, a list of chunk IDs containing item metadata (size: count * ~31B) +* *items*, a list of chunk IDs containing item metadata (size: count * ~33B) * *cmdline*, the command line which was used to create the archive * *hostname* * *username* From daae1cc5152e780c92ef9314925306c162d4a3a7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 4 Jun 2017 22:23:54 +0200 Subject: [PATCH 0966/1387] docs/internals: layers image; Blimey! it's one pixel off! --- docs/internals/structure.png | Bin 201363 -> 201690 bytes docs/internals/structure.vsd | Bin 99840 -> 99840 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/internals/structure.png b/docs/internals/structure.png index e79e76c429b729aa5ff064d298a5574619e31933..69566003a83f678de6302cd42ce5650dccd4ffd3 100644 GIT binary patch literal 201690 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%0AZ`~W|2Mo}oFXeF2Dw!Oz* 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�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 MubfLiW90dN09eUJ>;M1& literal 201363 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≈6X#_AI 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 z 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 diff --git a/docs/internals/structure.vsd b/docs/internals/structure.vsd index b03a4f28f545c6a1bf0b16f1b4931088205a0370..3c7ce0cfd4c433268f255538619ade613e1ebea0 100644 GIT binary patch delta 25501 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 delta 25528 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 Date: Sun, 4 Jun 2017 22:37:36 +0200 Subject: [PATCH 0967/1387] docs: internals: edited HashIndex --- docs/internals/data-structures.rst | 32 ++++++++++++++++++----------- docs/internals/object-graph.png | Bin 320386 -> 320161 bytes docs/internals/object-graph.vsd | Bin 150528 -> 151040 bytes 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index b355efa3..785c095a 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -327,7 +327,7 @@ The archive object itself further contains some metadata: When :ref:`borg_check` rebuilds the manifest (e.g. if it was corrupted) and finds more than one archive object with the same name, it adds a counter to the name in the manifest, but leaves the *name* field of the archives as it was. -* *items*, a list of chunk IDs containing item metadata (size: count * ~33B) +* *items*, a list of chunk IDs containing item metadata (size: count * ~34B) * *cmdline*, the command line which was used to create the archive * *hostname* * *username* @@ -339,8 +339,7 @@ The archive object itself further contains some metadata: .. _archive_limitation: -Note about archive limitations -++++++++++++++++++++++++++++++ +.. rubric:: Note about archive limitations The archive is currently stored as a single object in the repository and thus limited in size to MAX_OBJECT_SIZE (20MiB). @@ -435,18 +434,16 @@ The cache The **files cache** is stored in ``cache/files`` and is used at backup time to quickly determine whether a given file is unchanged and we have all its chunks. -The files cache is in memory a key -> value mapping (a Python *dict*) and contains: +In memory, the files cache is a key -> value mapping (a Python *dict*) and contains: -* key: - - - full, absolute file path id_hash +* key: id_hash of the encoded, absolute file path * value: - file inode number - file size - file mtime_ns - - list of file content chunk id hashes - age (0 [newest], 1, 2, 3, ..., BORG_FILES_CACHE_TTL - 1) + - list of chunk ids representing the file's contents To determine whether a file has not changed, cached values are looked up via the key in the mapping and compared to the current file attribute values. @@ -572,8 +569,9 @@ HashIndex The chunks cache and the repository index are stored as hash tables, with only one slot per bucket, spreading hash collisions to the following buckets. As a consequence the hash is just a start position for a linear -search, and if the element is not in the table the index is linearly crossed -until an empty bucket is found. +search. If a key is looked up that is not in the table, then the hash table +is searched from the start position (the hash) until the first empty +bucket is reached. This particular mode of operation is open addressing with linear probing. @@ -582,14 +580,24 @@ emptied to 25%, its size is shrinked. Operations on it have a variable complexity between constant and linear with low factor, and memory overhead varies between 33% and 300%. -Further, if the number of empty slots becomes too low (recall that linear probing +If an element is deleted, and the slot behind the deleted element is not empty, +then the element will leave a tombstone, a bucket marked as deleted. Tombstones +are only removed by insertions using the tombstone's bucket, or by resizing +the table. They present the same load to the hash table as a real entry, +but do not count towards the regular load factor. + +Thus, if the number of empty slots becomes too low (recall that linear probing for an element not in the index stops at the first empty slot), the hash table -is rebuilt. The maximum *effective* load factor is 93%. +is rebuilt. The maximum *effective* load factor, i.e. including tombstones, is 93%. Data in a HashIndex is always stored in little-endian format, which increases efficiency for almost everyone, since basically no one uses big-endian processors any more. +HashIndex does not use a hashing function, because all keys (save manifest) are +outputs of a cryptographic hash or MAC and thus already have excellent distribution. +Thus, HashIndex simply uses the first 32 bits of the key as its "hash". + The format is easy to read and write, because the buckets array has the same layout in memory and on disk. Only the header formats differ. diff --git a/docs/internals/object-graph.png b/docs/internals/object-graph.png index 6abfa4d345612e4bd72f81f004974f0a139c18f4..8a153428cd1f5d895c814f5cac82c463ca13d436 100644 GIT binary patch literal 320161 zcmeFaXIPWj8aAqf0xHEW9UMo2ks`fG5$sfnTSN%O2q=LdAT1D(QD7V)Fv<`*G75-7 z5F-hp1zVz2l@cIyrW`?;TIy<_G3 zRhhbB(R(d;z3$>s*U(j~+z$)?TfH^i27I_ubnM`PlaU_d{hDRldizu_c1ql^JDlcX zw=s0fIaRLsv(`06ch^;Cs9xxJDYND~>OGr16^&_+4yv5leA4!Z;w?WMs1mtmrNW=F zaqJ&}LaX3i{Cicg@7@m#k3*H)r`@X9LnJCO+%yk6e?ml5#_RIxulrLhzwyPk3le%T4@q$MJ;uW{Aa{n3+D8*KS5<3<}^x#7as z{#e4!NF{x5dh=J_6Rfh)PGr3??(*ih9b4Y|mxO=ix}APmUFcru{*^)!D$`fasWEG< z#_qg8%b1R@)>0EZ?X;x1-Yu&=YLcxMT5=6a^?*9l4Ja{D?if7hnr zr9K)Uc$*{owdT=y>3_2-wM`c0o3P>jl&|#uBAJa%+nRj7PDnzR`y)g)I=|`VJvEa2 zN*}r$Kl;B}mDBez$nV`bo#?Oh{^L6TTZk;WgX1Ix$$fQT?~weTtm@XmxtW92eWi+D z88p(*{%;}D_Lv_{nxX&Iz7G=b|6*0&67N3>AKw!1KaA4f67N@lvu}y_tE}o<;{8V{ z_*>%rXAtSn+T>f}{YOa0?pwwCkFeRdiuWG@sy_n%w~F^4){x&S-hUR;KPJd;74KJB z)whcGA3^qSRP#T|f8VI)e^}G}L2P}an!n1b{%@k1)6NZclsJ}9D?A0CT5j77KMc3# zFbX0Z;s#mu|G_X@DT&@d;Nho~THlDF{+rx-IvR}_F`+f#oNQ%-Kfd6#RwPx~a(l}* zJKlK4-|NT0K~en=gOc>=oylR*;xGO)X>|JBTK9gSKAQjj$e*ucnMj$2TR$#GL zL(B=&|1Z~^8N81b^IRxr{0A6(P!hi9bf3EMnpOu;sDPKkv`2>8Zz_{!*F&TYcVQlBWT?h96LCxL(O^BMx z@weXzf73VxxN=N|dWrwP-R%J;{!R`jIxu%uPUb(hBy{-q;2uT=9x^+SkHM>S@QRS@ zA%{V}{QIg^(&5{;{`YiqR97XPo%k?m{m1J!16mi_P@H>D{qK8e^bnc=w<@?B?)6`( z@wC*{h4dJtxBJLi@JlB&col-Y;6Aqd@7DnhE?1}jj;T~$f)C}Jlz#kpeT^V1wTSub zkbA}dw(n@!qcyz^RI+PDyl~6^u^1~0Aj|2|gy6y*;6p1D@Tz&VD7X~!Z(p@0`X2E9 zuT7EoA3&o9p%#7jV?=#W2e!h5H)X)7;=%vzgvc z@&_9JmTtbKn{SxpFX-g|cuYbpipOYs<~Pg_=RiWy`OPZ>4Jg4@N|h$u8%kFCyb~)2 zfa_X+65{XwK!B@6j)4#R>20IK_`tWnt`-$`rkX?4GdPs+3{O&Q9#n>)xPF<5@mv!M z_1gpWGYt=j7yUyTX2O$aZc;7;r@!elwa zRwi=pgEy~`9zEBj-Vj}MeEUKjFx%fBu3&zC|;5QrDp z!_}t34TZ^WY%0IcAQNJ>y$8h$>7WSQz^rMrt7&sTYj-*kw)anQzCYJ^ENzSTC)D)` zkNyGNKUNIG;SNTGzzwMY>kEeiGi)mZEb+M9P&Gb}Ln|@HPCm#Jt2zb$bOjY{kz+!!(WKb_qA`haZ!0xStvTIQH3D2xI2lH^HA1Xp7!B-bGBD#zRSmn?f0pI?RmxkQOpRM)vM9=NizoyCJX^_`pP##H^hs_1a(|h-?U$VpD08E>*<5gk z)4~#P!C?4wime_nwM!R@Ino_}8m1Gl821A7j&+)dtzvm_zU(s*RH@m!e~TB9jpU8@s(+WYGDop&SbpU9C}jl^%O8Av z1yUCqaMWc`Z$%n?n>W&R-(_VgYU0w!m9a%WmqD^j;M{%OQxM5WwOo!wPQH7to}uSk z?~$`o{c4x_*)L`Ve9oFk>&)^C6Z+s7C`sL1NGU-}9#1YiX&tdOekg(dR5ZqiNr<0Q zQcJtAu}n`qqym1`pCp*59oUF)*~edGe7`8eb(GwxWbh#?S{XxcxXrr@uYJnO>^t}% z{6)+dH>YxPVU~S@w$C@Z9^W>EZ(y5!HX^H+K)&-@y#&-y&T3H}SVZ%H2d|7@&AS?s zN;4aB;C^1i)E~oMX_fJrUAUmb^v*%=>6+7Rw~wTXK216z8Ph!zAJ(a)xv2^2zyA@% zF~F_mz1z)Qf-}p<@|D;+pYPZx2kuBb^Yk~LkmsvJszB{+AnLCy!i!r3#iF98UR!7K zCq;XC?G^bhOIA#mVC`idrF608yJXmvYG_3&(_;*bP8S%MSoo)gNHGmzyM5oK^p?V> z&UsERO1#>E`cmY+=mOHrA@)6j2Iw|6{tEUm6@89`M=6u6iGt3&h%mp;8aevM6rEL~ zsPlJ39oYFgE#Hp7*zcWWRt)uXM%dJfXdB@^ixoZ`k7_*;{pge$L3TssBDP>|=~bq& z^aBb!ZfVx#Fo!8?# z3<}+c8jZhH6fdZkt$cms?XK)*Jbrj6=zEQxZNI0)T_5y5E<>o#)7gtKlTc5O4)C)k zQkQidoNb64aPvWDV`?&yqNM7kfGfYU=}WYc12PZsyd5oQF&B|23j|xQtV(f>jUoe* zf?XPXujEr0_{dv;ncNr8tXi6))YHuPjqTeHcr4%_;3pVj%|HPAf9q~?*mJJ9@VunOInOHc7}(NBTf zQFB8L{-SvUm9(s5DtcAk9xfAu6FHTkV;Z-hr_OfMGgJ6CZr>KQfXm4r;haDh(#fqw zuJ@A2cJ{n+II-XW@;>3y@>hwJ0wud1t9ln!CXDu@;8h3w`EO@*J|ux~2zz|`Zxa4E zOD`*9T=1M1gQts+#m;?rd-mt>QITIU+XFn6W~A0ePT)Yx2L5NLQ=c5hiplotSY_+1Q6eiFZ4&o zE$N^EZ8`&9Zg$Y5g(ff zzIaSB=J4Rm>)S_4dfLM88ZVn=Opo{CTm>7ssZ4##P02AU%S-Ql+FS?jyHqSc)U4z} zmwJEKDmAZpuj(6{g3PBH`Thd!pl`A!V#q_?xKkCq8=AwX{>COkfYu-Cb)or{PXRcX z5e6HJWNA@hD)sR#ll>!CeiUX;+hwcaws^Z9!qa?s`7W8>ZB3jFC~Up5dVKqyWLi}} zC+_-X#F{fD0TEP*C!xO5`M5f4O|IBg%edv3U!35KGKy{Y?xG`@vHl(VQv!3hAeF`@ z!O|+FQ8;FsWR`Rng^N=9D+*}bBy>pRniBl45K$T9Y+xh4slMI3iJ((z5i3eEV#qIO z>=j^N!J&ZoJjs~^W2(dvj|HddtsiwqYAeg$Ob#UN`RNwSGI{vw`Pyy+zbL3carWog zXDh{t!I-d2b(ZWwRp^rP}2tD0BsdLv>;?!@M8g^zztMP%I)fbK#uyuE* z*G8xqwJ7dRrxD602Z>pV^DmSa@%`660l|+-Y7ye+b_L6UW@%vIuRz$M_Utp1k%5Ew zl~w6mmoNLt8NAFuk}?z3J;0DcVyl3^9Pqn4-HY=h1x;DZj;?pW`%R}j2)KGtfUndJ zP-vFL1rd`%`1hLAoe-X$gv_0U;E+2&Q1@zmE74iM^5t^6Yuj~B^ml#99+&@y;o_OV zsI@#H4qcCg)Ah+11btbj?N97`1mK)n$cM=n=K};GV)YHT-sj_jK{(i@mh%vWdQ8{Z z`D#}}2%)UG#|a@7Pd7#=sHf9dd}o>d%3vbw_L+Ns<*w&}o3s*)$SDItkx&d&(xd4E zvJprE23QJ@_t39?{PVGkry$ya*xmEm%4I}f2~i)nvdkZ%epux(WPq_IPSygY{}ANn zDXteLdt@CG0mbMP(%ru!9@)Tbh+V1)Ep{)YqtLc+1-1r6`wJG6$@B%3_?iAlh>=j- z@wVVD^B0r7yvl^$Le8E<`hE^^91!9rw#HnIF?(-9t=W4?ZUxUWu!q*qKduDWzMcW&cADWT+N7Jg_yBXi>Q^3P<8ePA^ekrtrCHQ^y~xIbjxVo zE+Wwx0E})Wdx^%su$&N5(v@Nq_T~IX&l3>1(D5yrasi4_%16?+u?AdH2dBau1~smZb6cLuq_w}yb*qwnwV9DCk4n(c%Q z_EPx#MTz&o_Ju)6QL=Mm6KDO|<+-6$Qk<5AvW%@1v#qqb$Z6vG4(*2$?+=q~#hDh7 zA5>lz4@fDu!;g~4}|+(3NxLBK=1DGHw! zn1--Y(c38_e$rH}O3r0(A-#Eaa}x*a=&5GJvO=gA(mSn)u(#E)LS*TVPrnh`D)8Ru zTn||nB*2&w3KU^+f%I`7{g)Qlt!vBk-#K2OM*hQe=4wAF?A12Ylq5F*;a8R-tJ)6; z-e{CT@E6*WA6OGd3+WcNlU&FGo*az^wsJKnN34~#vNyf9J@|rTYR2DMEq0(FGAl>a zmC6g3)VLdLFvj@szJ1*Qkm$i$BhB?biN7&i7)jS#ST`t6<-+@S+!3bZhz-u`nZ;t> zw@*P(t?Tyk6WnU8?@R{iT$$+PxKE*WUkwQ@W!^wlY7`v#bRD5W_mRQnwPAIUn%dxP zD-g};k4z`;I7lpB0KqGApVB8!SuHvR%oZG}Yeb9ISpPD4kqx3%ZGsvA4YExs4YREi za&NijR|Z;-HU4T%M8WiuOifFh_;-%~4SR@!UFA{-=@ua%_I%b{L`>)>ZGKvt6;ZfH ziF0zNnNnq)1Hdi@I}K__LEUgQBu+U8s<-aQ2mPuB4z<)q#e zA_qse?-U57V7# z4XE+EtXuM7G*{&96f^NYw(q%3hhKX>=iQGB!60_DoXn$1 zbi*ilRPE@pQ;xG=psm-SBe*g7%H+rGed*R^fU|>`$u}u9OY)yF^kd;6jLW@No{_o+ z)ipD#kE#&_Hku_hcLHp$2G#ylG#OOkNgCLl84Yg9$V`^Kgi#90VEu68lYa=eiUwgA z(|bFss=fta>CeDLN4vzD7&ZHJ?v?bXYVmI=2~31wLg(?yYSvSRTKgXh+)VUXfT z7i2Dus*JxWq~CqHm9O&@;WB4pFqaz*UGNQ&v_u`~Fu_SVjvfGuX$U@_LDxm&AT7w$ zfxqx^A0mVrPYC|l|NSQ#f6-{w@}$?uZQ@3JBQPqIRFjxz8@Rs}BDp!U71^|mOx<$uvsQsIs0r&S>h2o6qq|;X;{Xte zES+|1R=NEa7zIH3=8npK5|~r!>i8Ck@3KVQ%|#}hE0p&~Uew7H0Iufc(-+7}& z%rkCM@_hYGjlYWIZYvPUbX69CG~KeYz^*8nE-=|5J@UNHf3eS5S%A)9zhVLi6_#hNz<)FtYG{P!J{TTRQX*RxK5A_hotalbVBuURj!Za9*q zZp7m{Uz`FVI?rHagL`<_?P4cH&xoFmFYj$B<-P%^zQy))R?s?|>KpX6+C=Ez5z#Lp zdC|&dU&K%(lvv%RAzo0!q%D8X(7?&XI|Da6QssP;gH?W~<+n+%6Gj*b6_(_(l^_c7L_-;RHptfSX@5sf6 zL5DkZ-ag^MVOcgtwX8K3wF^`bW>o?}y|#~&Y5L6Y*~7E-RiLnzLd{Aj>pUGlGqjHn zkeVk$q7&ZrEF>@aZ!!NhFd^M&eH@t_@s^f$ed` z3r(?+sY7%n;1xvL)4VVxTAtHHrND1-RQa=4;8z!(EIcWyYapa8EA$%C>k2%Ofn$=h z3O2;a1Nv3lSw{SXkd;NNTK(G}&L}V1UJapG(2vPlR<>06ty6+@m_>*P%I2@n}#elp8U|z5r7IF|2%x z<6Cs>XZjwBp<3v&Mt4hEa^<<#oW?SHEkh-liPP1@f-^}ysgb8cEiA5vtSIcA{7_br zVS`h5G#|dSH2Au7 zcg$pvdfaRHxM3t}o>Fon8f+`*Wdnz1+K>=JE!|r&_Qm=hT#X+`jB8gyGQ2OQ`>P<% zP+F&^E!C-4CEA}RZn99^^31%#rA`}>Y=lEUTNvJd&J`I|Sw5LoR3W-)u}+_T0YZw5 zTUEVE-NPB4KgbDo zk2SoSnfH9q7eXq#zc_UAn)&u7CjKCmcL7QTX{j6IIs2Ei1cs!j$$J52iWDiZjB}8p zwwZV#E2Qa~5|3<%Pl)uuw{51zUy6#O03f_sisZWDs`{?FO( z&&sn1U2A)wHT*-!^(?1a-?1Cm(+>BcV>%qQ ziY-m$IPQ_j3ueghRtp2?V*hDQ}<-HY)2g&PAqs+{k?k$C3WVNSxlMP8qLlT0g! zvEKm5|Jetku>uYOLN~TpnFr3srR2njM!*eUPSXe5O%%e0EVWVFTT~Hv9wSvak5I%e)%$kTcusOV#|7(?mf?*o* z)Bz16#^dcf=_QT@`3qXQIZ4(=2)Z#83eEFqh>6<#rj<}Y&K>|1WI(s@_P$kzMm?#*YfZtcTOlNZ*=PKWetYf2Ftg%i~-5H(mnQI2lbM2+#$ z_L|huibSW0d6UD7D}^8YV~A=VM#HKW*M}A(&iC4;+!TLR)t-l+V{fqyT)EM_smXk} z(r=xJ&Z(y;YS^>+SRoE4H^Kdmlyk*hv^B`!eC#h73xMkX|T39Z7^Lx{-%64*!dx%^=7 zgN@Qt_yM##<*Wm(Yj)q#2+kq!Ukpu3=PAKC^9~EutAAAL_j|1Z3+dHTvZkaF(^UJY ze0tp;3k-GfiEAWh;di(4xZ_YYLK`fS>ggpbSE6$`k-Rd`ID@?-6p}+$qJ_lOJ85ko%2%Q0WkGjEib*y(o=Xc~ zI(V^SK6DD&{@YMJyfs^%jj;Z?QW75Xm?I7gKmOo@xWGqVkc#1Wolj1{ugqOnN39m5 z4~7dCO}k~;*SrFzx9GhCkkimO*^Ya99Ka#^#H4nRU>fDOWsF&&gXGQUsgun z2-~*~3meMrBMCQ%pjTH$st|~`Y9hD;qs*Qf6I zw9(7)!UiYPO?Evqyw%IhC%nLR`1@6_rHT#cCmiwS-RKT zUnymQD?=Od?N7(vRnp=0QLm~~^IA`%R}|NGM?fRUrn?@QJwm}^8QY5YGYCBu;=4+; zLWTR?)?`1+3}d4vN&`_nD}rCQMW;dI53daA%&LPHT~ zlNl*Z>3;e7K@m<^5N<=FX~{H@ANSuH&$bU3iDzXFS-Yscihq$%~6`?2wh3Cu*^-4SlFo&hgUzBG;vNOQB1xmF#*pN>rSQ zb#X&<;f0cKP6(0Th4QqJ-;%MnizX*9*nPU19DaH@ykN`gX`P``xSbSA2TPPX#(`5! z6S3EvG5vK|#il^4SbN2NO4*+>Qe=0t31)`(!iZJdn+zc53`H$a3zuq80-nF#d%W`Y z{jrOY-L0Ww`3tx0*y+}lh zK?@nY^s|f z8Dr>{Ca6WaJQgoLGATI1Enc&s3yU3!qgK6r!+IP%G0?E*%vc{@J6-pH8jAPJXpqf8tt{|5Lm_jV|YD#C)&lvp9-Qqe%>GIMcnDs}|S+DQ* za%~Igk;b%b>54ThS#H+UWu%=N!Tf2*E5q3(3xjl;k9UhH%pBtTTORv1cjt0?!+K^$ zTuUH&gEet>200Si!*vw`rdkIzqcO%T^X_iHcaEINBI@&~h+LdT@Upp2^x6K__H;f* zp(zll$E3&jYhof9^jNJ*NsiAYXk6I|YPk)ub=F#tt&cWW58K+L7=e8ieo6gO-jMOg zh6rs%CC}-euFkL)yIytJV>mfw?QCCzhwIv3SM?}aX&J-`c%0#QdUfG^s}bXeyq@>Y zsm62a?{DV6}v|?U#HKf*e%X)dpclTHpwHfl#M`_ZC;j1A6 zJj$*Ix73$8dcMV&y@5#%84ft&Ql5z4OLdxXI&X>>EV7*Y$voDZ8VJz6sSxwcndlE_ zEqSP6QcYs(2o9D2f&wg**||*5kIs*_a2TTh&}Nif(3$2O)2lYn5vC=(V!>=aT1rWs zdv|);Wlc@D^2&CS@)i$=b=w<|vt!y-a-kj(9<0s>u+Ciz!2<<_VPp#<_Sk*OH zR#BpOp+#o+y$C+w__I%TL$*{8zXoQJ&THJ=RCy+Acs_8T%8ayDnY_HPs zy!ORyD=UlfVN}$MO79+Q(ku%1#8CRl?}n3GXp!o46W-gUOeaJKzthWK5Y`$ey;feU z>-$)0zT#e`&EzYcS~u-U-@p1bBw*YvLZg&%%hY;g8N zZI95hp(;?OF8IWZx=4ZKNZr5;&ttclx0@SvSYQ;h=R;dGmSOF-Ng75hZCL(u={&Xp z+iu-;rh@a6STRQ&j!efx_IZCGQR(CKs)Zj?05bBgW8$kY}o=7qj^ zlg@DDtn`Fcu^axSW`qwtu43#n{s^v;E&zr1RllSLi9YunXia{AHf$VY{I2IxtilC! zXeA1U)@D^Gt`|+6-(X&8?yVK(p5n@T1JewXuWutRo)Ud1ehyKE>%PfqMDi#4-%H;t zRkmhvYeQKMQ)1#aQF3o~pwwSBdK9|9bsxHWC-|IaE1T?r$5h7G?`Xtr-rH=G~R)T-~l{^$}o8Ih9!jYjR8|lGBe~aOG zH;w!7xWp&BQ3JcS_Wz490%?*khvMuFq@9z@q{ z#ni!ZsQ4wxabe!>XIu`xEA|CongAwBli}wN*uUhgDJyJqB;#ogmfE&X*YI(3%1bur+HpgsuR=#uTd{<) z|70%Agw?*DX1I*AR*Q&fHHP5&K$8@ltl^ka^Fk&vL$spRbGt2EMzZTIe{y5#@wKb+ zI9UccmPWA+^^+^Lz8#9|6AnS0uOx6n2MR-L8m8ZOPS2*x9;)><3S547I8;lf>e_TO zcDladl@B91OPx|1ez^i3x9en2{;# zTDsLSdNF?mJHLWK{|ebg_K z3OQFpD#CnkoVA-#)T_m+3ltpkx`>MFNtXg-LiFEPHut_NYk~Q$Mv%fs^i(X2oX4d& z7={_Y4Pahu?}-S552GC={UXHRr;8xc0|NdD7PgZmy`F~WT(d&}S<(XqL3O1@pE$lo zs|gTBn~hBOq|6Z@(WL_IxaU84s0f>@{ zmoR_9rnO^bD-wU|fbyuZN2>|Uld2ws7<`!%vuhgQej+N7krD>tZXY-l}3IMyoxdD&vOUGFxj zi4E6_(RtGx_lNu=AcGPsehXpzj;in9(U))999GN4piiiywR!`%wqsS{0u+kY6AK+% z`n7WCCA+y6@guS1zK2A*yRiV@Vg-;&;C3)C+%QjW-DGq`L0b036DzDxkUkdtT+6$= zK1=fB#8yU7q@9}4{&vX(vTlv$p00U>m;yUmw_HZ?5?E5}w(t-9Eyh0)0& zO3wZrCZ0hGxBcC@+gJBvO)+%?;23BAM1L*6!Y}!)|MjJQcs-Qj1t60R5%N<+bgW0} zx_ojtx|hT==_q;-!0@a(YQpl+t?rpPB%nXy#?&kR=wPBcfpo+q81RDtLkWMu^E`y` zy`Z!uRtYBwr537JDb=DQQO7ew?5rYNdPZwR^UUNgt^@vJh3LexO%0X<&&MRN3SG~` zWH~xfqt`d8qZeY(<`xCos$qtVPVHPfS(^deMAh8|tP4ZqyQ6Y-9#=vuH~QS^LVnNDH5&SSvfLr9G~SZ7!)H%yy0?QwSzDc4}n zgYH<)D~*>NZ4+MW<=zG<;-yvt%HvnjwQcdW)}Vk4t$mVFs+T0iVZ1VMD=5cLH>7>Y ztn3JDHRqeMM+)hc0)~-Goui{xblczq1XiEyC21-qvwZ>(on{Y zS6LHF^&aCZ?B6M1Q4T#j4hE7CqthfXWy3Rg^J@0QSU?Wv1EMY{)eF&S+TFiQoY9CJ zZ49VYE~Qq$$Trh`I^IpZDyYi?h*|`lCCFTem?fAa7Rr+Im^F%L6&G|za+hRkvEhn+ zLq%{=HdZ@88>Nt^yIki-k33P~!>)p>K=d$y!<*Q>^z%(S19t4Fi0uTOW1iv0I8&`8 z*x4~dIw@{t229HTD`-G@a=O*fjPhknR}J);5cBwwt06S$QaM%iIR8g`(~b3SAD22n zHhJ7|Ugb_QGQ$?Xk8Z05Eb zj0i`174a!BI5o;On%7aSbU}={;QMnx``zf#_7^3*g~c$~!ax-XMLks-cg7+^wt2*q zi_X?S<(m~9u0(Abzg}s`x?W(zo>;Fct>~GU5jDz(JxPZ?CWi-sW)7+$wKUE}nG%D4 zM42yw&1Mey1W|UuPCA-%%pfV1r=$rPrca$=X~+Bc)C_4Y`h)tsdbb0*%|Q zVjzxx&qnmuRELp3&=GRAQhKXaDC=W{w#^q-*H z11nd)WaN;z{KHvl{o@A9*HNSK;blRkSt(~d-#tFr`F62nzJtKAjE|1(H{ZGk!Sfzn z<-bmk8P2hxj=REE%2A%@Uc3aS$IV1mv6@|V#doEpdX@v10XaMMe{r3 z?r6@;bh{;J!j1{Jq!l+{u=B7MkFW{Tj7;?iK~@N)eKrxjRQk5q*96Mi5RBN&I^>q9 zzU$;R_I1vvnW~wkpIml-Sipk^OC@h>eI3l>TIUxaPl8w#vZj6kd}9koSbu5NHB-q1 zE1I4guXQU;DpP7LzSg?Ho{_P?T~SWVhLpNcHc8d*L!V_(r6kaj5UJL8X5r%p%OT`O zC&z3?>bg=MR785-u|Ff7no=G|opUh9SNRbq%rYKPyqhowoQRKjKm z&j{~#yk`nh))8Cl4{n`5y}?1hGXn5iAEHQm&0N0k=7A5G^VNPYpC582K4S&!dV z!u;;tY(Q0Lrqd=EuPVz?b>mZg4Lfd39U@ku+XR5hjvbW=zaT;8)=bRmH^Up8zR#^~ zGkCn2qS1UBYXnZNH}!W}Esp-KuzXU`)z!7fs6e4E!z{A2=uC#c3e^d_53A!cmWlHW z_}n)7W0sA&2?Y=lueG|eSPmq3#T9apy{%1&E01ICsZ&9qqlT;Y+l*b>~~+N(T5c3sa#19eucy z?ZFfEVRyYe=0Ps94SQrL=mTxTj;Ws6JA>_B1sn%e!BulW(1soN>9`(y|}Y{Y>;?>Y>PYaJp7Qj-q=Rzq<4>9sO`C4oLqi*N;8&Ol<0`* z$uMDak<~p4ei`f2H727BXH+H^mBPksx_Yq03`~zv^NXQst@NBD%E@9VW8ne4Fi=!Z z=jar0ZP3M6^#mERZ?Qal_*&;AeCd+FSUad4nG+IgvWITE`*e>KF0Wl8I#%<|wrsNs z!916CfKcc`sj@dKNZXv4_lD1_=@S?f52%a2%Oz-E%$-`U^3YRFmtV@8Uf#{S;Gg-h z|N5Sz`+de|*2a{Kd(uLh!f5%0uS#cw{5!A_$8XivlUV$NeV!W~B#YXBQMNmyT_FJy z^?q#_Tv7&y-!>Gg{~%gLkMes@aiAERlB(>uDLGFbo42qmoJ+L4U?MMdd}Xv4lj z59wA98uflDipxGE=!2`-<(|bw*_%n24xb5ZK{|Vy8Jx_qQ8D5L`)BG}zu};Q8ulY& zHE(SGCD1H;sHooF23Bm2fYBh<%cT~H9!6Emts%6Xed&Ewlvnf7WChiBeqdYdBmU0k zEl$vqf=!lx+3$poEu*sM<%79)QnX@>%JZe zLw*rtO@$^$(x&64#y76l#m@Y=L+IszVYZH8 z;VQLWuav%Rv1KE~4zWbB#clp3djbprwQkUS&aAFy+vb3FQ(7*Z*LsWQ!d1sz4bS5j zdBi=kd6z;7AJN_|V?yR;dZ1k=(y4g!Vt3W|n_r|XU5e;aAhM8AJph}H}da8`j3U(`UP_0BVYcNfO6d^@Qir4rq zD|);-Y}jCsm#sO}E5qg6@Q^M!6!YKJ2l|dB z*Fw(*p)DlQ2sb(AWa$QkrF$<-0I=j1=0HL^Um7 z05%7k6@t_G@Ko9|z|?JGmQ8i!4%7=!WhR4n!9V)&9_Do)gg=CZxzX~>PoHv!*Wghh z9l|#?N~g`L8n!DVtNbp>O8&?MdjP=KH!(QPCA_l-177t)ZbuH(oM}yJ7 zKCFwh7wnHvdu05GuC8Z;2ZIpq2tYUPO+&_NMX3i+v5;uqtMnSp$&&G*MN8%ecl&}q zcECymD3D5vyTz9dd;Y$>X+UX;y4bMS-R%86Hd5cNocfG?*G1S#(**BA_R`|?j%uCf6WPMCU*GYTX5yJ=Jz-OsFdb}@F z+cKHLM;*mjhT0I{-e-Gz>NWRtet1Wul^!6Ma@lfvq1N)D135LCrP9wbK>jEk^o?6k z(&w5)w`anbou*nBLRxspz*tJipVRlpgj^L1bZK&BShAaZ#}^b*L#gVxqs7&LS0aZ$ zZ+99*wArCiG_yT*+gN!}mcGr~m!*aDo?+~&i^r{Wkg@p(GIiwx1a$@(uOlWD3mY2X zw*hV1!)PGQy&ktc-h(fV2xu<~9q$<~#T9x?F*;3`jmX`v3@H|lCX|VXI>p`Uu*h-v zFz}w10y8Dk!}>HD-{;A`;8Nw+*ReYJa-ylT5o?!jYpGk>oJw4HZ2&<)NIezU$&Kvs zK9&x(1m$@e(QUqGWB)j;77eXWoel50gIa|D^b%EJ>3O(w7dfzubYIy%<+iVlC*pjZ z$=T71AJW7~5ol5oV6Sm^e0>H1vsMD=DBoGHiJPj9rj|eB6=J%XUhS(yprYOD0U(+h z&iu=PiN?)Bi;j47P>sO$cI+7iW;^qsp_$XEGStbhA{4^OxjMLhhp6h@smp@J#{pwk zSiA8)*8l(zY1Yx(q#2x?Q60CxJfTyx$gt z`3l4KrcKcrZvRwIl&-5hbnE`!4<>ums03xy+w#+XxaCOuEpJR2ZPP1p zr4C^D9!k|{MJBaf5m@)Y+RSLpKd+!`3dAI6@myYXA|3~=ZQwXaKB|Im=U$I9VVw;yoAdtBN$soiZDwdeB6Fq4MGDoAmO!PY(q$o@+kYUtj&9}CC7X&=TQ=&ogXSWiN7%%WxG0pTG zxjBCiBYoKW&Eeo=E0k4dBe*H|%C0w+7m#SfMpFw4XcBpTfHpd8RRNDq?3NYuyHIs- zr=(ilX#;p)-V1fl-{HtEYHBA(skXHhuN9^>5H?_T4jc|wr>0<@^ufA+ll$NxdqmiL zhia#+19jMf?@ovE6UVjts*crLz9L9Mu-Z9+=fw^WktpaSox-JQ6xXf2lR1l8T>~Z9 z&dNmx1ZmgqRWaA!d(qRgbjEW!|weDlaqde z`8^wx*HdpE)gzvqd+awCjP3=;Lq>lKE^;A)dEcel=W}(AlE3A(5BIf%m@?}w{gTuP zMsXb(n2bI_@A%Vn4sj;m*hO}IM_#)|Oe{igYg*oxrj)>mK5Y6eH_T0S+$r)nTD|Im zwIB3xqt4RU`tMWwC(>gL@lQr`LqA|q*F))*-qU+t-CR|~Ra7_h$F#uSh(>bC-5jGJbi-x;IIDxk9Ka8d z2%(In%Gg;y6;F|1o#Ol$$*4c6-c4V2auHX-t9SKovVa5(6%?ogG!xe_g{9ca1KL5p z02hqdSa84eZC+$}$Ktvv+ZFi=w;wMW;R(SWvMhtvf(^>ZxZJ1YhG!-hb4zkpo{5LYv+X(2E|z^eIh~<)K|L{~kR4`cVoE)`W$uRfAu>n)_IT$W;n4C>_v4ws`$;q*| z2U1=^=oVGpLN53Fz?We@Rehc@5-l4jvB5)WD`TDnBCme1YT!F1GM*9@;OcfcsxeQ% z>}jlI%}*))dRW}0Twq&~yEgznpsTjyQtg@WZIP65CtL`racJ~CY3#tK5b7TP?zG3# zWO_c{;!rzhh*PRXLbXnA0L*RfeNm$Nk^Pg#y8UiAG10S+r}oR9cwq0I-mlT(ovWQU z{IYJ4&bZd^#P4u9DlA?_I_Y!xn%}v>59qr{bo|qK!R|ancBkL%jV8|P?>wuv5Xp>6 z(B<+K>+nRS6mZ(zLZeUZ9`PMWyaLUJ0&=1cFJIlA(ZI8hO9^_$)hxTSqMES6*?sT( z>T4%58+x)K{~ewXLLGPyJQH-T;XzxQMX7o4)sf=Sci|Fjy^{ao{9ya(>>mLzTT{pu z#C}DOEi0y3z5GA#CZf|Kg-A!g)H=93UoxrQ-V7K^}mq`#IsH4Yq;BvOa@CPKfSpxxZXOr zXtz-*hgB0jAy1t4^|L5VdttDt><-?`xCsA< zu0i4KBu@`b3JzKA^LLmEh>NO>cM{bN&C1!4wxciMn4X(y)j#>itFp(dCv=9T#|6my zYkoHWkeQT6E6QrGqc)5Xv802+w0(HV|D*^Xo| zaB6Sy3id(y^`u9=;*}w0rzR`2o5vripm};un5Q3R$CJE7uxbe7B)IVCl>*e-vdU(LCq@%?a7Cla#ZoXp?4>NAG5+8_L<*v|gO_w_hw5CnxKwuxdBtA* z*OUs+UKgibzJTMy_1e)}{OCl2YM-=|iFwxtq(dsQ z3RPbsw^*JdG|#Z_N&PPY%7)ra%_^&*l_Q-0b$LmzwoPk3V^zELUu2pSUtX)3C0ES+ z5Fa7}>>+WU5Eb-vLi1n?2*CHy#Pktcj46YEcEp4L$`D;ntUwxmy0swh!tQj-w6~7@ ze9e_wLwMz5@J7;P(OterhckIa71x*2w7Xnx&u0msOgNPfKIZ@_vM=Zv^8^Kc=oL4; zMwPZ(0Mi^(keDCkg*o&Bwl%ze^Y5-JxED!L@N+|R{2yP#ko&r}zR$nzc9`HWKT{!5 z0UdSfFM{CBP3s~bvdo{eOq?S&bVY1Py>0!^u0?}|%n9FT^)Ot2;-{>K@9IZ-d9u_! zZLmiu?H|cw(?>2@N9Pf}a*`S$5ZdRQ(3S&;2L_By({68i>kd6|ZhHD;#Nt?JWg}Ov z22A*4-)c%u=u5KjoI)6UaVxD{;e#yL6frmW%CV6Wh4+srXy7a;_&4&aALm^1FHeCQ zfGO~uJTfNNeGwwn)RuDO3L6uTWhZ%^wI>Y*ySV{YOBYZ%MJ=vk{Cw!Zrfn6~yf+;! zhVn+6^LlTufNnlYx{4Nkhw1lJmFe4UFWlcJRnn;q)1g`Uy`-Wk;z3E;f4Voy`SgZD zJB>kge@-p*3D+gi(_Xt3GxQ+@%$x$%k#eyL+%Jq)U)Z+K2kdD2y9yUHFpQzYu)Tze z?S!MG3Zia0oE-bA?wg1WNTU7HgnrGeb?VZ@^;|S4xnj=!!`nKKL)j+Hxx<#nJFoN{ zY^CAlZo6q{>pM@k=2tb7dd_q!|I#8WM-ljmRNcmti#xXd09|rbj9%{6Zmrd30Lc<^ ziad1Cp0+0^BtJ@0oNTU@jj3?XHgWMG(Qn+!KAdBFy>m}x4sifLuNLma2BA5a#LHXo z1w%sr0^G=iXnlf3b8&)ZpR!$tUgJU|CjwKNFz;?7snHHj<4t#CR;AjKgcy83)ji;S0=;7RgAV@5$07ZE_UP)! z;ClD%R*Ii@r{!Or+?XQg8-0UxFmk?^8IDN^fwv9@-akq%gg%)EXbQURq&V5PSDdpA zcg8G!799_L5&&cn%CEBpVWl1MS2_HisAX=@?fEX$2RxMWXl&slwS?o`FZ922CLTO_ zI4Q|1FUY2&^1Soqs9FQ<%zZmhoxEsHo*aGwz=@QX?=jvBQt-5h_9|Jrv&+ z9X4NNhM6t|*M_H7<&Pu3&lr^XzLW%FOkO+y4+JF0;kD)Gp<@aZfvQa7VhGn=zNO*& z9IwPnr7uTMKHGmi6m-MrL7~g$4nv!!MW$W%2p`a9K zb8Pjr;%etTTx42Sd-KVQ{C%AHM4R*B=hN!Jo=p2_UzE(XymX{N{bSeJz9;9>sn#&2 zx;a5#wtuzSWc7Sy3kWKMe7O2Et$!N8)e`XVyFMMasha6qpc68c{?Q?q&*7w3pH~%2 zX;C%VcdPf}^h#$qyn&d^^*}ZJEz)qsW5F_!@624MCNeR80%ga!p)0Ypx8dnp_dLge ze^Pd;G2ZcH@zlr_CiPmJ{lTk&M6EtJlIynH(FMj|FzU9>>`l*%JR(~!K67>L_h306 z+$29B|2XL8j-Hf6j7r!H)Hr?8X#6hF<^C^arXI z&BxO5A8Yb>q20(J17mVeWV}H8&;y~A%p=gzy}dBn)ID5o5DNJ;M@ zv0?mj0FF7USs?6R6|@Er5prBOnoj3J^}xGcH~add3bu>~0vZN6j_(8py%O6)4i2xm zfyD>-XTx3mE`~`2kcn{ z5e_m57vjy^>dsUGSAko~NDs8+_Q95AiPk4V>26-Lp&1hjZ$^XVtuS3J+3J8-*)Z%C zahb%GUhyiuTFmr}qy6om6VR`>dv^|;$h5$*FYUEw)%!?Fr`d;A>5o+PtsZ(%yM5|8 z?);he0=ds)F=Pa!(*izhS4i~^LGWwHsG%up2e%Xe-Ww@Uc)PGWnV>vNAQ!fL$>Dk) zJ)g<`wK$%ND-EO6?=D2KJ9%k>69s-h$8Iw9q8Jc~A za&3^-pFs@)&hVEJ^E>c^ZpNv1`-)dIl_Ec8?P(Z$Fg+!fZaJxz@;IvY=ZJ<<8@(VwqAMBmJ*90;7}F~n{EPgI2z%pG`0VNa6*71j5< zR(Xy|=%mtG6>WRbec#n6O1zW(8qj2cW8`@ zq$o1Z=X9!9!sm~b*FUvrox|-k#fbT7TZyIEQ|QxI;w0<%2_Zjup6`d;7n8Q>zwI7rXf|mN-S}SQ;l8W5aQ`o60w*>u&<|a2KD3g{NqBKG_2U18 zi3Nxt;0)J>Mca3_kkY53W&6`eByeFodAB;L71_QDle=jXtbIRVycyw@x<~eB;sukt zMou)%%J7Ud|KG6`Q}?l5&L%uCi-Xa4o{D=9&sBl{^kk%p+qsq>a5CFJSLYF_?exp# zSvj>yi^PZjS5Rxg`g`N6@1RQ-Lok+mqfh7f{Ec9KIDJ@p>JdyAEW3^{WP1YsDY2&A z$Z#ls!^7`Ob9LSG#$R(0d!2}GoNeulZGEfNS%4a=3CUKAQqdsKRQfe{uH(M!J*9|$CXqy)=gA$JKM(%0cvfC@VVY|O@Tp| zv5nY^oP9Bt2Dh9lr=HN0C$lo~53}_9ZLiN`S}-FCT?FCf#lM}&%kvvRF3(FX7Gc~n zp>ND*KKw7E2D&j%>;W`0mEtYdO%ct9?7F2C13@#C z30qeGA;QA5&t5?%jR;;oj1~mXt^+ivzBF39POdt9Uzs&PF0d8PN|xsHIaBVZ#zd~W zUznoC(b%8Ih_}627uZXC@7!@yNu3vgY#Am6Ie0o{n1r{-lAS|`7u%@+8*6~#H$|Ca zg((E(Vk^W#cbd5IQ<~A`<{IY?k#p1)s2G>jtM^k?;4c_EgGvKZdJd>AUI|do zvgtuK*zj!A;FEXr8XS7D6XYFFwZ}wPX-){JiMzj{m?1$>$gRKBOEK?jDoeLm=i;#{`_QhO5 znaQLJ3t{FFTzsu(KSCjShDYS(-(ol~lL(I`1NlFeJYdt1fC_DGyp+w5nB=O*YDQIo z(ixUFDbgvB&~t7F^rHrQiw{o+@6f?r z)+|aM64?rHs@CqJ04FAYJw{k9KNj64S=k`ODURaR^OOZ}X&B0appJh~giD7ORSN@N zvW&t<1FDj)d@m~?NXLgBPaV5m0BX=X)WQL-tr%2d!O+#?ts z$e~Z{n%Xz+n3p#MEM6;uwZ?^~30Bp%2F{@!GS8Tbpz`wMhv)%UL8=gV@5C4$GtK%L0>s_N>-+Nzv8BhJ>?sQ$dcmeHJ8G zqC3GKY4Fhvkim13p_ zsxpVY^i~#2Gsa|R;O=OV-0ihpz$v2<GSq*qaFW)i;loWvi+4_xK8QR;l^^6R3i$HVe*C&Hs*tDL)) zm8tnGg$gM@z&OLioluP4gn|cVt51&Cz^Bw44-hmSVa^3lU=BX-Ci@R(8+huE6r}Wv z32ZB+Q*AkG4i*|U@wK`!c6;FBwIzy@Ck>Iw zLHGYV7+wUAc7#0(AkXMM+Lk40O6s`Pj(}1o`5^m`{NEK@$h&sQEE4|}Y_=%0+E4Nu zk$ny7astAMcxxlwZ$cWbnc%N0QS~ql<~TEu4^e$5cb$F3Jw~gq%6Fe})qA%dCgaye zr=FXQMUVo9V;6(iILEpQ7>qMP8^K!kNsnJp?9w^`Uvahk%_HVwgPCg>jaf-JwoQdy zUTQs|VMrCX`ihFn=v$#=i>iA<)4eTiYzrXbGp-w&L;$I5X=0BH|A`gMjGDEjR4+Ym z;yD0w&*m5lgb`P+mg(4E;#|hx0%=kNA1aiLpB$$(ugU>{NWkYm4g9wtP|Tm|fGjwH zk>?cK>Z3qy!2h2`$MfBe6z1^S6BgT_&b~x5hR|sFnRL#Pp;z_OQzi920JD~0H*X#3 zNa-dru0|K%)u+nMpNL5z)kQwF4+0TyeAJNI$N;S3#soSg2Jp4#z5u09Wn-lhlLU zA)=C-vg<_WSX4)_%-Xs4sn)PRZyI|qN$hE!!U!_~%*1dLKNrwZkGSMvARj$}RP2>G z;EJc@`#MFic&{^$m;l@=(pjl3%-dy+=!b9ER80$ZgiTL(cbCeZ_(@oMey3Z+IkuWU z$1c9}^r{N>r5}Q%>>;ZUY?z-uo?+ry_F?W`YsKYdX~*d^sez<+29h;I$Z=xSx)w(J zaidd={u?$kyF2m@OlQLR-; zJ}TpLSkGKSRvSP6yx7XjGd&^*PqUSs);VwH_bi}l@8ois7#$b}oU;$m|By0%TfkLdyx*0S-1b%V4jgNjxjD%2fKO~D&d3Sf2U6(B_1H zUpIJ!u8&EM3s3^x14$`~<{gYQOBYrJsA|Cu>jp`uH#4ZJOrLb=FV})>1gwN-G|ilw z!&ABF(6AWgUsvnF2u^Tt8qOyMFo2{q0mhk8?E|a&B${N*ad1Q)aL}<~f7;4-p{izmvqop%Rn5WFrTHa%K6X(QI^ z5K`8k;2>;1Gg|MzOu{r0|78+Z{+J=jf0=|yhlOAM%OwAwW)jLEu?QAmoq$+$ic|&T zF>X%{np|r$G%TC*CL2i2u8iXvoYIZMF9cGcE9*V^u+!cLRNJS-sy)mdD+}ND072II z3CTZMm1@9~H{)q7{4JDkl)=*&>ME+X9)MlQA4Zzp+_G#tDxV?7ZuRFu0ID*kkN8 z`1#Fm&!+hCy$4-6+eFhHv{Za^HL4c5%mf>7(4Ypf)X*^*mAQ4SwQ0A{wO&|*?ChR_ zU)i{JLdj)Rvrh5@_B9N;+l;pdTOJva8^_z(8E?p){l-;`44@0#(L$X`(ZD>2(4?pH!u*7aL#y2{+6vq~0 zESq#+BqX8amr%kXA5wJ{wDseQ4NSvxPK2`GPG~63IL9!<~3zlt6j5wnm08uR24&$TSeJ0s>c|5IoN?$ zUfWg~v(>GeWkd#jsvoq>P(pFZZ5{<_3cD9)BxlHt5=crn?~kU?%+}Zhw;hoh0nWPa zD%4~!1#khU`^{D>H0R-C5`^e-t5VdfTqtr=+lPZ}LPj3qeTKeT+%)5wp?L8ohE#hg z!uX&RE0;6b34Sp*h5+BqOlLJlhy&Uh58%fJe|c-^?p%)W&wtDi$n`Fufw0l+-1CmC zaJmb-8RTk*`Wg@68zU05l@ZIA2Ix^1_eu=mzY9^$x~phRjI-k0&%Jy$VV9bk5Xa_P(G#3MNe^Dp zac3k5vi2F5tx^O!Bn|SaN#8TF+G1-vULs7;Zt5L;s1t>+w1P2YXKIkEVX%lr2?s$~ zO&o`c3}bDjy0i0GT~{WBnb0Rga^s*K^sWaReL{dKu6r9V`e@svhfQV(bTqpRdFRDf zliDE-8hKJS{*SuPjcT>rWll0}NwEOf$TP;PlZ;`wJftse{U*Dy}&?2Cv?NqWC9aucBHVWK!BQ4AUrW#od(F za7~=6QV?0Ip9H&iSO^=T4}{%I(Ugto7SUWl##qo$nviy-DB_Qb*_W-yk+J~L;)k5f z!rXSiFYaw1$HeRfjJ*WluKLsjlbJs&Hw9l%|M`;@ zs(1b{bl72$t@Tz9#m_(vw>w(&YlO^Ut~Rm#|5#qJ+)=N^^sWG zlV+&$)c79A9oA>rgT;IxyleF4!=H>b7awAVU67f;XpjL1pQ4l_AfM+l_CrBt%Vc{X z-mzc64?78Odq-Nsc&K(uc^t5()$w|X3AQSLRXsG}5()!*q-6v`pz(_ITB@+easpw6 zx)`pW^VT-CU26%eE>D+OzGPH1yG`S_;Jw#({o(V+(Yi{I#Eb7pYlpT*>Zh7Nf)V8K zgw)ZL@QKAAP==`ZW#^udvQMWLZAh3)Xv}iR!vSk*ZJhXF*TUek@eGH4eRz|Av&h{5 zYVRatB&CyDQ}cJ8$*>D_k7Y`?J3jHB6Qo(?;SAAZMQ2o56Mt;;%0-=Pv)t2ik05M< zP30yfuO04kXQuBVBJsBuZP%TvB+dQHQ82o&l41U$S@}=wjjG2Dk3@f7qv+Y?>$*^8 z+AQ1dH0m_rnc(!ybJ4+68&S-tb`rC3lH|6Ck>|f(&Z?E@EnfJ(@IskjCiBDbgP(8v zwOlu#l(dy`L&kyxciSy%Q{Niy^n9_+npv2a)wO_gydVhZC*it+&_fQ%aEY#y=SnPp zI-2Q$xe*tDUrUG3Uz|t&t zysMHu)mFy0Eq{$j5mzcHUXMK4Q4x$8&oVRsh>uKXjaM~QD$}tEob(#DGrYQAQ8q%l z#UwjeViDzL?-l;6fWSR5{6Qnza{?By3a!k)!JUg|*pD#Ho&jGU6|8WYvjOx4fGNn; z%Sac^a~fS3qn#s&Q9*@0MvwI>=WYQ0jKC%|Iy%t0$K-jV?dcleY|ha*!_L|`ooqU- zyZd(o&Yqa{xf~v%8^2Pj1y1Y~a|2KliB!U$tcxaCxTmF`eQK=AtxvGYt6qg>JeD*D z)YoIoNa{y{LNKikz0~Q+{OL0WXF6lBErZ3Xa(^z?d)EltL6YoRZ_K#_EYC+DqVS9W zo<=x$cG+eH{h-=cx$YgSx7~6fB|3iOEft37vMX_u;nPEI(3p3~MP%TmzlDBPRKxSR z{;By(9^%fZ&wF2S@|!xH-!Ou#ddSNe*!F|lbpJFyzzR(?uVI|dsmcLd1(P>=Bclu; zryh}-Kh4OWHp_*h@m1O1BIITg_X0fb;%{sbW6vtgKobvu^`&8O zG1tqlG1ZJD&$w-96pzVQuaECE^N3IHp#rN9+qGd2m-{;d-sW*eL%Hh&0H1`$$H#lv zp6RpRQ!K{#xNUJR1=ypNE2F+IigS@OIL9^MD`gVYjVMFGpOBkvvMqn@=r4(HkB?bn z+MZTZ0}Nd2#jWt5xwJgwpPD}%@k&{ra?JAk$G)@|7gM)XijXApYVI!n9JsBM@W!yQ z3i^YcPTnsU)yn2XKsAW{a32>z8>tek4hgO8|sMhB|%9r zo8h!4Bpc`Jy^RG+eugt5BtVBlBj5bJF8WE;ggwf`y2`RIeERbE6Sk~XnGB_$9RZNq zbY&%q5DG&*eir=o`cRB&K4JsmDG8(zwS1gwBnic(5&FZ;c{b}R8MFehI>6@mzwUB|^W()TS^KMLS_ z2df5#K5@Z%BMk)yO5hbPEoOt=B3ddS_hZOgp6&g$Wqc1L*E@iSVNE$#ZSNGcQ6ier zRP=^HV>#X!h}4uCbDbUR4PF(&!XHSTL!8DH_WjRW{b4v5jQCRkZ>j8#vBHVAHd>$A zx>Ru}_=B6lg?k~ezgTepd^&b9vDF36?)7rc!y}lZqVR<9jg{U(hqnKyfT5(H$`$WO zz)G5&nS>B&=MCFO2_{OFLubM0Y4$#lngo-#tHQ_mviZ7>_O90hng&Z>k3c_A-`t{0 zomZJqK;2iEp@(V1=E7GfH)h&C#e8u9E=?d5tQ|ZOL8_QkgsUHEgP`y!c$WG-X65z> zuiLdZCdhiI|MD=VFcAqHN&BxyZhG4q*H#ugdI!alc(ge?L|=Jh)07umd1i|#hK$Lp z=b6ZBsWF)RO2q@$(*fszKix7M>s;wZkM*{bJS0CoGCkF7pcXPcuyuOdS-le5Yk^7Q zE;-dJSyAT?Jzdk^r*NnQ@0m(DP*$t?E+Jkpo-bI33_j(kuL8#y3?;vqG~!#_psF_ zmp-H=qTU#mJGWZRhD%Ghk}|orBHf$kB9;1=}X>wXIo6`NiIo5A42c zOAh*U;Vx-yM{>puS!&CYa-SF!qXkCnVToj{7+<|^~A=!_7LBgOwGOw zr-4-0Tn}4Pf^@Oz(!JsZQ~%pP$d};l2mYyc6{HU(PW8teQk)n2lK9y@F@yJ=|g)93KuCJZxi(1Sh z^WQnlzn*A^b@dh+(h=8!O5whceMvkYbx#LaT@VmnB3_@*gZIL#J0}5)lptt1f8b8c zfNkb&6}v;w)EH)ZF=1p~hfH>W5(`ey%tadh284HRS(Xjppc8IDD_nu(RW{cu<8^Jx zIZyKsP`6YXJIrlXWMKzG%YS=R4cwkxifex`O+CS2cR<&Z;}*G!A9Zi0%U{HoFZn!i zqlod~UiHoIWVntj>m>VJR^=ZF#LG6_Wb9JTHT}%H1vof`1z6id_gwu%rn`6$@CWZ9w~b39->{p}-=A3M{`n zdc#t=9SVI793OSl)Ke|;>lRlKOHq~VTW8tbbWI8;fO(-5_-jAJPfl?e2Ted1pp`7g zo0nYm?-hQ9m4@repS{U#&f@fT_ykmmFWhFPg!GPf!Z0Rb@~kP%pr|%+CE(5Dpr}o1^t3 zvxn<&9QG%LfXQ>cWJ~%Y86hbCTRDu!Qk5U$D^!V_==i_^`{tdDbbCHC$7S3O7CYBxCD^+{!>I-=8+W+uY{?jEz#Sc)&Awy8j)QZ$o=eXS#}Wh7({uwUuE~O z?Wplpp)KK+XEg z|6rsY!4dMd=og`HXR5U7OJ2Z|`b5WpbNY)!oZ!T5?wTT!=9JWx+qB%Zt!-Pb&))rIb>E6DX1?a7EP4&=l;>m6A{#Qytd}Dk1;e%L!uU; zb)Qr}sV>6-)fPaazA2&UF*@wYZEoVn*wKzCq{q7oHIFYWfnf5A*n>TR%PNo+Y>yCI zXZEaAYXUM3_4y8H8Mt}aQmL8N@g0i@ts2UHq}iO~$%);|CHvqtMj&Qv#n-VY>3<~L z>*@ITk0CQHfs2a~-T&4^!M|V3ONZo*<^9g5*%BrvN8T!kXL}ZA8jp}hYYY}C5J=*> zW5^$$@p(2;7kZ)=6a%Pg=o*-BL8sAzQjk$i(?p?z?Xs|w&n97 z|B(kWHy=0RUDe&0In%CQ)}La98&i_^p)GC%+Ff>yL*jcC*zH*+SUw&dzKAIhwWWwa zX|$2JcOVBUPze`_L1B8!$~;Ukf7A2V{A;BV1xv}WbJq)Rha{EprTA_ARUiDU0F_c& ze`&e^a(x8#UwC!7T-acAda0z%%PO@8WHl6zX`*&zFDt7k07uhwOP_!Q`8%MIbF5Gr zE|E$)3BsX$uJm5pXN%tyW@Z>zrsaSN)fWrc0v+g!X{T@4F4=L3)FSiF*p#fKRjEP|PD|7s`pmZw zT%j31f;3$JRe{zbGwRR}%j&c=u!`THk}47Wq4dl@x^KQX7=XG7yWSrohuadH8^oENJ-w4SElzsT`MPiOj8FzN;! zrDY%Pg^9-~Yb@)^FhNMZace!7Ae#{+6pG`I+0Nz$Ga_ck_&0Hh8>uP;JlY|w&3=?s z=V;!C6WP18oZPgZ(OvL$ik=IW*-FJr8y%BE+ZuKAYKM*MX=C6OFXPR8__jVuj0Amj z86(rU;xvYx2(8v4XC(8g?-KU26{6t(x6nl@gQ@(JKeF+$#WfM4Mw|dwj`hsIZG{_+=8oYnUT&cNDFb3yYLPtZn<~{)jM}lC4 zxigw+?wP-QIRj=ScC=uPJcsbt`VEX{VFr!fltE$$0}*Tj-POpNzG@#SPEHoOOInhx zX2)xnd=c}voPywAX*C?b%x(i=VVMyRkA)hZ2RyZ}9f$%0Fp$YDYDWKy8u z+SXdKb1MvSAzm`}k+9HGl33Q~1NTZSSxjariQzy6=Rw#Suh@SGA_xP4Y=R!ClTU@K zFQ;j%afV5rys3Qerw#=!_?`1V*+GyN7t>5kcioC)B;}~AnRA)^-VhyoH*bT&eh)-mQu#L3WkqsKZmH{tK0)H z33wu%OY)O5ET-xS^5JCpFv}=@WPYi0q-gPSDtmV$Xf@HSCHa~eQd>bHjCB0td&}?0 z`3Jo?+iKrkY}F1|mxY+&%H^nDURWkO&(la@^YJ!*%{)M>Q3yS7c+Anp^`xPDaP@V%SN{A=1Y+ai>tp(5-0h*vz0^K}yxdA zKM^;VD|G|tU_~E7rJWg75iYuBB@uHp$@Gvm3nRj9{i$TNGFQLzUkryFXtK84o&EGw z<^JXLw89-Yt?b7InlJpc8R896MZ+UXY-(t%1Ok0{xL0wis_}gP;s~u^b3+#)>czos3cgU8Tg6oGp=lQG>w&>0(lG^= zKbfbECvZ)6Klc%gV@)_g5f0s6&)52k0h<1_nwkD&ISg3f52ksXxPPU=Q8U38bJ*unY8TsgP+${wQN8t-f8vIw7)*Yaj)%=Nl!gM z#riCel`zgF+)dz&pslR2Ra)%-fv^#fgU_5r_JcV!l33K&yCULqSCS1dXcGwboK zGiiNO$PBfRkeQsw4eG-8NU>Z#!ojGV!NFijOh7hL9^bO+T<+W9Z##;t;fUCc@ zUMIhOpg+-pPDeGx4j(+te-;g9+N6Fyxlh0_0v;tH+hMq&CwOAHJG%Q7ikF0~kFjLhj#OqhV7 zmG1_Z|cbnL|!JdAdiy>(9>aJLIYmO!h@gpmBCXhO7-roAF@3*M)Es2*Z1%D;@ z4o+OQKo!;2hWh`qetdL!HdMv49{~Y-tSt6#f!IoRABbyakmv7!(puY5OR{a%W$iZx zO$iw0R?GPa65y-Gb(lm7y>hBhMTyn2t|!*f0bCEvfXI|4!E`5%_EM5wY|I6FB zK~EgJvy!`B!dFR9@D^s+e>Y4a<1%yQa5VqruS@~fJTIH_h>!u{D$Wh+I1<|gSStuF z;q=KkN0Gf;!e9^kteuQ?yS`^VkszEX8J#HCUGJ+>z-*+7xL9*GZ~Kk&_tlT(-Zn?w%6|ud>{*Woitn!C;iWKc78 z@pTf#5-b+NA5~Z6e`NWS8IE{Xsj8Ua&5^O8H23OIyOBxR_e8z+n}I@iQNKr~V98bV zDT0_xA0cwlPPSHVVWkiugN?V%GDiVbjcyd*aaP3g&x-1W{)v`b?9ZJ$?O*S#H{tjy zMeHxTp77Msimdorsn8sNtIKH3XHTl5_7>Inl(vUENOf=UJ$1@Q1o$mmo&nYVDH)?t zU|Umv@7Db3rbi~MDk||N2ov8W)<0rKNSc2W91WePR$w~nZEjdMuGek78DVTwq}s%8 z6T)235iA`=1JA!*P~oJ)h>JgEh?)h9$vE4y(*LN*_0DWi~d_<;(<4>{)iG2PQ%88uCz{HJ4h1E zMUcQV2v;#>+gGBjI>7z|Wi#(3#;Q!`D*wG+!6V%JF$#!WbSb|w)I8bd#yeg6MuhDu zf5|@dlD;v!Cb*$^gBKStyxHRsERK@1;O?nNa#&F+&V3P1BJ%tdxn8^1N2=l`T&d`q zhQ1hubc0j*`I4Aty-meEGxN_1^5&ZYxXdlZJgxfPif49^oBubkF~J-|BiYcHEf zzN05)>Gm?V-E?sQMoRM$!BH%DV@ab;8f*f>CJ~b^g=bzg%pv4nER$338x6CL2bR)I zCMdmv9ax{`Nmrl~R0T~lOPyJ(wq;=t$u)u=S5NZ$dy+?bgM;_YUD(_`ea4E3z9zEO z!?HxViy%N1#W$^=^8p0pYBuLexENjR=e5|^@;0T=$dty@;S;nYJ(Hdd;^SH* zXPi-i9+H;@r#}K-%jYnIX+1r?b!}PAe|(kSRXHP5jjy(9wH`AonU$%jAtxK936^7aGPp zi~iCyq)?s3O%s0=*_tgPn%=m{TS&SkZ2OONRlCKy{m zKLIc?+?ck_fh*!D@bj0E!h{@hKzgST<~<^H4Sx0aUNDA+hMU-eV=qK=V#=&HE%;V* zKS==L;?&pJU=OIJpyeW+fv97_+}Q9g`j)bx(_wL*a_rqQQaeJjtC7@rGNtDGLO);D zci6GTBozE&r^i<_9@o5t3!LE_9ixL`z z8OSquKnkLNPYi9^MXW(F<5^;XAX8?8nxx`=fMNyv*?O(|9vWnr`%g}c*}l}>#-+WE z4S)IskIfY?GU}v(0VF)4BgtNzK?F7twPRq!~3tj1Zf>gUU|Jm`OD)G$L zN|lg=Xw>+e=tEfBF_k_|Z-!{92X;Zl!E}$qg6kB?*c(dJ~bJI9=W!LT>;}c}L z&lv+^^(z}p^c*O1uij_!#~nKgG(vAq+0M^QELlukR-=GPvJq=UnQ`$_M;Q?Oqs>4@a9V7l2QYFgQqOoq5UCFCtV(ID8y@l zH93q4qFiFWr{rb9Nkqng4q-(yI}iB7T(9|lN#klGW}&NU&D>>^;rxS7kWbLuM}QX) zVwhDj|JFGfhSsUNR-?Fjo1wfnZ95-N!ECDqyFXLm5szE65~pOU0B<}E08kVs7|aVA z`Gv>9;Prrbb8ApjC-ISP<&%-bJQ?q6z+Le`Wkl=;^2LAjRK?~B&VzG}gk3lj@q7%op)Dpc^9 z-X}avb}E8figRppex+_X48E+XwA4kW8@DUfYZK$mLAEiWI$M}#6C8k`B7TS>bf7$U zLviYaubC4>(aZ-|TNK=Wa+isXIB$XwuPJyCpcg$9Es-@7OTTJpmPX04e3lvc9P*id z_X*L=+h`3DCT|fHct$P=11scaJp|Az0g{bd54@h-P+Xqn8#JCUoBsT4S@7Ev&ShVt zD_Tud(@AM-cl%K0{hQKx0HEhO#`M>TNB6b9a!sy;2*xH5E+laFLVo43_I*9DZ@M*D zv}?l*m1u%ihr9k^vE#EJlZ=tF+fp#(8KXY!meN-1tX5HRD)-_w0)@+KGGxEH#H&n! zG^JxQnhAuEJQo3y*TS{n@AdTGSHRUeQ>vZef1u|9_V$ULdYTItGW(NiEjt8nL$sHj z?;{&B%Vc$MQl=64v1b5e8 zq6_XB_(fDt+<`6S@&YAuygcWAm|tXWJAw&9W4;m#(b|Xm91d|3Lz(DE0o;#gJdiic zq@#^AsKU$+14>R-*2YrW8WZjBSd`38fxzJ?U5RRQ&tP`&UfvYI?DiGr47WpnQ$>3R zEaX+094wQ>eI9*FA6rOi_+1j~@O&{)0E#nSy)hUG7rY3VUEAhrMNv$zA(WhKLg#L( z%64NyqxWSjOK%fFh1+p$=V7>~JNx|@?#>vS5HAIRg)9`myx+mr<}tMoN>lmuHlEIJ zvIP-o$o%}{J^R)BAUCKN3hE!y+LacNGRBNTV%6zuN6FGhkY?u?nowE;KP(J;?Y}cg*W+Efa3O&ev4uH5Q42G zW><}_L5riX5pp43feTS}<(SQnXIS)(YBJ$7U;NdN#`O|C0)`Ou9$ni8fgy=ocx$b) z`+}i#;u7v%X`vVk?g_PFM}pbDyU?y2h>b8q+CoOI_`%2^e2KcC!h2wvmlQ|(iwh#+ zWf`pLYe@O5a9Rr;??nO#70Io;6C43t6Tfqjx6&@JwlFE!G0p5AGQS*QX`g=}r4B~} zcU5q&j?eehPmlzY5V+(P1}dDdmCI^iI9oRdu@Emk)Vau*k&Veob9|-WQ6IU3c@~6CeV! zvuitYdwo_FAo0wa5yCNU7jPx6zy!-H2>d)>n3UN~X1CE2>;|C18NjulsTDUBBUwWq z2%<2-85$-hppogdi~RD3IgJho+`QXm@%)$Hem-<*c;|G}e14SqqO+9+F#4x4ph<-Q z(zP1gn<=W>WP&H}XQF$<^lrtOAB0H#T(g|1QXHvjtP&)21Io1)b7d|Q4 zy??3(X%ML|FdVn8z4W`y0pD9$M#b75ovennZ3@p6+A6||9ke? z*5VO*w_sX%USDtNc;QGcj5s2+;nKIF;#cXp@1q^z>sKqNf7t&lFqWjaw8TO7Q&< zBi&jmXC8zX!4{Z-YII|Zx1;wHtmhErW_mWGk!^rSC#z(H1a{5>`?*2eTNLX4DPYd? zjP$=0Y%Or_mSJSw{J!kM4T2qfXuK9FH#h4htS@lRPIiq4NbQ2uN|+6LG6T#8JW-1% zpS0$Sllx7=Ap8%1l2+>}jTx2PiGeo^7`gl}-++3A=U7LXtJsy_NlBK;J4fYP2s`tS z(#^Od=)WiLgTtN4Oj?&b|+sW_2=*bFZY|C@1UOgF96W_*|d&g*njV z_lWSR(DA^QdU=5fE^{bZJCL1*;+kf-vIlmmRLrh$P-Q-ez)@pNud&uK5Kw@uH>Gvh zv^>KG@nkXomvbO~&A^kB&W6IBu>BSBg)#7&oce#*xHdywF};UZV%N^`85aY!_e5Mq zjayQQXwA!{b0SgU3*wo0qaqn?BM?f!3$3=ZOh9ZyZHkW#-wTaZR&lcN`Yz0<2U!I# zIZ))5<|}iWB8;F60s_cV1{p_haN*zjzkCCvSp81{KpT_~<9S~tZM~^q#f82O!15g< z-t_-9*~YjrHyx5xiNK`Kwxj;m>sicG%# zd?_MBO5f*~wBzh53n<95gTf5KFRHf9<3kBkTvF?qPipzeE(mW&sXa4S(-M@8*L~$* zPm*TO@rOhnrsdqlX;FR-rx|0ZS`YuL;vV$76+sPoAH%)u_U);ehF{d_bxCj&5jyO9 z5H{?zXsk4kBH|gdvq{IGP6`E&zJqp+8wb=T;ktd1$=i^N$v#g0n-&HxS}^~C35^P-vCwA9nkQ22YO1WudI4JBT2GM9}Kpz zh+Z0WzL1|5+Kgaq{4dudkv&&xUF`Zr+a0(`CNvw#tDNxtp6%H?fNNga{ub8DS;b_^ z1A@CHyaKR)S=@hFRgeaBVrlj)$YWQ0OG%%ah*4qVno-1#ND+&q)BYy3f($5~NFaLc zB_$fu?YvedfGV8i&Zw===O7Y9XZ#e+G?avW47C_*ba7KHHs}AzpOGhMH3TK>Aq`I5 z|4IpiwIXgQpmGbcmm(ZpeGP9drJ2s2h-UN#&w>B#8~&^2|9_+AC#LHdZhnW7?KFD& z4~01=Bo^~YwM!vk3COee2 zI7)rz^HXH!uL>tyBJQ(&_sdb6>hBJ9s(zdb?NG{h%08u+8gqJ5FSUD9=WthTQ}*-P zA?-g?P4OcQtfID^yel-EU*eOBnj5f9s&db`Ji7s*D*n$ot7vSHK?!^|Yu60~r6Qv> zI+1nB1%1$KmiV)Z)E{~#dI5=_WWl;HcO1T;*6Mr1Np(R&iyprN$@(;5F!Ac+rN`z=W}@2#?vl4_i~9t=Yd0z zePct6w0V$SW5eIQwneHBS7Jk7&_Ag=&*lJ_HR_l-U7-hX1M|iuj63k{XNM}+-b4A& zP6$Rk8PVok>D6NEFIQJ159=TXR9yT~gTHc<*}1H$%}h#sUwZ z(09%E(hi>qb4n~Nc{!UZ`7C~r}l+L}V5*Mw% zmc%oCl-^%A#!METeTBj~Il&6~D<@zttCfc)F1EX#tM&A>=(bk}yu-++V`UC@q(!-$ zj@>9sD1CC8Gc4Ist0@?}8`E%LYGSn2x4zoVT*#NZ?X-oIeRfm8h~3Zl>Wh}>y0S4T z^nm(}C_3+I_}MPVZr_0L=fJc3;2Lm2X~-CvMR#l~E;5S-rl^ysNozgNtg=If>2T)h znb2EU`T?11OP=!x&Oh;j%L%0kGUWu?@8LV{>I!$2BJ9S)N2MIjr2Y92+e{a|F1o3- zB5vmag;ZX448(W^9_?wpRrz$tjKd6uP85@8YOd<&3Do(bK&<*dPYeWJ0dY37=|ikW z7Luy1&to4a2+sMpC}J$|=RS?Eakxd;e$&QnB)b{E;dk$r&wgp$&c|i1)l*KyM^bd@ zF$s9j2|RA)+xF5oN#}d~T1#khhcn~%W;VPqFpuJwihQGfhcBV+#>}kshj<-lIV3CT z_QnidX(bx%zaC3vel(bb16kLau+oTT+tTOX; zQOFbw7i@FMLaam-P)kz^1TqB_1b-Jc_k8c~KhJYN_wV)mH-9vEea>~xxz2f?_c=#I zvo~4ZOx_j>S7@JX_J=cHfex6&9lz$aL`Ztytw~B#H~Rm7k!S*DI6Ii`Qz;SAXfgr!tGo^(Fm*9chD3XWq70PnsrOx~Q#}@%^YyEOh zYmcE~M^qDzf5w4We?HP|ljdUP<4;?2U;%x;US(uNdqrc4z)$|y!Djb21imKfS#Z@R z;B}lsgiV`{(*dFLE+B^cR2VHekFPV=D6F(=^B}M4rQNk6n#!XLE42ngfL2F6^R8^q zvV8v!=nm{_X@Jev;Ez?M%%`fRE7CUgYr+5g-QzN6+34D~PS)FP$yw4rC}J@Oce-85 zjlzKi@mT&I$ea&nWr`8 zRd)6SDssX7F!6FLbzK(~$%ffB+OlD4d>rFn%}&-z$Prz3@G2NpRmZb#e9!aGTk zCxAg#Cufoi8nsf`q;z7CP3o;^%f?dz^D4m7Xc{t~j-MuaEM^F`n~SSrsfIOn?+96W z38q<|5)%PDed?tz>QV@U#;_G688`i@%N;k9sD%{tF;Cac8sIN3F((5D2IN+Z*zf7J zS||_s?D39Yy*eKU4w!XZrGXuHLSV-P+_mHfp!gzB>R&?bm)BcmvFL)VlbwyObFe zVtyFzK(C!INcd*F{CAJt(O@O$$f(k911n0X>)JQ~Qo!eicQwLNzxTp+$gt~v>tf}|w~dc^OR{&$>@tfKQRXWs4h*rcH$e;+v2grA`@*Kx5fv}Fk%(xO83zx zfy*sylfi|5HaB1iIk{K7iY3dLPO56}En0*8MZS8bA`e5UQ=95;Fd!=OK4Qs;i_@uh zu1P`UI()j$O?*f0@m-8Q;+NuaXfZeZflk%GVMKVxC5%Wej_r%5{_9n(`|x*9{L5^a5LsSev*wVxDRX-GL@@A&lwnV@~X zAy1Y&R)ZPMo_>5p=j*zOW8|7`85A{Zl^!j+(8K>0SINk@g4?=_|68d7xs~&1u}XPq z9kfqSvHW%MVg(`sFW7^GU*wL-OAb`>34*vQ#eq3@5x{1GCF)<+g#81|0H|}BkGZ$N zV^nu_wq{S3@YaD~+qZ)%N)d!mla-$ST(~n>)p?bC==?%r>i4mk8he;Qi@Dc)tq?Kg zNQ7bH&2PN*m@^Vq(z`-#WA6l~ryXs1f$;EYL?zF(3vO;;_U%kMzF zdSE`o<}a?pu6-Ot_Ur9^W#6*A&?$92kcj*8GC}lb;vHB^*fuUia4RH(a3FZJr=FZl zC-lH39vbXDPsI_@D89y%i;**$+t6y#apwGkD6=75T3Q@=GwQQ1*a}NkVe_mBJF}Ev zhByWVm9IdS_B*x%-b4>*&h!a_{gBYb|6D?;F?BznE6ar4O=?L~iZSMfvnOZSx6Mb3Q`Ee{jWx8H}WoLLNJP5DN%}Bg>}c6+keSM zR;?+4j*h{5>oSMD-qavHx~F}2H|~pIY)hzX3TP;qE(!99g*EYZ*|B`P39zFd^6q%2 zk38D|j47C!Q$KW^UR!MPE< z}ABCz1 z5%TKA)qNVZ;u`+O6~~n)25tmXUwtFy`nI`15pUs`sS2>h5T~=bCc=kEh+1?PVZK(K zFGu{2i*qsgeUPvxLfSyT{+dOg5*|k2uk(qbqq&BMGf}#q*d_%56kSY_dCtzjRUh{B zAH34RAb8xRtjbCsz{#3ROPN?v+3I73%eZlix#kfB331!wLTM@c1550`5>bdzrL7=B zB&wyKf;7@zV$YkttUzI8+0|d~qRKGb^Z_d&YW)s)6NbW&$=;?;3eEbI`$3HefM%u_ z9yY&iwhqP^tdPi~SoHbw*`+X`;NlX%pSYsann62c@A~(;&>neX)1ALTTHaSo&^OqL z!6$0wSsTxpFz>4;!_oxe7kH}6ak$MUZQEFfzncBsqaxjehKI|@kH>S7A&Qd~s}D)C z8%U1da{Hn_oM;U`@t`rvzE_H3^t8I$;pRuc22AqLv&3i^%Aaw)0K)CtuB5g3_b49$ z^2EKyc#>69oqj3Lieoq1;3hfY!%z-=DVEg+T>3JdIGnutI&hKh22#g5)g^ZOef$p> znem^yNPt+bi5R*-^+Kh~K}ZzU7e?53ff4a0=nHONcj4w_Sn^1&>jnI2m;~((Y^L8P zHbVbe$E1<@4I&&YfJ_v)vU4f^HfQf&jO?Bd;J8MHCDKGq^CQ@8e|FWnYB53;^EN2t zR<86aOioLZ-D~chFUc#w<3II%MBpj<3>#K_cYI1O^7}c)$fNsxOoDWt_767w>6_jH^b8l3ZJAnXt^j<=S5F#JoFOX`{(Wvb}mh1*Z)!6tnw%f6IK zHsYKVRuWK=5r)}2!sy7o)#!GxxDJ@oeVByi`#kmDERxl){%HUNLinVi}IWhBFPKnGBvlf>$g?E$~Y?985((3|dl+2Kw(8m@K{W`LtC%HE^~$;{s8^#A^&8hZ}96yIB& z`~MK%`~CwCrVyjeQuf%rh$KJosV(tQesxq8%Nav&wsczWQ=+(Ac=M>c5W^X~DCn)1 zFt_!%_~0A~HQv!zTNma#u<+w)0t6R70nd4ma0 ziKZlFf2w+ysx*f8y!$WIr3i*smu2v7QPH17Mn4N?f5Ehr_^)>dKoaZkfszrncZl8d zEE(hqyh=}fX^$LfsQ6I|9JyNG`!AOcz=ms2hU5VLhvctiYTv`sm<1`L z+h#Kerxl9KZj^3H&FCAwXOBXh9>NhQVT!jrF^JA!i^U@v0}kboQYOLm%4ogmqFrD) zGsx!bC$c4pFka=kt}Yv_>5g0(!&v^(W9|t|rx35NpQn-CIh!6wA$nZu5~c(9Er{k3 zx7DqNvp5@Ux(%Y`^}N!ZxcMiAMM-GcuqN{GmL<`Mig@> zb?526*J63wPH`kwJJ-@83Q30|BP@2dTI$W7vnP};>kHbKzA^@C<6xfxvKmh$EHeDW zgIGscdXZsg|1Lba`Nb2I?wqbK-?ghm!s7jAI{wRhnzpsC-!F%Y5_D3!C z&06sBKcDyuC{ft02TkYlH~ST-pVC(UNo&h~E$D;zgAUj6^~IBakR4W3L&I(Q-`zuM zoWDJ3XX+KE#fE*Uzp~0Eef5~Tuc2B`-E|@~434eqO!jkZt4x5rIZ>mJqof=PIc&11 z{`Gj+t3&fks#d?GYDNFKssVuN;DtAJ~Nowe%sH(O?S#BxfR<;#B$ z09wXQE6{yaD!I4WFM=@BcOYykc&3ddjLLczF!f;&c(P*-jDQGm7X}5TpTAlEdZ}ph z`&#~SIR4|H-z>RQVm*t1GWK=|z;<{2fo6K>l7eASes8*6o&1{6lKoo>B-MA4%RS{i zk-MAT(hHVcLdSpZ64p+f|5t^Vkqlm>_v~v!bBpz15u-=#&A@9k*|*I`B78J2B1>yL zm-8e=^`*?b2QZV3wx+)iF^{!I@mzWLK3*}&(=hlb#KRY)gRZ>(J86KB?CjbwU!seu zq~|<7$4_*q6KWlo^4%q&awNnbZR0NK9wgvsQR{c;m$V{vZu|y5rRuh<>=a1Q<)0${ z@hM&Z`KSDE??$i*P3vGke^&0f55@>eWv2BT`E9`*+n|7|Sj}QHqE_#f2yy%NGHA)< z3!w`VFsYt7v;oHWqBvpRvkT%?-ar2+Cle+S+u=YYYBk6a z!bgDx+2h~i`u|wkvvc>H&(fnr^!~3NC5E9d%s#`!`-aboGTNPLI;FchVml0z7lQ*D^*> zOILf-syK{wu_BqU>H%Xp`Q3Kt;dYP2MlGj^mUV;2AdiaMCA=1Ye1OSM8eR15Z?j-m z>ZXb|U)74)VxyPGh-jb7OxX9*v9-eB3UfgA=`k6CP@)EVN)oYJowFbIthLdr)!}Px zeo&X5{74946%)jjtMeIQ?ZB!0^oBHqYg(FX~`dLzXop z4C1+q$_lNWvQG16PcN=dsF2xHOGf=6y><11G%k$?^@@H<%(Bc5D<8^p=I0>>g~+=13{UxL3dMu_(&+DCu- zX>s=Ck>Ac{+&^=uHTlHulrL8Uzsldr7GaA14hO~?j?iQy2yW2l zzj}pr93n2c*R$uLa28=$Q2+#9*-jF2~#pT zM%+PD>xsB7O^zGt*K(b*DG~FS8-a_DS6=55G6~U<9!IHKE*0K55li1{_@95PoLM~h z&#d6|%RZ6-8nH>h7-sFwx(DDJXy3>?Qv_4CuJ=g~PW+JK|$& zTUqiQZ_b7C*eD#!+9c`FSkb;@((MQ2NK$VAc-KgP?l6V%6aT%w{lH^;Igs=-Q|GXm zdjkOyn$ZJNM zd#$psk$0#a82BVTF`{MgWAT*_z%2gf#H$b)iZ!s9rFN^wcb07_n1TgTkVDXq@)}p5 zaLtbw$>~3Sk^a3N1-MtI(k!RdPc79q9gToQCfxSgY`80mkV~XpE)h-%#Y|ZI`jMO% z_n}oTX*<)*SO6<2@rNHvgO^4Q{u~R=^LNb+Z26s=OrFN4WG<2D-iAI))jJJO z_C#%!F6V4C*sB7Wc(L{a2kH;%0Ky+I`4Vn7h?XCF zBTu=spn8Z2T+aOhMjmBpC-H@!gF$U*{_RjDYJ6t5+#5AU%C%5?qV==o{J!UMkUe{; zRH(qx?0Po&C%DLO$-nZm@5IX4>(};y3;HLl;#_a;SVcAev3g*z=cknkk9WyHYj} zNoz*r38FT4;d7kTNXs&)R+=bsP-gOjiP{40?w@oQk_2y`quU=_;?Pc-f@`d_vYp>s z0?4<(M0{H&cx1T(l81|>s;Q1gGFa$R#smYi9i=sP4kso`lDBYFZ z9}x+z)tLbnvP0Z0LoV z6W}7_?Ei}G>RIaVeYPE3(BEh^*p>6!@YwaRLgo^`Cn69iNutkC14O7gw^WIiK3@tW zcy9kyu?SSPZ|nZOVE5`ZH>G=hm%xy*>>Crda@_a>PJWZkEO-_w_0FVX(Z$l`3(l3JNoq_oP=$ znorE!Tk0_B9s(+82N#Bp&K(59!|qc{IB35 zTXkNyKqrdTUncn=^73zZ7v7Ss6Xf2nt!`>)f1=XDMR^P_`+-ZwR{zH+^1u6T zFjVmM#$A3dm-a*)(g*(UiSHkQUC+OlEEVr*@|zroWuS(+P3izFs4QAXgg z!HZA-af))8u$RpA4FFb1?DCXYg<*D+G+V{qFF>@A1w{D)>19s34fL z8UdCijV33GrTmnX85U^fNlCb+9(F?t6lXYPJwse7*PZiYRuaP5P_F7n+J|uPBKRPNuKy6+#a+&0_KJHOK{D- z1JUMP=D+McyGjLQv zfty~kH-xheMQYpmxOOt~N{>v9^Uby+XQ_2b{vNeo`$f-gYjb-a{e*_rYw*$h zxmbRV{z>@TanLwc0q$3?NM%VQRW1Jlo`sXJ2tvR1x%e;a%7?2?i8;9+QYS$o1f}}v zGS8uJDNG2_D}-?{^D}LckR`@VctovGoi<`CJ_kp`6()|=#SG<}F-8e@ZM*=5fkyCY zHcNxFV=vp*bz-UZULL}(4l2IdE%=$6zo8q< zQ2`T|F(cx?TPrk&uPTrcl0!ayW$VW8_aF4nDAF@+VSlM4(YU8~7PsF7FAwx#LR)jE zStI*6Ii=I8rEI2SbJt%^8wuR=I)bw=1>*OcNrVMwV8>k5mslSfu3gqVquw%6!}clh zNs9!hf?ymtV~Cml4)5R+7D+c{{fUBq!d~rJz(}0~SCmMg(j1jOjxanh+ybVPQ zpPS7%nYr|+u$9GNeueyG^56CaD67+)LwL}`Gt5_wuDk?kb>mDUc#@IMXK`LM zxWT*Qi+_YihqAJ)a(3mrMtVJ^+rCdgg`V=VO1dlmXPE&s3~H_y)U-HqqcfiFFvUhZ&9wzj{b9z*TTo{(jR|1j-KY1YsDgxs77~@Nlsd)Ys*~u zTc!^rSHmWR#LKnR`gHW8BIOv&4e2>qbHrizy!nPMl!21RwSxxp6ZWNQf~j>W_jCO! z?eE0F#Hcvt-;mKN;TdKam6yMBY$|Cr?93pu4~pZYs#~b$;;)434wUv2$Hi8{FZNrY%T%ER3fK(?nh6^q$m7YqJm^-AZmSVLZ4n zzeX@(Z;|hj_E2lbPo9hK9TygNpe_gULDNx6DHGA#5l$O9BCtkeg$Fat@bjaNnylVH zYj7o+r~4a{v$8G^ohc!|^*D{5c|$zDJGOY{8+{U7PIY+Fr|5106+VDUs_$1t7AH3d z=cC_n)i?+6<@Sup9C?q!$F=|d?Ar=gEmY|qEM-Dl>|;Od$&*mYD^qV>ke2l?D*+Z3 z(R33+KYYaVJEJQ#sG7B+3*S5cpsx64OjEdI=Wt<_9|;RHc<0rg{a$Ia!g>U4N2M$8 zFVy>B!_i&va468=@JJ=F_hDRRqJZ`uYxrWWBVSnXdA8#{6L_iM;^fv#HX59%_B1E) zw;|AHB=43yFf0#vc2_*=OQT2g!C7-)-n&JVWv@1SZG7U!drH~|22i(%IL%T%U!rF! zVT9B?(g6i1uiN)8L&8d+7zN7Q*U4(e-f5|xX@1lBhlOpLw}JMOT+SN$)1C&IuW;D( zOPGW|-TYLKKhgSlGI`y^{wM@Ffj(d;*b4NAU^Hcofca^RjgDK>1vlMUCH}MyRiqYX z3ihEFRBsz?`1GYG{pwO%#Xs4B$Wo=Aps`&(;mhKQf5{5@oF>7o@3;(b4BTbLItKtD zkrBJAV24e3$7gM~6e#T-wSb4orNn_fOqg#r@A~#Ge~d+7c=lzrXN1R`LfPk|HuXS8 z!uI3s=*jYK5#W{Pn>%}&$cAla$NLG>b^b}AxLdBg@18kG$eXWmS%|sb*N;%2mWJv+ zI3iDr9QXbOjrkq`_>-=Y=M(QtI)QQ7O{O2*bB=@mh^+y$=NFs9fK>qs0Ho%Q_?IF^ z(NyDKXayHm?}Q{U61G?Vk^gXAkI}0G%b9V+>+9QeA@Cqfm|15DDFX!3=?45<0J68nH{n^kvrGa%%2r$XIiAaXbY2M;Y zTLgqb8g%nlXoB5o58F9h9!8>3wt>^J{UCXdN5M--H&26*uw>3*_)g#UIpf;ZosVDA zt5Z4TO*zlK)A>7^W8X5D;+@)15CD!8WCiQn=oRNXb zB!m*qHToFf$*2(^b)j$T9jh(WHv{;68@IA#32h8NcV*tG`ZM;6$YR=f ztD3=@0o={Yr;1~=eiwBH58xQ?6smqHlYQ@@7A8DGbF_cFhW0QS!f6rq``b*)#)__q zgALC#)C?CyRz95_F{FU+d{dI%^gT$aMK0U^%`6G{DRprSQj>>BXV-bj=C%9d}`~o{)+mxcE9EhFmgz4(QiTC^G^uLoZr7WvGwCRaMiqb z+`lAy<;GlQh%W9jqoqzhL4+t0O?TbA_^^u0vS+OgF>TV>;vpcZNZxN>hZXJ;1e2g> zs0OTS_+V$8%gO}t3v;S$!}^I%j+o#NlYsq;+S`n?3e1|`6;)w{E#YRL2PxI8=LGYw z^c*a#qb#wt5=VZRJY7)5*EFxxP7LL%{E(&!kR}x$wNqR(3A1l6PJo5ad_Zy|OEe_d zd7l zQ=1|E`vRd7roq{pxPRpKDoM}sACLGORe4RT$Jn(IgB+IcBZnq#>)F;b{9!&U3+%@L z%Kq{`EFzI>R!a1hD4z@1y%2lToaOx*w{Or*K(y>qR=Bv~9Af{AI@t3{!Yw)>nwNXQ z4Qx}YO$H+5_KG=#jdO0r&DjFADw{{VF(*xrOb)mmp;}9(jZtY4Eh1P zEt;l2-rwcY`8Y-P@N*yMOdI;{fwZh~wU`_#BFA`^6!K<*CfReDJ{D+Q|g<&<|ou*Zf< ziOQD7bDGn2Z=(@g9Fk>?GI5PzENqwFW{gl>0)Gf~AnMy&(5F;j_BNi<2%)Bh(4!86 zHp?&%v+2sR(s^5-Vh1HY&mh2}d;k&sez@X6b1&5zU&1PjyK7;dXsav6CVCBGy#+Tr zj_=hIpJS#Xhow@SilOUaJ^rnE)`eit=`Ab+B z!%C$I4NnD{s1g<0dqfa}fZ3!YvNGs?3|}id>Pd!Kwfb@8^%+VE6C0$pa9WF{j8y-X z`190Y^w&Yd=(5z97Sf<8p5*T79ob;?Ik{}Upt>tJtht}2II%xgd*pql(bSLMEY4{r zJvIu=+=6W;c^4S-rZgUvCEYce$~O?5s*YmjWNN(>N_!}M;jp;^@kH@HN{SVX;T=qb zUp|E(7_yh}lxNS7@@V#Xqm~{F$AufY%BggZG4>DGKCgFZdzZ;pdK@gfb+SzNwrsjU zSNFQP0hPiNx*QSQba8ixgBc7c#aEfk?z-D4n}Rw!3#3=BETGJc^5LKyL9yL1uTX<^ zjU3%`d75Nrfw=9$LET0}_*}({)bIh{`iD3&8_%~9Wh+dWOU{hegJFnGqXP7-<_vrs zWw5(5d0<6AHR}e6Fe~CAV&rXLYK8g+N;k90J->$+_GQAq;CAiT$X_Ml}A28|-qS3Y8+ED?7~3Fvb=8lE-6N3hMU@u&NcBexP|*u=>i z&&x?vG%0bupYTTD`&5vuFtH!Lf20|!SCd~I&M-3(WNdQi&1WrKxe1t%_faegs9qyb z8I-Rr1A<*LM>FjNz#IOC^P>BK%rBjWAFb6@8Oo#7rX;h= z9N5TSm!$I4Z)vh$5e;>2dS7dhCqr-NMTxssR9R+gIuM~GX)?B$ewb)vH7L4jf?rLh zIK@As5pp1?x^oSKryL8{qvr0Z_rMA_ausVTr(&hMY~AVRfvY;gsO@<(&vW4qX?yK0 z-*t}D2U91XA~`by+qZHRM;jrx#fME%l6U5%LwK&u+q@DAd@@eE9AA?}Av%omq6l2|qk# z>dYco;&mTBJy7C&*3g|s;0zKCH^!ojelgp7Ueu5O>w?c!yxObU!;LKAJX2+NFG?RT zULFm%?_}O+gap$GC|K@BBt@F(dPuJ% zG0Hsrf5#d33qIoLi(yYN#4M~-NBu>3VZX-rpDVe#uG=)f`ea3P@9BVGr~+zZJQKC$ zn3L!e_Sp%kWpfxKuu?4!Ht_KLwFeyd%f4LX(4IWT_5)ZtCLD2-w1#trl?R@R&vE|# zktROeSkxU7q4^ajtv0#etU3XVbHtx>TpRb*Zfs`Qi3~98N#hba?~M3`E+k+`m({#~ zivuYshDX|un4s@TGMOzJ@;ws?cpLP#s5@fpNun;Tqo%kkHHNhiPs5_6Q!PA#T1J7O z4adWTA8s_PHD$$PUQsspKqh@#@!PggXK48F+QC>Jf8D)oGvm)0ztoelaOfz$jUyfw z?X9pbnQSN_v9>wzRK%SxS)`wfVv0|KIK+r@ZWJr2w=;nFlhhJU_fwPgMY~XvxL55e zJ;jR$i*D?I4hFL6=BL<_zd~p#JKTCDz-M7kc0TrpEQ(L|m|es{2eFCRQqhr57>?SZ z(KeS2td=6tbMN5$@XNhsO;LZ(lzi6wKUtjR`Q1(PxdtzmlV6wKdmEalx*c=%3M7uB z?9^K~wwWI#Ss*Dqit}fzxR2dND!_DHLCJD~sE@qPmBM_?CYU$SgmI3djS|FYw{|9S z)tFYP&do{45!=%^O2?#w+`UXg!97$bQ!zpRC)m^R{dw#$x& zPpF2|we3NUB%6ueupu)*?s(^M{Do0L;C+4Q3h1CvjI@2DlTbG_u4>+E0n--q!s)&4 z(Dp00Us@T`PzMIiP5Ksvw{i3TH4cQdR5+>2IZ+rs!&CyM=Ci0j2fs$?|2gbnZpIod zlrpbI+*EfrJ~F13du=o~-H5vK-Qb-VcSb3%W&Wqo6VEEto?40yHNd`CL}@^jXT;YF zoP-R*q4yh)Q!kC@-ihJdu3*`<)VWB6ff(0Fg!7pXYu{X-fPiVd6t2>HdDv_4a(5zQZ*hyBAj?V|fvkH<7m={-{1y@tg07iy8_mB_$S7`J zgdQfsQT5dvxR@U8oE!V)o-O-;Fu``f`6PeK4>MAR4$gDv$YM?FQ(wh9Tk1VC0~nmd zS4I)Er!9O%3QX8c`n50tZ8Ctk#Wxwazv%1xa7)*#zZWNu|J$?l%9Yor@QCnQM7nh= zFC$`R*$D9))MV-}K5Zu&bqlbK#;W?%5CLu;wG+@f7(pS5s zX(qD63nV7LMnGWWz9BJh!<46qWF(h?-seDc5dSu#n>bpNiURz6y@qd;Dg9Re152^JG-W)gTb}X18%s z)uOdf(zEy5|6*$dSE{S3XBJ+3&@Wmy(VM8<+i}Zi3S6koqQG`V-NCnlj^E)ljuq*_ z1tp2Z9gbMkrRCRkK8DbQrTZDZUeI>toi{|oCg}nB9TfRwadXuFes=2Qwr~ih=qL$a z)KmhEfrZBLc0qVrDFSBq(aPd$KQqT@%TWjIxBU)r&=o{LTT67Ox2pB3HE)jSF)xrb z&8GV2-wL{Lspp6%Tm#BAcc?_&P5|ynBE*bn>r3MH=h+}V#s~8!FTs0cO;({M)a{3~ z6YRnifxw$jtFeQla3-ZzI4+3Z@WncqlMVF-^N5=fZoDmY=hF&}+1wqF-v;3Zuh0xn zMk=$9^A6C?ye!QFFV-7OUEz7>dk>j686vX~cizZ#-ByL=<^`UO0Ys1EY`{aV^qr{F zcT3CiJr3=fjKC{l4-mCdE3X7twk~Fd(Kp=>evF|~=!VvBgtZu6veoNmcvTXls`O+O z8r3L&EJO#3GIZoxZY@xZg(n$3{2r*pfQ;_Q3TPo^T2CZc3jB=o;TQHx0%}bbiI(|v zx-uA0E04Q=me=S1)o{5l&&Cvt0f{SAPOdm*Nj3c90Mv3tJT!89vu987=I$(M2o_oR zB#ON8sL$?7tFLCCeRxp1U0{1xD?#nMvmidQ5E|asLb$XbTfuow(!!g0fNPGxFYhNTsj$(t z*UdMv(Z_pKcGOS`XIOb0iKRf@Xh1gja|Avr;5iHVZhXS@F*f)uZk8HK2JSEXzc>E%K7xygKbi|w?Ri-g}ht$-Ki=K!B(Zhs| zekRem>gFfbBg{pYyCszafgubLezd~GT=s+9wv~Qk#$FO{OK;$ zB47Wc`!QOUP3=xA|4eJ7Flo2+4cbl(}mq9 zq7-(N8Fu}k?0!OPechzP>$%LqQ$1+0UE@uy0@z>TkEZ#6D7kT>?=?BkdfkL{@WSb@%F}e+Pppc{e%B&bp7{!;6)ghBaOz|d%#K!L02HsxZ}gG%2v^Kla$&gdt6r-zAx=&2-cdqEE;DAqAtZV2Q&_ z!516hU-Ha=G!uR5RAvAR6Hdcd@|{v(+LPk3H=9|9po5{M%zeOdwrd}l5L+V;7JryG ztV9_N+~@lixkI=a%|D7yxjAyT89i~sXc?} z!|tYi5eH@~l?`6PZX;OPEqp%6p~!18s&69?6Nh>3WMzd6B$zx3Idp>vE*O+j5+MvZ+TJQx39g#=7ikj79JK z1$_7uK=hzz$%;^n;0cH#5MF=4WhALt2*K2BGbKWr+X&lk=@WZXgcR zr|5JFuYu1cteZgIj1w;pf~GX1cr05MwjT9-tR8PDMFD3}ln#eqRI&lm$KI-@%>C9#NRZ$Hm|~bRaThqG!2wfQ z(l2>7ByJ3eS5Q?_iG^S?a@snPsCdi3gC?=v$3W|hfjb}4eEkW_^LaR9G8Y}`Z;~e& zjvbZWdVLVZK7rl=Du8Ohv9am+=D5tGU4GD($rWzBHtx6;Fh-c7j`_IMfuuH6Soa8|oz^o&8DQaw)a~!F5Aqp0oZnP`42lyibd>MAKu7)-=>s7|9fpP+ zxbnp=lX&Kfm?&OrVyd%t!}<3P}zY-*zydEEK_r5p<1T1RP!K#Q<E`qqp2LPSh$i1YDAnjU!b^_<-~Ey&cnoU#{Q=8FjNX~Ue)0= zT*F?YY%Ou?V}r~gd|a1n=eF{gK}6OKa8W;EJK!ud(IZ3zS)Vo(Y(i@J$9$6hd`Bv@G(r&p?3dp z@XkOTu^ooPuZ(oqO|yOOotT)2Rv>>JXz^4%f6>^)&SdApQ%?Z&(ya+OZ?&${6Yi8^ z(Xm;H!$BJOHgkGX_vyOWdCeFd8-nLZezM=D9+#*FTT7EOwryD0__uN_ZkovQbgwHc zloV`54-0nwk;jn@CL4*QhZ(H(@%v4^R5AOKs0f_#L?xpoX@l)Rz2H0P6Ki`Z^Aile z3pCo#^dssF+g=IG0+-za<_x14LeZH49~c+vKpG=G#4hS8=?zb0dO?fhdpik{Q0g=EisJrp%O-N(vv4Wg&4GwYo1}N?n>i_9N__O23M?Z_RMHOq z48wLB1^?6%HV(3tDX%s=_b{u@c2TX#)pxz^k-=LEK5*-ZFI=6qD~c#f)(ZMFX-A-p zR0PBJ%fJ>!Z{-JymcV>#PjQnid^+@DLIQ+#{uYUS1A?hGcd1kBfs8wCg^wCWX!UT{ zy)k#7K``tM>w{xgRo{%M6iJD_<6yN{|3z#;CrkC^DO|YM)0k>QQrW@rVoAZTd6Hm5 z_;5=jH5gu&*ex1?IL##?VZm>m5yJCZ5&Kzt=alPFH#T<4mpr+~pTf21PfIF>n_q`U!^6 zOiQM4U!vBJF<+<~P$fR+i>`-0! z!;2BSEq3l6+btG zIrYbNuXIZd#1M)MEE(pP%|Bb#n6FcTNS=7^j(y9ng(<&~=q-nXp$3wib4L@h4$R$y zHLe16bqS@;f#Ttp@9$I4SLAs+4;`gB-X8uvx*OkYkZU~K29YrEVrJjT0VD24zCM^b zBfMEQ*%n_|Pm-htX@JWZpmY_u#G_p!WoC}m!GiNkVnvLgIr+iOo}aXgNkG5O-v26o zNKsAE=ij&a=?)00=9Mtq${vQ&Eb4jV5EjfX3>6Yfj1Vr7~JC(Pv#AxcaFCnYfh&|yM3V6b;T==y=3DJlB5113& zU3#vxA;HiP&BmKTu^yyAd8x5ployINR)E4J7N;R8rwzJxDD)X?hWqMbh&? zwvdk0&(0_CBsVod?Zlr@dzc0@FcR6egQD5o*qg>=(rmoXZVSFRv##A}srW~hbW&GzMw(2%=JyWVxNuH`Qc3EQ|4>M z(`;_-sUbC7e6#po?7~Z#&5@niw51G0Sw;$)rBrdvflVtz6_?Mha%3#8f+-J=>3$r% z5JF4t2p(wK)}@K7iwX_{arcyk-k_%B^8Qp+iMH+^VWSdy8_aljcAIu)1SYf*J$*uA^R#dN=S(qN>i|3X$5kgaB3>NwcyxJ!4D4Li`=S zO5FVZxIw5t%m}N2p{+z_)%0Utu9gVFKpyf-Gxjv`*k{ltV~yQ&Wq4o!Me(K6@f@%9 zZa8YS*^D@VrBcca;5J&E?tJ`#ZGCCybhTddc3ssNEsHcRmj9<59-1bio1F2U1gJYt^@z_UB*F96MVmRVC(-|iu~!c81qdB*GG z!vgQA^Pyh=UEmZ@d}mi{Vi`Gt*M55pNL4>SL0X zr{5v8HJb9i-A~JAJH@&NQ|0|n_TB3j-DJ!yfS7@5hgMvdV=H`WE`p#z zsH$obiJ@QUMOle5R>LT_(^5MMNX^SXA9fg*q&VvE62a~}ZNwLhJ9|{)Wi~FHh6w5& zG%h~{p;SpqU|r^utsDA9L#jeidx1#ZhTsc=IRtt#!*HCkOketxkMFdO?=QujlKK6; z6MhgUe_0RkR36c*hkaMe1nn|fT`6<%d;5Z6#=Y7|(C5TJ*ye?;J`Xx+G3S+}ZwHSF zJ{p?kX)xI9CJ8q$kGB^WW;(8)$UdiLs>g3$UI~%-u3DR3dlnuk_%z9)={&7yM8TIE zkJuouKgRG&6vGL4_ou~iiT<7lpxDOU-ZNFgEeJshfT~^%U2ZF#Jki;PD zHE2%>lCPu-)LbOie=#+!6TM>cV7uZ3m;h%Lu8t0Tn2-c%A-P(|W;J)hP^?aH5rT2{ zx(7;HlCx+JonXPu$Vrla_s+aTUL~$jGDthdnnF+`x=o6RE|S(Ia?Lp9rIn9ynwCEx zcMj#RwL;o1@+2Mk;Fsqw%+JUEZUt-ZZO{e1a&F<4-0Ism^h+d%-`0;h(0BQHEt*`>VgFWDqwO%NG2$jZ zEA^J>t$dVH)?Z7ayz>)?gsXx$IBsDPmf-Hc-e$!9Gs?r>j%HY+-pMM*9vSk=nR}Zs zT9?j)wxFa5Rf;&qBEGRvy`WDs>IpZ=>~oSurz=fR#vci)Rv=rZVSjRrfOlIpb6b=4 zw;BUOsszaF?%KL!ckFQpZLlh^r!C0#o5%2n3T8{r*A?aM07_gSS_W$XJEGzZuGaU* zS8$nw7TO?mKO390-*hXBqe}@a82>^Vzb{EH<<6wZzIUrjj77FgVnLSF*hOHlyz}9B z?+y?2=APn#WTGE5D*%`zM`h-26@sSnS<;&U_g};A3R03#RTSUP4(k-b>jzYY%=|mz zlJrJyN@gNPQdV9vy7wJAX2BU+Jk1)@23y}6xAcx2nT^3<5LzD}QC$4?wx z@iK3`VC>h4Hhsxi90FkLd<;6h2L5hP{r|A{-ce1aUHhmbiVZ|W1q1*~W(rZLYFri6{lz@l{B|v`rdBF0X^PY3Q zcK-X;tmSgagxt^l-1jcm-ut>*H4D8p9^8m$cV<<~05a+9qv`UDrI@7LbP24Ru9-j~ ziK2KvXW)(vO`7|NktL?v=TCZX#(g3-fB@i{GBl^hw@hD6ABh%7j>q@uUNkFh(8o2W zPQ;2+Dq~JMpZ!E$c$8}ph7F&9FxB%=Q*k@`ionz29a4I( zq8YwUn2alZXt3Y&Q)cq%;vm)Bggl!n^|SboLUAzG?=w;|=$B#Y#sR{tgrMlOT2VZP zM>QkE72l(<&3Htk#K8x5^c`HXM%!ETDzmm-D;lL4)!s@$?7O?etP1^_Tt6u%JE$yYzh`dMYg@1y?D2WyTgO97l z`@mcT+ye*Ece!3=TllQ~Ca(f`hUJ7>`CpXs9AP@A%Z}wsM=MO+KVS9AAl&~{&wiw- zufM5sPiwdA3q-U868TZIA+1TpB`4dX2|76s!K_AuRnG_?t?$i+o>S7wW7)WOnP98} z5W3+O_6Z{`lJw`pZB~>!l>D}qsKj?6*7h6rGVeOm7E2~woPn%)e z(#%f;Bz?vHu@DbqJn0L`i5Af}O7D++E$~R#5Uo*tJ^c_(z8iw;RvVeO&u}}*J1Ml8 zkdh1UzzNm>fd+;~3BzV=_I8Tw-^sN1Ib*R9r4khRIbS<}nEp|{wM)pu)T}z~nMRuq zMY<$jb)pG(#$Girze)!>%|6ZViL&2A%^%vhD~2|`=UMCM*n6@|G7I0?tHeobC4-6(kIw|rf_ki!r0zsR2x0H$+GZMouPGl5`TPL7L_{&45- z@3#xYx|+uvKDzDbrkoG0$2W;Em$4%zB4 za_17v`$^JI8fKs0IizTIv@Sz-0zf=$Vs)UJK@%C0=z(o@i~>(W23BWW=X{fLRQO%5npmOXh@P`yZNu6U26wN&1ot>Zn2*wI7lKo^=s@&O$qacSEhU6Z^dX zr@^>^xl>LW48BJ&hJ`$QSM1T?n0Jp=Wj+k%W8RK-%I6CggiKct^q*hY1COe`zwKA! zQxCnzJ)GG^Nz;nh3Ed0f87!06PUmhbjqh|P7^-eJD1F`#`6b`5?hLFViM97-%P5uS zQpK#zaGAdG1AUt*HLpXP@>|`X)Vq)Myu-S0tva(V7SiE&EN>knp9CL^mkOa|3}Nz} z11AQ1R_Pke$;fy=EAz0Xr@mLiV)SRRq|UEXpT$O;c;d_{N^5cxxPlVnQu6Ey{Sr3Z zKIFdRlM1(W$8|{AIxoZULrtEhs7hdZRhE@xM&p6$lLlkai{{n|*KjG<#lyn+mQsu4 zpQl|t=PKWEFIJA#lBTYkdIoh0UdORI85)CA$wj#nMf#bp zJr49a_DD(AD0H-Tr%s>1G90b}M2c98LTGfDbU!0}qG|YvhM$;QiOClgmvI%S{dBiS zL9a?drMu$Lk>pPC#t(d_*ayM2Ej&N6AuMQF4n9g28S#p^`ozwNQLqmR7DeN=qp|Eu z>GqofIwLMH+B;9s-uokF;yPj`Xd!YK&?v&9V}TLKXqSp7XWBbn<`trUGD!8%%|(Xy z+;t5|ja#^eVW9E-@<&jkbl^u*3`}+HJe|MkhLkk=6CXvGv0IvJi(VRxL??`w#!#2% zg3$@jrAgIFm^rL}ZtNinlJ1b857+}{X`aE0DUDwxWK7l?PU@UVebKYX%1tq599f)g zRP33*Pr1QxFrhA2yo%txHW0+-f^NvTEb%a)W5@1^|8X7$t^Gz1VYugk9P~d@?&gne z$cCm;#P|aDxK z)#NjLORnR8r@cMzf!p}sY42}X;%m9jAIpOfG2O-MXGJ}&;Pg_QtZ6iM&a$Ws4F-Gj zG3=?#1w>O5VW2KU$C11LTLrHrn+r!3f@me-FHUDMh-}QZ+-?1``V8% zw$l{v@YDF?((taj5g4tj)Fa4XHC8>mL%LgNh6uLkF#9fY_T4ucz(n*_U`n-MzAtTE z(q9NzKrUeZ=6z%A@(i0<%2@5AeTo5{_S$mV1*t^fv@UH=m1o|NUsgV$!NBQ)4hL)? z1HdRJi`#qV0?>)*J6rL!>$&P-RMjlmESF`E+~y^UX%Hzh-j`lH^WGe_ZXk9EvOlu3nUz&-~2kJvy(*|8Lzh_0Qjp0qh24j;b`s&>-1@-r<; zx7E2Px}`6R{{_gw(>eL@Iq)R?*Uw1V0O=+SO-xWQRzP52~JY0c8tV-n5iVWX60AI%BT{ zn?~hgrQgsi3v1R#AYv}O)~^&bGcAkHrcC<+Q??>8Bvy`b}1qWx{oi`XIeT?hx~D$sNA4kb^_@7kt_(vKGP|hxqNM&ZJw7XX+_0 zBBPBr?zY`{BDZKVUTw!I89MYgdD?3o z$hVCz_*!%F)j%|6$51%(5<&kC__Gmxsqe{BeQ68~URv}==#Nkpr5zll2WP^!)FEU; zdd-|Z@^*J)tO)v~?P&m%U)6rp^YpQ2Q3Kr$F;mfK^cF_n#ID%R#entIkBOi+&inL# zfYqV%%NdxbK)<>ZrxZliRbB*@#)aYBH6?R>WKiPPH?@U2J3J!dMbTXay$O>SF^rU2!?0SMRYs)1&%q7OrBV)*gXY=rirI` z29C!KQYQjiu{FRu3Y*W9dYckLxDAj3qbknB`euMKvfIP8C>25W1pDA|A>(!k1z$4? zBP~$bfyjNd!TX?o7UEt-6mh1aW8#1u1|kw-ieJl$&+fBAdEf`zNh7)Wfq=Htdi=63 z*^kl9$?~a5t;J%t6hHBW85(sD#ZNI>J*o3c<__#d=obbz68BV>T`{=KjDoCTnTN&} zu^@Bu2vd)b2BpJuZ@3-sNw9yOul#u!@Qd^__-2@g&)OHk9=$X~ofs-`-Bu@+!fCpC zSr2awFJ0Z6m{}Bs9&Z}@(w9Ug?j!H~ItHpzNZL=( z1eQ=j;#|s&!$2=w(Z`HA%Uo<_eK4$(!oHFVN>cX?gu_1F+p6JqsQ@|bp(x$Sb9}g{c{r5? zq1cc4Nl6tpy+kqdU``KDqC)NzwC04ae}AlIx-+Z7gFrTi{uhkLxRdy@dj!v(pDFAJ8ev*H z#1vSVX1`ZnSJ2zL#3AIH;Wl;}f(w|Ow?UQ0x|^g~(dAD;Q>A%L zSoa#yQVq17=T#2!qIDHx$dm)ZH9bgbOnPZndz#eFx~HBfit*M|%4NU|jWs!d8vgj}#K# zIlS!Bsr|Y6H?Ei{WeZz|tQ+pgEeSTEuw&nRRH_0d{zw%%&!B=)Lz;GAb5vvh{RK_I zOJndBKa>1UPhK=TO6$n9>2Nve@+!USa$%$OeV5VvHeD=ygr>nNO<$mD$Kq_FpzBiD z!DK;cPP^q|9N~G>hOC7S^Z9GsPY7H8A(Un=dS~?Ht{y2|`5q%V!b&jeGi_GWt5Are z*D7UT3BwzQO>lHYf9IRI$&l$6r4KifysV#eA+H-(j_iWF7?Rlh=XF7lt+=z1{Wg#3 z@j&L9+|1+6b?MzAJBiqxEjIUDYhFe*>R+|6%#-|`BKS(QFZsq96Ma^vYjuzNYAm@p zQ&?=wp~s$c-tZM>!+z4;;88}))`mve2oq2Bs-}D`_TO;2T-+^gs%k%Lv(LZ~%%pMV zYj-zZ$T47>WuFNu(hMVhC~az!a^K3p5-z92D;Bjc)V4!va5#^6lxF)dK*$Z}u0H;L z+f^n)*4Adg`BRUZg%ET3%Zt1xBazzZL#j_GRqkOpP(%6JCt18$y%Z{41r%f%&46bd zkJ;dcCDKUx7R>y{UNho(M+-X{kUk+3)8{W@Qlt)wO5S7b=!vpwUy~(k5{nq|Gw6YS-=rB9)bbwa`DDxsT&t^5d zN^-nl-_&`B3q3(LJ*S4V8{6CpscHFjP>6DSS^tpg-N>5Wp5F#!Py+dnb2LwjH6v$q z9O3t*>`r_tDp&7=NOdHep+OI2Dr8}3@&yDv9hxt3)r}hHgI$7P0VO)i7MU4N!JpN` zYp7S=9Dj3rO-inF&&G`=O|D;{141z(ZnL8Uw=j7%CN7{k=}k}S$1-eHEF14OhB?}v zVI8|!KZJ3l98lzdv+L?etBtW{2Rt9dtv!1^U9Ip>m{H!Zx9%y~)i2Gz3d++qmL8w# z-T^(qx~!B{wKS?*jlxhy09o!+98`wRL=C+jz#3n+EP|IZiHXiuMd3SJEGtx*y9Dtz zHuxpJLyLdUcbFYCDbXRm66HHo^PTx@q`9-iM#gBrBD6Fay>Pd;yk7SpfnKD?%jD%% zPUhB#W2+4;7;shTkj^nE5jT?C-Rwj>+q~_B)COm)O?0>t8W+O1UZQKP;aezbC7n)h zi&)_@B@{R|`i{CWD{F9lmCh*LeY|G`!4P`m!TS9V2)9`pa*1pD&0mmE+`A9{QgrF_ z*b4{~8jh7ozrE9_dBBQz;7wuD(RT-SHs!rL^Pw8%L+vfyPP)jDdQ7F{M+7Ez%%xl; zlP;WyBi_R#cmDe9&iRy9IbxI!&Fkdn!%b&;{MZxyWBioaR zHkRqJo5tsCS23IJ&WucqUSg0K3ox9M)nlD2D=+HR)c&xyCx4ocH$=gLVw71@>I?+S zw@WdZ%AN-yLsN(DvW&oZxB2EUczFh)yvmA)prS;bI8Q!@8;Ya9THCuZ{iu=1Ssdpf z)hm>p@f;!Db2wn45e@h-Ji?A{Lm#)RNU}O!`viCO^CvWk;zE)r<|n#jHJRBB0j>3+dS5(0;5Jng}=Mxmln8y0xVl7hVIWqJHD+CzTi< z0E$|I1*Hu)3>;6_wK$--GN$puzW$!wcZ84Dku(nwu)vIZ`Yb44G5uY)zmu~-Z09f_X#XuSR}eJIbGvHa?^C?7zU(mf zDZW7k9UTARvGq2FRk24a(v9j83SJII^nT9wqd2ClJX}ghh%>yg|BfLLMP=c z(;l^(`(9h7R}?x4wpua*0^~~J{mKy{1YN^N#TPz1)!f(IQ=_!Ui^6l*BmK4No(g5S z0U5=C8UIk3!a6i>J5}CP486IA@J*$qSc(2@}rQ*ClGw;m!eL zVW9K`=g5mX$6~zmm}c||SbF(1$p0ad_dC17_pWzl0&X~w@rP}LG!FKxv1lRO*( zjmSjy`Rbmr@=|u!#NHq`ijwqcUSOSQ7p?9}n{4gP)OYfuOl8qacbAaw>@7WUeIh2G zj0WU;QSaB;B+d$w<1Rolkq??nAk*XjCy;5GXK7E6TXkU5`CB2WJ8PjB&QBG&vAbC< zF=MNDke@lTAmW|PRCe4%=pVCo^(35~NtV2JPAc1HoM}MfXLs?OIdPKN$*KxWGjMo3 zH#euDSWcjK9)w<17R}j~?@A^ZpzU=D?{foL!E5KjF8-mWQ99eb=hhdU@bXcX;*+{0 zo4rT!Yu_Z{dcT<)6tHS08-?LV)zOr4NprX*zp~_TP1&%d^^OHC%_X&{n;^J}FxA^! zl%X22$H6d+N1vswj54!oPJL4t5$P5uX?uL_I5hQ*^Cl~B-IJYF2X`2zDIcEg(!!-zk|CmO2aUPQH5>Ntw3@b&P;`7IW>V*OBi$!;57^iN`*lVd zj}S@bwS&n-in;G&ge25w(pGcUy7y}byKoQf?%?g`?@*_k(nzlqbAEyAak+pG26 zAfmX!*zokYM7r>u(G9^FFhygcG->)!&b((^O}uIZjdXb^e-jov*fMu2f!o{1S*x&B1CYRQkw*m3U0*6-SToh$aHa? z=ggH(-+g2-=WX&?hP0Kv$=?WPIA#q|xQ)bTQ&rC${=P%`ZR-6K|8%|VpD?*fl&XMc zoFMOhj-mtq1R>Y8$0IO@CQ2MzKWKQg&L&}3Oy{=K6B|4aLGwP#33{ZDcAd^K8uP~! zz<@-uhx`=T4r3GfF0-4$8zr24ZN3I)=({a2F-O08Szs>=kUY{SOf2kF2jlyQgI%@* z#s&hh@q?en6BAy;i#y0lNIr@^Fjw0 z)tYwEhmBcMP>XK*SYzu_QHHR|wos*w{86ATqkBxynP;AgJo>_01SwGo`uArZuJvsf z|FvzW8;2C2*sVw%?X*u~>e~^uugKZO)t}0DJ=k~?gX6?;G{Xwlv!#UV)CFZQ;|9bB zPVNU&lRAOh;=vm_HA$>nE%&8UTW*12Fot$q{Z;Ji^G?L{r0T<@C=8+MxXAU@>T%Ud zzdodGYZ%|KN72Yl%*|XkRd;KY2E{X4O}aHa#aY$t4T#76!#9mP9?ePmWuElVjb>X_ z;y2yp=}#p}m(OVDkDm$r`Z3-dz{wBsYE(h7a1@+mqo(W}*&r;Au8tPWq0a_*cKem9 zG_nC%sZyC2|4?d8N2zfvd10r%jcD(TFj-yGl&&QFvE2SxW1|V69K0Nv#6O#JN7g!)g44BS9q@ zkPoHqjuc?L^l&k|Y-07XlU{9W__4yiXM}kAgL#oh>Ls9!CG>A_HY3!NcJwH3U|QcJ zkEt%sjT9XSXnLVb@uL{M8K?QVA8gi(Qj$JG8!p%aq@Z}qA1(*Fc`?vZkveY;JAgA7KYmygcYs!gFnvo75zgi?V%AO7DT_wOd&B}YFacw+HT ziM>LJ4KB$PqgsL?CXlQxOW|9_b3Nt~sIvND5Sh5yUz|Wo9t3PAc=-QFVokyz5Uc;9hTGp=?LW{rq&@=o{gCTEE))IrKn|e{+x$p*gmL^68-SsVe z2I@x`3qK;-hypJD!`^23gU#ri~^z&iVqMa zY4isHN8mPIV*nNDx8m`iKdvLt;!tyKxy=vQ4bQ}Hv@JeUvd31@)aA0Tg;!b072K< zuNbb<&?SySRGnvi#r7eeF}D4{8Z=@r4Tr-(<+lO^l>VKZTu>2HCk)escol#KXn&`L zsbYZh8jFTnf1?dOMy1orzA~>69Ac2>wPA_BdV?;hb~9H$;6owKs^d93mJl!=9;BJe zx#p-(Xww_556B`jG8cWu=8%v#W$h5clbydvM+z4EHdi|dRA++DB4pF3pm_&ejB%E{ z@jv3I`kJos|?nw<6^()P&q1p}!Xh^?1{*mbJ!aaz#L;kPiu)DTsLMw{Ill57DF&;IM zQ9dOK#<_&}Ki6{dbEOL!2%Jv0E;mL<19N)ozOU=Jh5lQ+0%+ZHg`1SoBRo8EpIHo= zfxK2BmfkeX9Hsc%7g$Z0vE8-vz1y#1){v@E#p1Ga2netQVJoBfU4_d&;fAesZT_dm z!^1+I(LF}c<WNEn$aWn;}L`k&Ln*WR~c>Cu{cbHRlc;YblK~UO!?ckS|(S6Lf1578IrBWZH zwc2va-8Zl9AePMxwQRcaT)ulHc@83|`D2eR>QXX8jbvaewV)QBki+WJ^;x!T(8Oo; zTYw6;&?ouNEqa;>FW+30Z}y@$B^b<4w?1mxo;k6e-@@G7Cco4n=RPLy=u#2{72CZ> z>eJKx2qd1dYPeB;_L1*1QG2(nc@LpS7il5-3#ypnZw|KU_8%+mz91(v&VvtK&w(yb=fSU{X-EH0`|6y^Ne`&TTamklG zx6D{j0-_`a_;>r@lK8+Sp;0XF`&lB+X1Ph^FswZK8YR7WJEDt|ZuUXLi02vD&OaQa zOL7F4eZe!Nk06@5!aTD@Mn4hmz!&?M_?HKke%giXyZGFi|K0ch>%LpEMSE<;0YN#L-&~e|clIZAY z3P;SpyV%EbZhkgf!NyUs{{k2vauAwt)l#T=9)=W3UZUZ&jG&;PBE@)Grp2@x*9RzQ z22F09uJEFQCKX+lNwpDHQ~Copur})(`8ceGu>wj!aPQRwL?zrPjX% z$<3In`z-bF>AZK3)~&v$+MagR8#ipCC>caT;>yl_nRA@OPl~0`ZiTD zW8EaxORvcDeysSQ95j=t@u5zCGg;SlW4QF?3U}-$>@A`4Kv#>ygKE8M{xv z+R#RK)rd%LQKg@m&ftx{rn4N$pfPlN#!#nU+8BQG7EEDPm3^imYqTWjzDzrVmV2V7 zx(^E~3F)UqpVg^|0qC(V)hSe6vN^<7Flr@z<=N3ecnwva}K7QfxN z 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 Zzc3`De)1oio*{m_UeEju`0IoJ{Xew|{WJgo literal 320386 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 z%0A1)kVj*3W`+e@v^0AM_g-kZ(GeE7PEA>#&+LS9YnHbtCl=MW0&s9!&sdC%$kWHMcI>14;?v^6gT4vu$W|_Stl0_R=9kpn7a2qaH2oLrh4k2zW z3yDYj=L#@zphr@e_xU_*wC3yNbeeoLrdnc)QIe8Oj&!xRz1!1{fYeV0DEhHqXsi$0 z_jF7~wLQjulfyP_00J_^Z#d*U>)K~i_m4h+MVVOe$3uGSNr1Lz5dFU%%XNNM?R-qP zc*k*r5vt4Z#H6I#W#87_9}V>Po85N8druGTM{cU?sJe=V290ar+)B&iEvNoDU|Y4w zuuahlGuYr&6NYMj-`Pjpc1H@ixwzO_Lmqu{GSf7jPE4L5Z_+PE2?k%gvSa&uKnctS zp+S=mtAMx1@?7dJUy@Mr7F^@dXt^rAhgWYIKE})WgB~75;up3eEN^bt@@868aDgYtjYwikfRl zealH&Q{|?T#dm$zkp@ ziR~ZwI~Y4fBDM@&&RN*tzN2Bk6ihtI1&X0e_3>h)f2H z5>ZrC6n87MQx9aqQ7Yt5jCy!V3BJUE0>bgI$2KQ_S#6yb62Yn+R-#`z_*N_VRwqGl zi3miu2laICSnR{UQ>|gKL!*}wWG}y&nas6c5o^{9z-h*V%`z4DTOobhlc&;eRhS1k z)x3}o8x9K`4i!KJ*ouN7Ffe~`bF~LgaZ_2PW~`6ni&wh*D2~~FuRgl$$;70{SoUzQ z{<=^T%@O^%`sbX*lcxE394UvCQ&cZ1vh1(LCCOy@gL`c@@LX0#fVINTpQL%WwZ=Yi z78w>m(2tfC?nULKdz!W_oexFfW@cj`1;DID({`%YVXp-5_Wiq~d`Y>0vT%lPfBS}O z5>uth8Lq$b`J8=|qmoddut1=PUMcM>z$AtSPIP^d=89nr7}?cFV4^`qD432ouu@Z* zpXuhs6$+4dLh`AO*o}`b^Ne3W-vgAoBaxx-c>EABU7B#EMh%tKF|8Xa%e7j1_;m{2 zG}KH*&}#ISw*3Jj0@0`%i3wFBumz4-Tp$h%V0rO5ys#Q8RR{LIG`ZW_aJUsXQ` zNv^h0c*MTt$NNy0JiYgyhddSEZ!P6$5-f|Aa=g7vR9PcxIobwa?ufAGmkrM79=z-& z#s)_)w#jof-cB-5oQ3KN{cXm=$M)=`LF{7fgG*e!*XKq|+->VBv~gpgMKc2kYYVs~ z-*Z4YHtYdm3f!))lhoREynaT|vZCl+qi?7w$c%T12R4!Xvfy7d+Uuh=rVL~-&e}<9 zqiqVSf~KNv*`P=jnH9?gB8Kvn@r(tn4^1);f0UGzw18%UHWjR|V$^Az4GzwDo3MnH zJVEcwT%I;mmrsKB2l87*FC?7MFJjuopQ_OFc`Q>da+1CuWc&3TW1$AQeb5B2ZmEbc ztN6m$*jT^D!xJmLl64;PlHX3q4pTWh7d;Y58KuYg#GW1lPZmK9UB}!LkZq~mfo~nV zA@7Cr1utUwm4{@n7GM|u4a%GAIhm}XB^9x}`1DF$%JhB6RwN$S~@8wv`+(N_Cq5(iR1%HM<;n-kc zHnQ5za)?7C$0x;q1F;0#1J}W%tQMzs>xcw8@9Ar)f6(neld!yKM+Sg*Hsg6`CK9;2 z6^4fVI5QL2mAR38m9-9T`YtxaDcR~%M_*sQtfLVuC}_F&&607pf7l-QS2cunQ-wY* zxS^{{>2W4mTGa2&EY54zg8^_a-E}6Yhrq>?kLt4)~60obzp6~SgH>H>_rL_W_jmR#y2w!j#l5H7n4$}6`Y|eHT z!^bp&-wgaT0fBf~3`ibuc1Fj0-ZsfsqdJ1pZzTvD;Iob9q5+kddwP4bprK4p&~?ghG2Yy)J!rG)`NP+^JVC~^;TeW}-ykKxXW7M* zg9z$q-;(HX?@Ai()!aeECDY+-JUN;^i38lW$?H3735C@H8l;PRI14LvHH$O zgrEShz41Yq%izBoQvLP8rR3A`r`cf6w|D}(f6vb56qlEqJl?U=-~S>uZe@1%sk;F_ zInT67JlXcH_I}(uKCh8EA?jVBsJ$Vb=xPmcSfwuvh%e8;UNl9P^EL4!acX%Cw#En` z5kDTYrc|p#1o@C^(clB_5rqpy({W5x9i}-;Pon_+A7!QPFIIBvog9tBK$m|PmzElV z5_W0i(-jaO?)nQ8%yg_#`q@gB#> z7hdS}oHv{KKDd~xI($}VHSuNJ+mIoCelC%FOE_8HP`Kt zo8slL0)+YosETKRnei+T<}SX8TbZ5?cQ@!u&U1|IORICljq;VxjC%P_S;>}SIbdmk z`w6Y(;9z-CY35sr=fh9hW9CQ$Z@Rqms0}^yMd`j{f1fCP9G0Lg+BmEUT=f^Z}GvZh%5X2FPACRsFn{$?i3rL%89+aELy2m`|yY z7;oCu!JswikIn-t*i40iuu)YJHk9V-Dj0SW5`k2ww`FqPkmxgb(9z3F03{-EK_s6c z7FZY!`0>N_Ew|*Fjvj!h@Qd)^l8H*mKv<8Mm@s|nXqv2J?}sx|tKi1){1@z^&2yj2 zCoxaZKTz6&iqWpzzR_xzE%m%&KYi%VgR6vC`HF-=jK2dhJ|@OAk2SEW8S!+t*wugN zYc`Zal>kias>J_@K0NU<9=GdkXZHKv{!R;|QX? z=|Oj1-}w{HRe-zsXIb*uLNI_N?oXNjKauUyE|j|T$I%!wXjy6iP%tDOuK#E2jxkjS z;QD-T3~uX}1h3Pe3KmS>+?8>Xz6c1%38NNK%r?KvKflC|Qr6$pgi7Pja`fJ0SIG9{}*FLF>?KL z^~)%Lv6*8|RTtE6K^XI*VXXdIGEO9BIIWl}fLzXHfiQNHhB5VC5@+48wu3>|Oto^~ zD5P|n5|@9C|q}ZcypUT9fb^)83mDc}N+DSSJ&{j>2ZO#V{ znKk!>|1qyHE0`6{Ffqi+JHOK?io%Qo_D=lHNNmVXg|tP{{eXPDCYZkSJ>2CYQISne zI7Yw1X*BXBRc7G2!{-M|kihStZ30gf`B`1Iaq- zZSoI9^hAj0!o2C$#}<<<0I~u((P&QK8IBJ|N5Mc9PQYOWAm zx#Ax1!2-IN0_}p(B(TexLIF+iBFjqfV?dYKh5a;J{v|=iJaaD~xk>TtV6OWm;7RD5 zw51xPn`#g5TKd!kX|48zC&F#jtxRAoDtv(8MtTJ4NtORW*P>e^Ow7(7{TqB^%sw0? zL#Xftj9--ze0B_C{E3fm8}z#Y;yyo01aGqae^0iHf^mlLpD7pagqH1>g~tsA?c)*iL_SAa*cF?7ntT4HUpx0@SVaDHFH!9R_b;jZe~pDxti@i>6Pv!;S7TiwyI@ zzih3|O<75EZ2JAB}f5wOku|6r}Yf6|?DLoH;z@uwgt42IiPgSP+s@Al5Nw6p z_P0y^Bw)LBQnacCXzs4gNdiQB+b{CT<{fq*`dGx1N7s!YV%>^_FqY+Uwq&+724L)z z{Y{emhc^&G4KMe0?{x$mhnhP6gRzSID!N#!#uP7{fFE}>=LP&qOSaznEcaTamI`V8 zgi1s~dI&2{ASqMZxdEI)`h=QRk9x|Z>~f*68tr3snwXhd4f5T}+JYu<(2tdn=5|bs zSXK7fU%n!7Le@N8RMEctr0hV(zLPbTa$|MDGv-hcO4X01;NIh3w}(#w1!sWB+3|ST zK!1_b1q(nu+>|&tAB(3Bj}uhZNbk*sdo3;JUr##B!4Fwxu z``o167mdl0_7?`M_JvrzKX?IKp3(HFl__QemF@@?ZPGDWb6ddzBUVGm{VccqABDbn zT3=|gCW7wCX+h9C0elU@bSkM=XV2J=>9=xa0*MZ5ifd(cF5a&3p zQXWNO<2&7yaSSL5A{bw%A!k!r8<+A7up}J%NE|l1>C=~XrF>j6V7^Quf|2-U-$_f~ z^A4Z8ss|rRW|HyZeUZXmRd?8ILfKe*>@B znx!VcwRx8;2f>SknQZ)8v2GS<)M;hv9ZpkNrbUOxTV?U!>;!!wm$L9~|FZPBXPDXV z(&?cK9)LcJGz)ZiX@%1}wEBA61Q1|Jt{T<0(r1J7AOzOFH^LV@`$T?j)HkN+v_jouVKU)9+2UPO2Cyb+iKhy(E!LHX znhxQ_=bYP&%4g<5bwxC}ekRO%#i=p8+`JFPtW_6JL3oVM0FF{;ZP|QVMxzGOdF5N( zRF(kXVe`(~Y`)J;Yvv=nrILkFgU?OtX2C2OTxIxBS8Cxo2f;_+C~%eNWJc$^_-_B@ zME9R8N|42YboGtbbjp!ckn(=PLI3WgN6AjduCsNNHQ}tougYXBa56?!F0MV2Rj&3~ z1Cl*tOVP#7YSr;>|7u^MwN?E3lG{jwk397qp5WrPe4J&-ZQ+ELPd1K=bYI0ZOL1 zoo$Z77U$X4X~dbb&nL5jh9lLxppc@QNK1I5-@xA_4o$tER61+c<}K@gzt4NVfd{mm zq_}tXi!SsxtrkeeO30E3MDc5~Sh zZ4-xe5cJhj){RYze8rP0UMXs5H25g))^ zlO@xrQn@M_y-#~2?On1tLt9uor(~dPTj$A#y}BO74VXPT$&Iqp>OpK9(hA3qqrT2+ zGvG?2WWH+L788|?9!S4FSnlr%#7v@T`#fKUQ}eEEbN=Aci9BtG&)y7)caLd`KR`eK zk8XSF{Jc;-hvj6m8h_!`XgbKK8OU`7S#<^dKBKb)(y}HNqlCT6PMOwfwU0u6bd}qa zgx4~EkN#LoKPfDLVmAGqqB?EB={V<_()a!3q+-roZpx~iPn<=rmNkYIqPXFQoz8uR zC<2R%6GU-sh~h&867{%jm{pwsE{eu#xX@H50C@fU7Qj;%_G%-uxA2`Hp%?3F4`{A# z**k9mZ1TkhpaqP^15qk;P24XcZc){chjBzrXO}La{|E_3xbQ| zveBQ%^{1p9K2!CDaQkB*B?|ht4)6X)^9L>2@|t*wyt0g~ZVuD(TWL<2{&42VURiHb zS1Pks>=eG7WJUKLjyL2Kt<>@%ep@R44Y+c0o@x+ax*&g>e5p@7Ib|q~WG8naW6@ggNLlR= z^#9QyX}`AC(c~Kw?Y=_u3NVi4H_z!#k{P<-VTDXV1eXL`2NT6;cctVBP}WSz=k1gl zT0;e=yLHTus;n>buZacipN}6(#l-Qkt_E}Wb$y7BwRp-cO%4&PVH6q z5W&4`#ER4OV>r@Ez(->~(HYpE!}BGrcWU%wtvl_Mj9_>l1VHR>Vd!H-bK(Hfqhx3X z$WOBlm7Ov6Q>)Gzx2G|VLFK=3$sp({1~JOTuc}`!_5K_jL)`LLwa2(CoNcj(@$!BQ zZ*o}k*Dq*r^CN|rdXstxuLhW!>BzfLw zu@+S>DO7Vk97DbvmtEW*VcOhL9Ae7lrlq@Gz>HTK!7oB)=AitdN3!wK%ysEgXG49GZ);!dX-fl#4|UAFutz#@#S7zD zQ#76Bnp&@+W}iDW)0FR2DD2S5^_o)4H0#D`GG?G7 zyL?DI0qm3`P>t-TM$~cP-8$zT#=kOeq9@(fE_TfW{L=G@`9$UI*=wF$ovpq&1}|yn zsrY-K*Jh~df3>I1;26_=2j-^*?=CAuyBCMuQ|RnIuXN;0pK(h@$)5PYJ*Jd@Wo5bw zr*2aAFaf&)_qIk&s=me?5~~Uidq##CTOB z)`(Lkw36Rl(9--2q@j$7!8CT8>)LHzG&1Wzm3 z`xL=73H5=_&2!#JK9#J;+z`VkY#0J603Ulex<~sTBH7UB^>XmK-}wABd)z6a{mwGVbcwp1QUHG`&Xq&lBG$@=a)she zZtpFrYWBf(-*Tp9_8D=M1N zm>*qAyG9{~#R{=}AC3C-t9|)0IN^+Q^HM>U8?Ygt98*e9$hM3b(o`p>k7i1#@thkl zI|6Q`Je((6Xn4}@;k4T;K6c?oj29p`*i{nbDF=l%Q;Hi0RDiMjWXyyMC-3K-$SP1` zkq;DI)D`K?M=-B*o$0rK^x>_}k$-xOKbMgWyFPVj4^9>N=Bb=5c4{iR(1xXUhAK((*nK*@5FOw;eEd!AjnRr6J-a~y8V`C7Zi?FYwryZsvqgBO;t zAQRl>-In&;%P_0JN948x`;W#{Keu-rZYBE#&VEpe_B%Q=`IYFiKUXLTFQ07c@E&zK zgvRhu6gCKW(GG#Xb%BH;V=zN$yiO*AZomU{*1iE=X$&ds-}1(mCBK`1ENeFo+Cb=) zKgSh?iJ71XxI1eoLAe(4bC61q-_PW~5gq_py{=TIo>-90PHG<VQ=@1{?==zC3X@%DtpPnk%HJOUH(5Fw(e**}mrlA0wS5#wxhUF5^o#nP2e2 zOs*Bx2jlL^FKg+LHhD09CL~p+Y3--g^^vrf_QrEdgj<;*N8GA4Ft(H9A%lHg-hMZ9 z$vNk`#=n<1k7cQe*>?_0+SJ^4)@D~sABo>Mx2(kh;H}s1Z@3MabLiYJu_m2M8|TjY zzs{XSJ9mz}?tXCYsr7e7&c?LfIncrTcS-=*DQ70F8D_%11LF?N^U5rWONVQw4pLk* z`AXGQdqxyW&gR+g{Dyf{#?&L@>Y#1o5~St2Pr^-im$hXep{?5`v%od}AuwmlcQU?} zGx~My9JEid|H7lP#nNC7H_PES*sR?9Fl`llx6bH4EHsg9bhCJdB#MC}g{n+94*~z3 zPqtR$9pN>qPs>5@n5@gNKkeKXQkta;)YnUayEH7A%w-#{-0E^U0z?A9$9{&nOuX#n zv{6S@cz2cQ9&MTj9Tq;?#xX1YUEMq~YH-Z>;ZIN%LoR<%V>z`LY)090g9C5J{Oi2S zAkw!>?^AI@zN_%J!goJPGn05{?X}C?gR}~I++vx^BA8BOaYx^A)5Z1@488sSb#PU- zPSTgp;`@Mrz9(K z!-pux6auB&?-B~m89iVNDcxhe>cs&YO83zJswvMW1GwHR)<9EHkhciU_<)c`J}BMt^soSEx*tcLcsQl~-90zS@?U*72HTYPUyoRn6Lj;4>w(A{ z9Ib)mTU`rox=*-{I`rk6wVwZ=bXc}a@OyMj%D*FB1YZl5mor7#_y$(=U}A$BDfY9!8hCfnnDOjYumWY&;Va@U~}5{gtubIRw-qJezs z5xzn=U4w9nwJ47QG5YPHv@9hOEspqwDz0%IONj-)(QSYZo@I2M-nk1_FX0 zA|gn4qoQ~O0VNb^7y;=Rfgy$vq(dcT2#ap%uAyTTBxlHhnIk=P^WD$jdCz;!`~BAX zegCnR!gI&IcV7G2O}+gA9pjGeyPV?2MoKk$Ooq^l1jxJ=FMS{NEBQx(ZGGnheDjpx zw4Em+wi1IJMwc5NJgpz)^RWv)l>rM$E0uMB`j{k!-KbvR!>ZP+pxt9LzhjRLDq#C! zuu+hl7!}A+OHue0q8tB8K^*QP;KZznk;v;ZThxv93?*lH^+lm1uZN`7+*8By zJHSgsW*JkEY{bGghdBktIFeY}Rb((?w_S9N3AwN9*yt)M2PI5xXL|UOZJxSTi<=d9A5(9#_t z1r7t(OR0iT`pEA!GPJH61>efUzU^>%mK486NG*~wa8adR+XSn_D@&fcTnl+Ne_U(>?ZP*FAo&BT#O7+L*wrCuhasYB1z%Olm@)M0b346S$I5cfkgB*C zSSKS}=CSKI*h#vbWm!6L(p)9CO(|(atg`fMad^|+cAUr)R4gJd%`IXNh@!xLnS3PJ77A=Dg*k*^&wnV2 zbpO9a5e*{j{}RQT$sRdhU;U7_9qtgIJE+gDIMJ!e=Xy#tA1t0bo3@G-lcplI;b(0s z`?V*3*4&xSD6>ea#t7dxow0ef1$655vz_;`AX=A`Y<8(Zm90-$QDM&np#+b+gqcX252Xkn=7p1%aL4Rer| z5M^NjT(dYP8e&%sbjzUuEtm&qKvr#Zu0zFjhqH7ma(x`?<-X-YPQ|8I`VNM60r?ze z_(QTynX6&GGEU}hy7coENBXCg)?%qIOyIZ8bfuk(*`nV#2db$Dr{N8-$66q$Qt}06 zwfZgs9Li4U-V;;P+89|+EVxUsfxiHnutY^}W18WEUG8Pe4mOA0r!v*NHEHcl4&KJ; zVIE%bf?0rhN1sTDChyLCH|!bHSJy(Q%9+d&d@nSh!^m;QAsWQy$027fEFdf(t(#P? zZ%7uv9!LPEVxBg?hyZC$K?%n*%w&K=*S~#hNcvE2Ht1vpzY2wmR={YJFsVH9zp$cOiOXl;OO8sdq%~|@_;)$K-;W%Be zcxY!v;kyDv+3D+g-EGP4cp9MaeuKgwWr1|_C<(|!$#j8m(SRE$x1G831(Y|z>NNt5 zTx694Uw+Pk?aiH24N$S9S1Uc+SK7snMfa)&Md2$n>{rUFC&pUn99?8L_zIjdmGJ0Z z(>1K}SQ(QveSj=Gv7}2_L}3h2ZWy|zRXa{}yhFpMs{c^I$|s1z`-qrd)XB7RZoi(G%jUHE1EMAZ<$A+4*86_6bfC;+YzG?>|d(ly6OMQam65|@Q(Kw)BK zD{i;!;{T=id$^;Qfm*Dm@8J(%aECFYFBeEJQRYJ%*)|6?9d;mM$7~E2&dVlk<7ks* zY8HN=sIX#@8(WsGsV=%Z{&^)$XtCR?;$t4YIxj}n_mOfSCAwyjyyzib@Lnp_kv^nI zGRil4FLL~y^);xN;4gcq;7Fq}>guEVj=CTa%{W#NNU~>42!F2(ZV=fGlv^KS5P|6Q zFlZ7a;+cr%9f28T5m%@=Wdfh0DxI4}7RsM8N8a$ck6D1K>Tsj9; zr%Uy1fRhVkuf{n(7}bKQ3HzM9znW%sJ2M2}7{@AA0JM6*aFOnnO956Rd9N?e zv)J-x)#ceF)Bq!-n1{5YtHSa<#|q~&7pfMc8#Nq&oUuiD>LIsDvtLTLq~y*Y)GThb z?7IU!?Tn3mS)F2sOF?k#yhCtLkl#tp;XCn6@Q`_O#~`;n3|M~6SK4TB)*zyc2D%Gi z$b@z+Wvpdn%;fkdbiltq2Pqc{j;TmN><#e=$A?LJr_~@OteK?BO?xci3%S&&+_Ewo z+zENCs0-kshqRJBK;m2_%bfY@k3^+o(qB|yTTUoMMa~uE3#Mj|{qgYvZ;5?*Ka2vl zTqSPAjNO&nmPH-|@P-?5J0<3SOcPt?i;rk2CRcw6tE>@}6Di&FG$^YJDf5Iv4TzyFfgUs3y%iN|dL@t}0 zt97hCm48^kaDHB$2e)mBET)E=fZZQmAFqG3b5<=@@@&FQamd=c8W~1!>i13KjT#1b zD-CK$Zya%czAwSjq;zQ}bdrBLnF01STTs@<3(DGP3mUdNfj|O;fdHcLuQ1S-91aOc zIxR$^>;?Yb0zeY+jtnLlHb5Cy+Se~;(`BlyzF$q^zw2y+lm&0$Zs&&Pj*7%I zHY7}65|s}Y-ia-Eu4NhK(BNfq4dk0ALQ>$GJu4k5PTj1B>#Zs60df9}_Fg=wchrLS zU~zau_hxY941z5OGkLq|=h z^Z(+g=~Yw7DFJZ@ai4LXVy^7@Tu?DdU;sIrj-U0#_pX4-8ADtEOs@Ruj4!t$5|MAv7lLH4m)%OPg!u7B% z)(Q_b*VW~f-e$Oc1xmTr-BN<`$<61~0_sHlGO*|*)TeJ^T4AhMsi(lO2G=Xxyxww0 zEzklxIFoT|jO~bRqs^vmZ?g^e4^QCG7zQE(1tx$KN3UbZ# zbr zG{_od9^qU{x7SR(^P!YW&hF+Qmg7>WvtI=8?Dml;qw=s@;+X;R>kcaiY}uZ3EsC`J zn#{5sigsei~L|GY(`V>_i*HKnoCSyI`2g+Mhl5|Z6&0Pxr2;_npR%K zprvkstm8xULFyT;^Icw4HvpvQvz+Upmq!#gc}w_~3@+MvXDVeABpdy`Ae(F#8oMof zc+vd-bJ1Dki?&vcrT|&F9WW7CkNzoS2|OE*Oz@Bd{?5wT#Uk8d1xhfrEmxN{J`Es! z!xO+-*Y)WlnRH*<^Cz*PzoAT4k&`PWud4Is$c8JVx!fSP{sVdeNHYbeDcE29 zt@z`5x4z7X;;m}63zhSW`quPX)f*?%u|*%1UoVxJqpWN_&5{vP)%c ztX4yRPrHLaS+?mn^%{53o}+P%QM6|MZBfn_#}?v&To~{ma(SA3w`(nK*|ePj3Izm_ zVH|e2*R?HD4|}HGmfx)5e>;(j0nn534~KEQaQ9I~kX@^~e*g-N6E}ewpOH%90l5mY zH8KCUHGy)*|JPkXB}vL(CCR0`Ei2WF++tt8K7M+1F7=TVI;r4~UEp)&-4PhkTEY?- z@xu|XYe{xCH7K29Oc>VnIrdNCi)fdCQ)?B0WlRV6iiu+Dxb^o-?kM8C#)G_$LTksN z6w8egbEPhoUVj@fz; zBI&FODlAX_C~@Ayz(7qM${5BR#+TAiMQ6O&iUssW0R3bLIR^|RgC5aC=OFdp&OxsR zl`bovZ$!_^n{ugc%jbi_6M%B0HSy>tlfSXwNMK4e=OH47osur-{XSSm?=@wCVy@RofnYy6H~r5(>)1g~=hF>qF{#*j5x)-yF< zP0u`*7e9tygbh3J+4^hhBuiZ=*gLnS~=>*dSKD69^%Wr+9aHwi>{~T zvX-WEgu65H%=4VxsC7Nyr-dl(>VyN2pq2y@Zp^k|inpVNBsn2o0}|pJQ>l)Q8uT@r z#mt8ZaTAab2QGN8oImgyv|0HA@{r75_A7CIi9b5aSqV^L2SRX_is0Q0PC{`tV2lDl zw9^g+4=GI?mK>tjHV2z7qAqKABNBQ>V{jSCQ*&w~s>#{9oRkpCMo5;vuSEnt!; zu*;>IbL%sYov8-%6LHs^$vMxySO|3L3_*U@!Tj__1wII!*7^G0eBI=AXaO?h1=Rj) z0h(!sO*d;Hub?#m%Jhe0T1ezR{W?9cEn_>rb)1XAy5}yQ)$6efaL~NLuU^fX$>-_lODn=a>0G9VAml`XSX^8 zJeq9>2)XyUotrXdkWzRFFj`XQ>#6N4eL4*0Ui&yQ3bfrEaG4BW+!k6|0Z4<>6ECM; zZEn;IFz>j7vSy1}`4PnDJ|(&=)(QFRCQ6 z;%NRGj1EE6``XSzn-w$}70nmq1e@I_Okt`?L;*I+DUgUhbiTkbt0uLy&2=w;6{u2a zdpE#TWh^^ZW%ie2YjHYG+W^JFMJj5PCR_#L#X=d1N?lANFr0=iFwvJ|15Jh-A}&R8 z4m6NKh)!S0$iwV1m&+cVPzy+P`5@eIP8Rx+MiTVDKsLp+&P=rakna$J$=eNwkJJSr zLC@IT9u=J3J{ZBuODfvuv^a>=wSE!dG2(v|Ktv=VnA!04RHXkR+zec+dAElp)^Q5} zY*%H$MxU0){|j2}w5?UOd=uDczyWbZ`-%<}FL(pAQ{2?F@Ckt{_>c=!y8wBx4=TO~ zF_WYE3vA*+0c2W5buzZ~5_Ei;uJyvvaGUQ!O6~Og~(v5X;uJ*7nV~{BRBps5x#i0m!X=kN^4P4BtrbXxy^Mhz1T) zkzl&(3D80R3Oe2G#uoe~2ol?z_y3AlJ^Y-#&IxFz`8?ojG1GRWYHP29DNfKx34uFK zr{i6Ar?j1m*$J0cax9+M_{j1XG~iU+xy2Y{^kQXY-$1M>kgCgr@&Ir06eKzS@4+Tj)ASt z#aPM0bQ?0#H#71$w9RGle>H4vYFQ6}4!mWnaH>Dye_?n}QY=G4bCl{~2LzQgSDk&h z4*G74J{*J2ROh#};W8Q3>D1{hy@VBVYnqc|JA_B|OZXHv=ni!zusR+CT}}|BT^ahU zB!SkZK?kU*>s2o&n#@V>p#Ni)p$%uv2F|tezu^8!w+GaZ>nDN9-qWd7d+eT@<{989 zoW5cpaBqfdJA9zA5T_Zz-^5|qkA@BnLk8-$%O;sZaM6y{N(6BO2Y|W)@zS4%10I8g zzl1WcO5!o=rxB12Xau^A$yR~+E89Q&IYDcuZ6mWJS(CC`4f_>CDje~%Ap|qY0Um=! zVm4XtMAPGr%WHtw@(bds05si_upjGK6GSHCvj*UhzZ@IzQEf96*Ky~dMrofh&>q9df~Fw< zg@#`U>r^SmaiBoB&HvRa7-ZPw44QIo))HF1<&OUa+84M&LyqI>ivG0)R0&Mn-sG>= ztN(7uf~slYTKJN1^lC8ZNGy0_^o}dT$bx>Jtcqb{#R5XdfuJuC#=B)5N+ZBd|3jq8 zz|c&Vu`}?CWTuHn6rhV!BG(6odBob{sDYHDOY_j7AX^y-j-SIuMEIK)a*6X|4H!Wh_XtU3j4BD^4?3zKPSTA3)$VIXlQQ)+U_A4A$r+V1v4Q1X*?YUsL;KxqS^5qtAYN8Oir=1Ouh5Viv zy5vSBCCHE;wzUa~m(lyDg6^haS21&5mKL8S-=W4uj{IKV0 zp0Et)JF)Pc) z06V;v{R(0in(0=Y_(@I3e@o^fNb~L_XeQiW2pSA#z>3*{*U=YykL(;dRT`r7+}zzO zokjt|%Uou)D`KaH9zSBU;vaWk8-wkBhbxk9e)V2|B`eN-9{+u8I?w;;Y~#qYH>e8i z!){|?vI+mQ0m9bB`fu^Pfm%F&fPQI%L{GXy9o|c%3b3DtSXUbx8?|64{n}(%_mYaT zGVRx`6)&kPisZ#aVU0ht@@j^yKIe}B)~|tN!}vE+Vvk-$;PLn;&Qh|n`XRO_tZPCZ zfa2DEw4wZlRiFZB79GmONJ>fRBqk^4-8-W12nxet2@JAuN`K^FxMc5l_U#8IGL$xL=BU_8NlHEoIT{;-A}p)%Zis?}W$x=~eau;W z6$1@KZbVH@y-w&glc_2ajT@F^nBDpmVIysKV$?*X<5H*Ha<&|CMUE5QBly$~g_fC;)%fJ14Qp4d zn13+#5YdWW^Gt=`pQ4XO)gV=^9q;qy>Zdbc=#jH;Q{YoECwc{vn!nnEsgcY ztMko`W4bLcR@NV@i66_k`U7H}$IGuDL~QZ&2edG@Fp7u4S_FrjdF3Gg1~BLXw_`lx zG|T=J)D=zxbV%9H#<0b4GfT>tB$P=c2Y3D2NIrJgAdv0T3@nU^j}+X_cBfWF`ZX;d zdPbvAwq@;e`(s1FE*Rx!x0!c_oRzCMk>j;o3*|3sn+eabr`m0{Q)tKghKW>XdwEhY zC%>;~BUM*P+sr0!FYMiF+Xy`6XES!=D}O)>LIN$xfmOh!8D?p3mt{O@j#(Ixcq!L^ z>EYnlev*C8Zp5CRAj^XLpdSA#SWp<+|GU}-b@ax5^ratb0lor{r1-sQOo7-lG5T&8dw%LndjVsg z8SKNX`KPOYUU-Oc=o+LAdV0pC_%;eNz%4RHOyeM?B1^co7M0hnG?U{qF&P=JEHsf; zkxZ4IipeYb9OY@AcLJ+51&QzA8f{3W_L6`jzyhpoxldhhxU{mX&jS*|UkjesUZNRj zd)dBUR{nG~@$p2d+E}HX46~IHT&5Sd5rv|@PXw2TKeC3aaXNTeKCr#CSz_R5d#)0{ zf=;y#2CmzXD&&QX~%q2{?Sgr<8-wBNaqnM3RmHLH10xPO0uJI zRe?p|kW%g~S*@m@Eq8KyerNx_&-Hq%sCb{=$o7&DLdTMIv7Ago zc10F?H_Qp6_12&*vA48Q5nIPfoNa*D=v9*%<9<#~?i?SF>`9iYnkd!MvO;KtU%r5?_G-G{2VW z$%Mvxj$L|Kz1zHP-(J2A=)1J0^r~&F`S#$1ncnH(lrU5bi*k}zg|-Qq$H*DrA^wI9 z0;~7|U3UR=BIg(3z+BH5&0}ZRc~uyg^B6-jhq#)qvQc>=5H~E@CxF3xkZEqS&9-}P zzfCYUbR;!pI<;Uvc6Kd>FaU_QzLbm1>FUsZN~exxFaAl=U7$Q7u=5lMdh8*rlxyY42-%j zHFRusF41QsKebgVyxx!pW^E%7QrJ|2SisKfH?DJ@rCn=)f(5T_n~6z-6K>2l_AUdm z^&BKI^zsHl7c$eXfM4*xZ-YPLsJ9P;ypd6PUp8U*1=oY6OL;k=+}~`(-lT3m)SSJ+ zrZ`sRDvSzKId80qTHR|RQQ=(lBA_)O1SkM)RjmnIna1~rD3 zV_&@3iFt=2T&VO91B_!?<`y?XIFMcoKqZxR7nnQ{{E)){@l8FF`v+<==IasDH=6rb zf@or=8&jrI(;J4KJQVFCDy3)23}jIh|zEe8QRVom$7B5c!+tK5Q0 zH~M8*%I4bSq{+HmnqPYgpoS|P=Gso6VU>HHsP-8Ne^M6~Co3)80e)ku4*YRJ>ta8v z&Szoyl1CzU5Qvk^Q%j-1MX?fACS{TA6L-7cGQnhI`lR%+voVg}yaK&IJ%$56x4&Pr zz12s1QF$wOZ9-%Zrw;mKa8~A*7KYv@g3?(OM52)5A+T}o4@mZxpVwXESE~)A_k1Qr z0Z#m}Q~G>rt7dXX^`*N!+p24G`njme@x}FZXJ_f?D?+UCy0Pz2{=*qXi~LP_k5w|2 zM}gzy@ZyHKoFnaRzY$<4?)~<-t;(5=2)^4b$rf8O{SNhs+k{W-d-zuW!>QCQYk7_h zWzsC}_~VA|lq%EbN~4x#&YH+1lbRpX@%=_KZ7q^hsW{?{=v3GRj1f{o#X!5gjO+Kp zg}46CoXyOEJ0kKAI}ta<$P+E5-7{(`Q0a$@3OR9gLrD}~Tk+Lc(B-WwwN|Gx?e3X* zg&5EqJe-bRwXTBRGesZew4?bjfU!PXLbNfe27}{pO?>qQUj@`#4i&boOnwcdy z2}zbAAXbS`UYgz6STv`v^iPakYED&O9(v|qI!OtE!qH0evx!fCmGf|Oi(g}Y zdE&nEHgxj%B95t4j;1hkmGk0ro!!73&_jl~LZs8&2O`1-#J>-h$pkM0J&f9SY=h5) z^U;~6ZqI%H{HJE-x1x}-wC!w?{g$L;Ebf9`re#MhX1YV45AHPExFvMebL)_TPzi7s z{Y{8)tI(dl@7yjKt$CV4xmCq$^}bhBlVZn4?ZIC5ikBu=qGe=8hm+UpXI4|2w6UG) zv5p2+K!kNLpFJ-k(!zUN^HD=X+n|l!o#YjlgN3oKtM~nK0-7hH#xN63ZN{Hz`yOs3 zDCF(U4Hfbrwx8}tdvRDvpDA8B=h*v*iFbNOux3WcHWBQbn3}RAumK0|Q~CD_Ke-mf zU_3EyOL!Zdv5n;$&~Lxg8CTOW4*@eou1EF<^ezE>1`&anTQeJIJP8H-`_Rj9(Db^0 z$UR&vU~g~#rsv8Wz2P91RxFBulFGW3Yq3hr_e zKjF1!crZ{HCrsSG9wX?8K9}o&K*as7R-9MQV=TiX&PWV6Wgj+vp$4C+ZbjI1qDf*tR z+t#vaf4Rj&RAtQWE1zwKR#TaTol8{f*aBAk8gqYUXaIKWrg`pk>M}C!ykU7~AHZ5j zrImdnH`X%ThyY^>+PSq)HY~q-gV_Y|+{0VEKvm^+AQuuA195-eqb)ca)#(2Ngq!`& zrv#O$odIvDO24zxS?t0`nzpw-dfN#goXv)#!OV_}Pn>*v@62j@$o z^_T@R`kIrJRU1LT2+E|3_$Y^jj*lHTI^Qgpk67(WY~MfllhstPJ48HjX>oI3#YSVc zae7BURCpoakh~Xg5lveBYPce=5gzWqGO{oXTI{)nm;nw2fK0J*ed6C)E76LU<19OwP8O>y2meIvDW^L*xCi9}ys_2R# z7>h|4DeqvdR!&+ClsjeSVNwC*6>Aex+QTjJwJV2|^$**QkVHr#K#qYDhC(EWgtHEp zHy-}`HpteP8K#&m417#VN;0$i9QUPLUuzwZ6P}P(k^Jalb6QbSc!|)3dM5t-GluIn zLfj=8?&zp~mv5wmBp)sC9P4CQ?WX0I=-7R7_{o!GCs$WVD{6iY@reqa+^vvWbz$1y znxpX1HdouuH8*DLsn6^|#BtADg9~nFlw_5U*r0m~x*4oqfOlcV8lN>w(-L}ew(+nq za-O{6&iW%CJMZ`j!Rw}z-T_vqUz=!+@R6h!`FzUaPfjIYbZ74mG2~g@g!FfnOp&g7}uS2BTvCE4=IufAG67Ss3m}gcpA*-zJy$3Iw;0s|}hNQWV<|(BmJYIO<+8#97@I7SmBW$MX75+i;bP^z3JMoEEg1<1`o; z^W;lzJs$h*_vC(}22xczxm)WVpIGObZ{QEA-Y+3Zao7Hrlkki?5AwNzd|$oBnS=1iW}AWKG^@6kJqr{5lGi$h@n1eOIK z%54geGrzC&7%VcL-eTKK0!)>8ta;rDFq6TJByn-^Hk%7f3SvIIA68BKH+sGb4_w!= zh|VU=z$dkqIhxSSee0S-$|zx_Nr#g*X9~KR@e}H!4PTE@%6w)H1iBTCkf45$W}th+ zMkch_IlBqPs9vb4Bk0%jZ02eV%hfyKxK%)LH6?SlvLs}!N1{YdFU?RH|H$0|bjbpN zMuO+~?9HQ8{%bXy^}hrhTtx)~81ZS-(?p`Rid7$Sj9mLc_vw@R!=Qqg3T|Vb*4P)E zHn}$=md)M0nm@jYV%q&q^w?bbSSItfV)^TRgLh*D}Hi|PA+vY_DX_57I zd;NAQ@$6R*&{o;XCAzDU8nsf-0O;`v^9+1y{2d(lNfD%hAIDEwwW`3dBWqJ^R6km7 zF?Ys0b~6tEhG$+kna~|)T+5|*qH)e8@u&=yf|n;gvl6`(c`m=2*_>mZPm~gG+Ri^k zFkO<0lAmqtSXz0c>cM(A8|DoY2zGvT3jES5+5!~7tMC$PBj~G~#OiWO17uDhkL`@XjShg)1uH`D|Y)`4;(E{99NRh?p+c|@vcafaO#7Jknj?``hy zdpbEf3bm>hyjS0uJ6JVo#&~3IDVR#;s{N zy&6~9Z}eBlx(U+KDcl>mI041v^PkQ$?Kn-bAX`^GJwQE#yV*j)5iwQLs`9o4FGV^i zzPwa7xytiZkSP`nP{B`V`v%D<{mFgtPMq}1GlUJ##f?nqD5-nlFF@)&IgG(Wh9y`~ zB|ERV!tlf&X)hY1210$?V4Z!Jm7-%`^$HSZ`U46?OuONS(80P}0^pv7|1lis^z$nh zmr(ciRoLtTePM6Cu#|OtK6kCZp+6zQUIpmC3fJD9uXs-zQe7O9Do%gtRls1~EVmd< zfm@&>kB)Xx-K}ye8IFX}RV!NmTU4<@;oO#NjrhERZx!fQLttKy>J z{+9d!QI}Qeq=Ol=o?#CH!M!u^a^z24SXV~JL z`GeLlbG$HH#VL|~pr68z{4I_4o16jYJAoPT?Kn2>G3Ok`E6mQPjIl6jsQ5CkZBqQ6 zt;BtIBwg=(p5U2!d}`P}VQd#=#dJ^)rfqR^e;l|`$J!g7%TXFzcT0C-6>lE2c(2>q z@h^sqR7T)lr;3I3v z4n*I35Bq`}Te%>#lfh8iB*EwB5z*2>Iv_2cjjU{LWXre6G;Jo@7d;H$OdN}CzXiX= z*NhNk@su&QUAD}wM^`lVv@|MDf0>2fPiA^()17xI#>AG6=ib-|vzaAZEqhbE+e!+wF$0a9gGpn+2tuScB?-J)b4Tl&vLB`$_54_~yr9|$Nd z?0sbRS>3qsnarwAO>(n2$Td_lTuD%EQS6O9f5fHKm0dkJld$~EWv74+{;S9y}@AFX;bvT zw%4TW&JC}I$nUAjSGH};#k@LtoWUXHmGDO0Gd?P`@mq#o-kcr))EWrKz;EaDr3Ix- zdlVQ@QV@Ou<`5aup(FgG6}$m7laA;e{QGOa#!x~DyXzF_6FYmULK~h;VV4hpdmt9t zrJSR3q4QRtY`h9z^qbzPZ-|_iCu}D3F@ut>CkL#W6}}VsCtPJ=@nz464s&z)L9YRd%E9L5#^bT;@wgzFyz1%6|Aq~dz;B~lw!8hUp+uLjvjMR&DHmFHD4M5397Aos=K(gn{G-(3T_KsC3`v;SUBn0dRmZ}JEnweQ^}@Q9is?3QTe zr03>RJ>pt#&5H~{I@lD$%@&_VW}90)`l`7dZ&_{HV_89dX3Dd4 zhi;)}=PhgW<;$0I#N|xReqBGQI!0vw7H7Ath89bn-)Rn6Br#5`R#AxrARA~V3o7{= zYz6rMKVzXGS>{tr>gp8_S$t#S!4!zgu(uMlvD>2y0UJmv5!f{CM!+{OP{7-DuNt~R z9oihPFU*I~)8C$&dd|E0jE#&^5vWH#jT{{r6b$fKP;r!ckJWC(bGc-k#*# zulzy$wy*_zOOv{Gd#VhY_SzD}Ch~mIV!o?pF2v_075Zpyt4+bh`h}l!-Tm`6BiS6O zxpE_cDPW4lQ?_7Fo~ZDH2(O6Rfb?PQnrbx?=1IA*5t89^xp3y;V5CR<6k<0{4}Z82 zxJG$(Y?=2jw?RYTo2Kxo??-+5oSOrM2ocpwx2?T@Pt!pASamPLb1TWQnao zHqF4iA*rL5d_Hf4|3}lNzwsG-V};hrWSmcma=VORIlu0&rRBT4biX@h_Z|~(+=WJT7GK8q)h)LR=zeA0QR%9KP#M$Jtv(uKm^DE_Px7!03{BXQ zO1o!^T9(TV!+!*0s*qCKq;!UyuOe~*%RhIVgg}=6KjhxtV5Stf%11XMrC$zn3jUqI z05dGQ0%^5|-nb8U`!V!74svv(PJIBJ?_zJd9_MI0Z#^>cQf^CV`T0u444-Six%`Il zigWmnysN|j50fDN_T|?U5vE(p9wd6o5@Fha*G$H7rM^LtIQ;X_ncf09R~MJkU_qC{ zh4b+DUCDpqVMx`9y8f}&^gZZY!amNMQl z7-+bg(H^CRQ*`65P1+jYZ;18z=Py>eZbi$_EFGVCY2azSo!5#o$*#4#B<-*5dwB=@ zc7BkLj)G0d18{ZgSkncts*emH1%R~)ZM#)Py1G30^Jj*PnO#ZX-c~MI`*sS#Q>}O- zI6=IyQZ7L}9C*>{<8$@8xg>)U)3HNh5pqm->m-#3$^jbR3#MUW-(bJ8-S>EF$aA0W zs!81LuKJzs-e!yPAkQynvtGA0`XpG)kN6W)lOvwilx|gFtT&#Wkp46lL^{XRg%M?4 zy3It%?1_HBr0PM*33%@{zq`C(NRxzD43!|EbrtBH>KJ}TnFV+RmLd_8hciIa!$1lx z4MnCZKg{k3JUU4MZY|m1sFkqo##EnG?Fwpe42R3UXI=1z0rm@aWvYcKxoG9Vi8PQ@ z=r3S&jkK>w97!e2&dgZOM3!hbGn@vt-;#%|KtEOV=j{_xKg|vx`*tdnr&eA%H=n{Xm9WDkM17B_d5v)1OlAU zykntUXyUIV6JAK*#gRrvyp*r(5R}IL6|#Ter+^0Y*qJ0GB&3@Z!9EUW)vSc&(OOd0 zIvn|6N405CvpY*T!SX$fN-QB+>Bu3ZNFJLHoJX4dzVs@YTtDz<3omRSNU3Pi{ zL&J{Occs?L1m4I}I1E;|=|1+6avFUpk95+dT-S=(-v@PtjISaEBDuqk({OEz4(~PN zLx9@hNFJEbo4rpBaWu0!uOO0wzt*To2_|UwPx>HaQ^U&UL(e~(V%_P=GO!ALjER!> z@>orbIFYtuUYt?34TifkpnOaU%s|;cecF3>h{!HC{Jiet9}rjkjUXSh&a(j{%9f=i z{jBDK2O;9ap!DnIhSDz-J6{W=Y{2BF*UH$g_`qzq!-*@@6h@yt#Wv$2h$eO00_y}5 z46=YczMvdrK4@?+&dPM0VW}ay|7GmJlmjs-6Jhp?7h#R;0b@6Nu_~~WGPwwbk0sv3KYEnu-tU&2nyEEd)Nj73XNne;Ut7`8kd-sp z1d}c%2jv2)`KX?E++zOm)vT9)V3xh5SeHE}ymUZxCAh`p=eX$T+|p(~0fd6pYqyiS z#4zo2ctWt`yalq1#jv{##6E|Et||nK-dKHt;K9EhmbcFx#z0H{$K^M`s_~yY?ixKv z=Ew#?ICEdbS=b!i}oocs2Fpv2Z#MA5WLH?SC*slyHj79Lqa@kL-!&aK$ie8xNGdt zs8E1elwv9Wc3*;%<^K#nY!x zsjE<9*L7WxAYoJ+ovUX0Q_SJZc991bAfxNsXeqO4@a8AvYeLA>QVf4K-uik{H@SI> zI{m+wn4jpAI;pJrA++nP^LCFa`hw|n1Z|by()9B3IgD6rj@C63Z~KaD8OA;2R2yQ} z&(n)}O!WBoK)#j&PQD=rYD(r9^awMqoO5AVh&Eu_CjiVibxMZsnpU3c#9E*%bQ{L$)t=0T*>-`9GS z>O%98$M%(N83TyYLJhw2BWC9pfNLWbeY)d8xxkmpBPL1q6{u>*@vO5a+9UB>eB3Lq z%dXw!klfT}YF^>hp;L>pEPZLS!4C!`risg zhBENg#!hEerr^%1X?@>j@#fw@?C(=6F-UG3jm0^VW zFs~qjY=Ubzw-fWNc3pxnAG)T|0){HHH|>b;4`(sFgk5PK6l@MmZdIEb=<-x9ei}%r zV9jb5FDWCV7x7r!Yx0P7;4=P^>4_}j*T|V(L{fhf~eoXWCAR zUxUL-*8jNm_nNKAYmRsU9bq>Gl;*Uf8rI{(Mo0>mtd`iEN`v9|z~G8Pn=<|pdSuc8 zfGQ-iEAV_L?$i{@mWoYA&}QYo-9JuYDr{7vS#p48J?KM&SzKLixQkZ@k(nzDq4NZNXhH; zxuOZj8-7lRb^0_Cyx*K^!*2^4gt5S~)8`U>lZ%b@4!#nF=6~LrKBP3Kr!>jF2n$J1 zc(05qf+cMR`c44J$O4nx`#=pWKBOxd6d0kQMB80Ll3FpgjmpCQ;l97w$Sn%52(&KD z8C->Y1>LKq!=YqA(DeVt;+>2y#IBj(dAT2>hMUy!Gq%mY;kSzz74AA^6?jZb;kN^2hYD{}j1K!Ab}}_Tze4@IscLXVC3_~+L{d#zGdy`(Y`bQm&CWO378@o{_1ytc&hw_ zPNT}w{5qj^#v@&BB$3ep4447a8P+ED#2U^rL5;`Y^Ubh2v#ArPx8BS&f0R5|{*T&Q zVxqj$*{k_pGswlo8o}!#>&h<1qEMH*Z7wSp-Ai2Im{c2xN>V(t;Fy- z0RhL?i=S9lS6!tvp{9d%1d>`mD`T9JYI6&YdJ%qN6qo2`FL*?yMfh)WP!sNsIl8%R zXMDT8D%^!3kw`OO_@|Zvup!Rs$O^e;?B4+ysV0y@wA~QptXEbJJnb#|IjdS9qj(gg;^xbOe*oVXc zvrZ=$m$ZWdXrV%s%tZ-&E~$SW;Cv61U?18f*^+p3=7)Dot3*wHHs_)4{M=jwW9jqw z`1qa(`s>ZGT?Th+QBFYtgThJ;nDDe)+#z5yjUk^wZ^$Iyn||qUEkjKSunBuV>6Q)F zDn`-_n)Hv)M!=t}1mja|W!fj4920x;{Cb^)RzVqct?bI(8ZK9z_UiaXZ7^DO&MN1J zo2x06$Ig6dmP8!Aq{uM?dH8U8Ylorhjupx)Fr{+m;o8py zu}G#tD$oRyFIP3DC}F~Wc%^j`RN7Rz)`Hqbq30RqTEqWZVkgLf#mY!XXxmj}c$X#K zyBg>Q6X6#BeFu!rE*!G1vF!T9PEp{1baeb0{t?vPqZ`H7rrHUQw5ljQx&@VTHo{34 z9o%F|>NnGpvgAKg>#vtyUKpgNm~p07{6O%5Mpb4IlK!COK3L*bOk{ z0sMs3el?-YfR*^h%F0?{l@*NG@#~GwT@i_Xi+2gz?cRH+lG2*4D(#|+Gdu|Jv0!+y z{DPTp*+0_4I#l0TUy4E{+WolCO6co2{wA#6ovBe~iLl~;uH?;)O?MAf!|$UJnQEn_ zeLX8*h%HLGko=LSZqShr5<*G8OhH*H)Gr|+R;*u6b!Q{ljxvcC7~@6ru*h4R0yL-$ z$2%H1AZ`lw?(*o1zo$$o_PUp3sqrdod-=I$LOAKR<_Fd2h*)RVyx(*AZDX4Ai6fYN zintvcKR~{DOP=zMH505jJ*v%r&sw5nq#`&ar~b@}t_D)7U$|vGW%kUalz2V$;5PTZFQ_hao^3-*@&Mg z6v~O&?~wrA(*+Irr=mM?_HK;3H2}Pv$fOXbA&h~`>9S`&$D5*mBxILwc!VM$SxgnO z4y@WVku%Jf#SS#z5`Ge?OMs!Y-Q=+MWdioVY~FtO#Q<00+C7`Cz7?KGC{TvFrd-Ht z0cGTWgF2qq-3m-W1qWm**(Nieljec-;7&{_pv79Q4avsVKjZbho?t+o*Ok?D@@(qL zR#Q`x&fM))k;E{ow(6wM){4x{JXo*EhyyVAs-OFg$R&BuaUu|*Qq+90q^7b`w}~P5 z|Iqd3(NMqd|9HI=ibTR-N_a)dE`zM4^eQ1)vuBFzhU|lsEqlq5u~f3JlYQU!Wh!NC z5yQxC?2P4mKdIiY_xtnvo#UJwe{}A1Kkxgxuj_F=9*>LlQj4zbGH)-_*xVzOr0S!o zspXquiD3;#q?$7kD!4=J1teW)7r*>*pf)k#0I~W74~YyPU3dm*Ne^l#{urOWS__0b zK=>@4KWKq{w+HSHdz#$})KhUJo_B=HQd-7Mxyq*J=!|ZtiulJylU1-Qpzx4qye08$3Un#oSDY7# z?I=J{9n(xoL@+y$N}Ly$Uqeg9zeblKs`t|flgPs~bx}e`Xj~8fXRh>;`P+<62Q)hS zRCgFj%13b*9{$EEPNyo9wurwbyzTBBo-5wS$olfLh=KmZ<<0k*E`3ssl8(gc`cBORf4wFZwTFg|5Cc5;b9H@ajVFyD{4)6}+ z|A+^FRYk(w6r`ZfeGh}1YQgJZnjlcUXgN2b;)>}q7s1MmtQ!vLep#nZi(z5m6HFnw zHAZ*!UoJMf*S(g#Daur~+FxKkQ>LpKI+2C;Y?44nia{)?;rPp6;`GKRz|=ymK)VvH z=rB}V`+0Q7T7!!PZJ-o}iq!oN2L^J|w})KLP#fi6k2w7+`2x|!5aK*} zIk~!)4nGdoXJt%SD}kN~&5legS>>ETwb!N}H66 zqV@pEF188$I=NEd;nE?Nspe9^Qr$-b#`fjQ@go``LK)QOI7)%wlvX8bNH)nH`Omz1 zT}lcgrm7K~HJGm?P7F3nR$;1rv{j>&+TzMnKQ+;;2><)o;ak@5u)0@Pvt}6#3g3r@ z@{7bZW{5NoegMH%Qf4zipRdjeH7;z`PlRPPuj~-@FI}USAr@IxGGkX9##C3QCN|2O z4JO_!jpDcL<*MmfevP;n7c*h#Gd=ekErs^g%%qlQXZHmU`&@~7PUJUwm4Lg#%xzYl zO1L6}YVwv`X6_AxMs^y_CPQX{->*EtP9>*X6ZN*G3$8YpRU)d0o&ua)Dd&#k1Jc7a6)Q`;Q z+sEnr)@~X6zMs%ougbITHJ<&CYeFrw7d60W=8n&j+^4@)Rt%E*P(^79;}V0WI_(At9O4@`41`i z{}Ou4;M(@q!}$d?um=hyJ_fLSk(#6%hylA~1J}gNGXO1_QRz)r75^q{z+#>v{E0dZ znsSasY2L0Va;$^}TYA1^R1B%lCGjH)=jLx z^FHB6Q%g7piHMXNW^NI;z3R10t)c@g~ zoa3Z0I#%WVu~CU^qe=cCjYv|w05knOko0YNbmt;~?nw;j>weX(FB+FETp#nDZ&u2I zCo~g5uMM=%G`GTK=mRb~Sj0F7G5`cmf%|yqh%yNnk$b_3-A2Ma*>l(5`3?UgD$rj;MWf2|^g9oGOe(slnFp|izOhz~ zR@nYx-^taGKJU&$qnCNuc;a;GJ*wQ;c-XL8-@hm9wv=ZSEgu#Aan3x>z-qRm`@9&v zX|68*#cD#_N&Th*a6Pi$TiP#Ik~+K8w)MB?z@X7b@qDQ+O;5y%jlNuc{%RRh96a&A zsY>`^npzrzL(HwKM@5sCz*xNfE9*@Ox*K?{C&%2?ZP_RAQI))#C@uM?J8GSF+3K($~m1f_+4s8C#8fY*Wctnvtwxdr$`pe2;u$MNl zKniVaF(cK`0J{1$u8>JN;BC3tXV;6nd?^W>**czjwgP@SbDiN@pkuUoJ$@15?E<}G z^V6e0jtU;aQ(`Ay#{LFcS`&O+Cmgxa$FHi+&m|;#UOzszA>sle)65BA9G~#~)_UW( z0eHBO3nCsWPBmq*1_Pe#S2Z=;^alz&j5et#MnrzxP1B{Pfw>0xfEsPpV0Co{e zuY1BGN*?=CIq~*qx_XOBc_Kd@%MHO0zVwT*C*j}L!CYt@kuf|J#e46;o;m=|p^jEwb17}so2 z@0NH4qoak6rHRypZYn)q+}?CV1|Dm%3b*kFugKXNhC4wWDXTXU*Byyx7GF~|9-%xd z_9@%=`uWbGO+fHKy{%y#lwnqGf&b0-xMdVOlysBa9(MCvTE|+_2BD@5N{SEM9iZ|| zmkuN$$0Ur7^g8?%oVWR#`E7_l$2# zhOQe{wrp@VGmzXj0LN|!H$cr3I?_lS;xFH;q-`F>r*Ct(NzyefwN&n!c(37neA2t& zQlq=Oaux9k`~pf2fiFF+WU#84rAM?K&9nxW#(K8wjdtldwhzcf;{UhSJ_ZZKmq5dR zCBA`D>Eobg#{&n&5R+FsrvudI#Kw5}9!}I|A$~aVQNFh{%s-21^^KGC=)AA_7p>ei z6M3fRx#aUMBF@~=r^8JtDC(XzBuYlm2!9g(1h)(>N`#kEA9jJy-o4M3EVbB|dLmN| zS#*L|?s=pN$j%8@Rexn?ce+3q{K&O8841L#{ka$i;B0G>PioLl0Hg5venqdP;3_Gl zy~8E}pKT?Lk3pMghRP&MyH59?Y#Ao;L&$rnSr2vDCK6^W1n8 zLrvdkhuH{ipen0ARi;Arq?}X}`7~Z^1s>^G}P}ytz2x2xwuF_8Q=Y z67%FojQxcSDl85!@xRP;rt0e7Idc=NYu!;ku8g-eR_C|2^hRXI;8%2#tYXlGZ3>N^ zaQ6^wzos?4GSZf>*rD9EfCuK8i9!$^O*QnFCO&W3S64Ggpbr3cafs#g@?X8>LloR1 z^ufEhBza@I^eT{XeLPY0ikIs;70(yL4VxpWq>7GpG_$#sn1W;|P7s}$f0;w5D^@V` zt_$f6u6t6f*qoffYEOL{sUb$(+7#^#p#d{%#Rf{xIo$3%5!b$kjV|0|T(1PW?k3K1 zUFEu))#ybPiIK~sap8Ls6{8_l(e2G zp#*JSYz7m%VM|L(X4)*w+~a!@Z2nY(`b+$SBb`qk@T}ubsS?UAC4=H&MrH$i_GfPz ztz1={vH&<=tY%HlMS)pfR<|kRs%4gz15R>`Q*G9r9sCOx_*_%uZ1C+e#U&LMY~l8M zJNA0L`H@hH!zy<`C|(_M`&PZlS|le^bF%R;$3be|MXTF)zqVer>yQWH*q=~h0DH{5 zOJG_6>Bn=#R|mIL?BLbM=Zq>uneqg}P86kA4y;zlg_jJ0{I~fb2M=cYxo7o^bVF?L zZPy2TlJJ)32J1GjSsiu?I$zUrM5-*V=S~tJQMDW~90|P6FQ5~c^PM(bPC%}aigG{{ z6blE_><(7M@qZtO2%mIHz&q7<&g?HA{BHW3A5m*6HVafqi0+cUSEaR$F0G9cnPoT8{RYjS>BETI zf}@w6$xHZ3F1gH)t5yCBtCK=o2|vBB2{(o9%(XgO>s+Fs3oxzFE|$elTDKhDgq@5} zO!PvW?K`b3VRfyd@e5~W7_K0v(#HqKde>9$PT(=xLMWCT*t#6Vl3cJh@bg5x7)NvW zcfl`wmxCho!u-#lt!8~;*dn|hzQwcMpw~LpFHm^G^E*um5@3p<$((TUsBn&8Cx3$; zmkN=7&1lfd`9yVJu_!FeM+nvzPjlrbUZ7?=yf5oCW!!3r_u|Rjy0rE$jbAnd{6>Zb zGqwtK+Y{91Is()|60&j4VDVs5FGxae7zbDX$>6|s_}TW<5=rI_JaQBym4kmf&Q7|4 z5#i-ubgVN&0h^{_@-?l2pM4_ ztd#uBgzvhc6byUwHROfdmZg!s-F5lU8Hp_AfWWP#B?o~_?fc~zPg1MV2@3F1T$O|T zdOEvMkY7*TL6Wrc;OX7ew7aR{SkIdg{~2i|2GBcoJC{l%RtSFzG&+`s>hZ(0B-zlNYg*vMlfLi55uSIq&02PyA4(Tzy*a#m z4UO+yf0ZZzUVW2w&^`DTC?(FY zmC*lR?)2&TVW#H9#32TT*zGX!&8o7p`@v~A9M0zMdaD+4v(9OtWhK=XTpH4e(pBb9uIH1J8^PFSbM3$MnzFXyu zldLuPIxyu+dzp&4(_eV8~sSeInttM^ktcqkmrrjd<66gLs*so^XSReCU zJqtKVrVXL;)hkq}&B~D8#)p-ENg=Y;)YO{dHm%mIV3>M~Z_g&`-j(%PSxlP$1e4;w zQ#B1L`ET{CPp7QBe^G25F70i!>?W=bCo! z_2Woo1RGs2S$swUc7}t)Td&2U*AXa&da0feR73`Qo#Pa(`9_0g;G4NQDj*v*;Lr}) z2zgFZB%3t(@+Ls~0gIr)CQuRatEXZOjZ>Zw{BvhP`d;|(+;uU|A0bU>rW++{!j&Hf z#yx`CTng%Bn~Dq>3l46rh@?9@6!hLPz8VH2;)AHcz`o=!OqikqR%vX{JyvS08TT#_ zyhMxXbr;062Tz`r&~au^UXYGu#veyHZA;3C=O)?X5-VVVUShpo-~a~7`1lw?xRU=i zRLp?u8v6{Ad$|7l20w07&~UrJ7o;L0DYO&U_!m!(hRBfSaSvBldJlo+Z<^Ki--QVA z_ImReK0#|*VqGr;)wNy)Bah@$gCy`1BUqSy?CJd>%4!9dQ+2nnImv8JD?5NNae43Z zUZGiBAZVPdCT+>m&LY97Q(6-lTYw$JI62zbmGf~ctEjLAYRf*Y6<>>45znL*l z(90=ym8n-ajI0P^Mrm*iKGVyTU!oW!>QYRRBE5l?EQGL(`O)&Z4d~jC$7W|kgtC+M zlv@-oQH}}RcquV<;;+o!;*O%NznuqK6e?eEHxF|@wr$ktx>~RSf?{b9#R4aKSm(a` zNLAcS%Y#7Aq*a$$Qxh3*fA=ei4mJScgv_v&t=%czAG9b>k;6=uwDh`3`t=0w-;_;Q zU}%Y}p_O%Ozs%q~)n7iF8wS^GCFSLHRwH#UR;<$0Te#V^Z<7J@A}^==?*)>dZjxxP zA#i_&^XK48#t@-)5=~!F^ba&HR9%dw!#-O1hTKfaILomPpNY$j^k;Pz9O(uK-GU;j zH9!M;a*Z{XCI&X39!0(Z<;Owv5rV^magXZXEX;E^DNRSKGp;-V#ptqia70^HM$`OP z?4TwGaJ8X-{wDBP+M3%uo;N;vJ=N?o-&}z31-?rvoclwCx{a3~ie7ZvY9EpZH2dXIf_1aYp zP_`1d0e@aH81+vOxvZtfK;C;7{rzO<{r*>76Q^ju;+`s|LLK4tbrNvxnI;m=n2-rJ zQ7MGQtDSqzx%@k?abS4;;#3Y2+gn$&v(w>hjgNs=*%NbSBrEC6fAy0g%^5BB^nbE; zFi;AhH8FH2PwJ>2wFo=xF~7cY#geiAbVzfvdtmST<7lubs$3Tl`fpDma9jq6rH_X< z-;E4?_e>~$A~G1F0qs)1jFIEws98K}{JK^l5~m1ftMA+|Z>^3~vSFXBWTn6!@FP|0+wjtkpIi2%=Y@yT!z!?sfrz<>t}&-s8Er6YHZgy3*8i zl=)a1uc3ytygYAjSD;89RWC1$#l(b~f-Zgxa63}GN{(Pxkids2E}!ymGv&|q8WSK&)5%<3E5p*vXr;m%p7=RIurp?RfFJ%R90|iWwFcG~c$z&jFJ zr;sg+usafh7Hbwaq`<%~B!S(XAm4>Aaqe&Kc=MrmAxFLc1W0JVh3T2jA{+Rbe=7I= zz#yNE7WMEHJKD3bFEC)#yR5^=6_cW6yzxeRT=h=&=-Wp zBe)CS-J!$q#l0jD?o!G}Pqn0MkIH|{&!66B|7VrNEi$^<|Q z^gIZds{KU9hKeJFdD=!wV-1muncNe@!%_q04aRU>o#D{( z^z`lX!pry%Rxsb5rSArtg7br#w($ig=6Bp^eIIjgpQ4)UQ}P1YH`vE>e9%5_d58z6 za11U1a^d%u7L5G8!Tz1`BfyAQVby}fDJQ%b3pOb>f@L44I|LfH5l;xH+2)GGr~k_5 zVZlD>mrt`l!aiVTqHQj>Rl$=oEl3DnT$le^fB&0}zuzaK1)kgMRb#|P{Qf!D%&mSH z1+u~QNfbmOqz~UUU)sGn^lfDCdYB#G5X9VDUb>BNn*cf^re_}jVx|_*j%hF;k%9Qi z$;+!c=s7V(D|$FapYVpjdggteJkGM4Wzj4&%rA z^xH95&AtKW{x$-bn7_Hj--J3t*}^5$d0?ESBSUx4DXFhB{KOpSS_i9fMc%@TQm0K+ zlr0--y1!mnA_()T1{hZ&WaLLW&lJpuzsRMPd>sV%4R=VJp!-x;S63cB$P-QX@?JL8 zozMR8T-IxguJidmMdz?@C?W?Ir2ug%SXsme_!IpbdI{;O{lFfEl;~YOa&XP}KXUD9 zT5DUQ027NKkJnx*c`?i$j$+%R4FRjEYk-swJD-?n4ee#&=p#(EN*k?i{Vqg;JDOs&dMKUeZ)?Scbc+eI? zTj%Ut&nPY61HQMU@n4gP43p5-eGxUO!@vJe8>P1PJ?mX{x4k2c&AVRgZ(38a>eOk9 z53iZLY93Z5#M!gylnHa>g@hZdFHIGmjC*)Y>A5 zqe#Qtez;!Hh+*|)K~I4P=*!OsjO8Eo06bfS8DW6cK7xgS6deW~Rq?Q&c~Fyr>6`w- z{5)J~XOGTY+>T5=7&1kJKQ#!0C4o1trR1V&f1xJS2j`-_5 zPGOYAYd;0scft#5S0jHj^GX{IYO1H~3}a6JX?)SmheIq3E&5 z6KQ591zRgXsvO!B+GL(oqSjS8v(ODV&8t#l6bFzB;Gj`P^RWCYO9*~g%d@)sA}Vu; zY;&17-dVIZNCstKRT)6%9wpxy5x+eG78j-y=e@nbpx?*lm%}Ml#jte!Q7crp?d;sv zVfTA0yY;Rwdv41xiDLwKhNKn=|F|m@y^e(=HBF~bQWn0kR8&-18w;Lti|eR4T;gnh zfRC%PW=Io30hzQwrkfYOO9e`jnZ;z+@tgY*uGP<{U&7Zx#Mhbn_Mdp152pl~M!q}X zn!DG@A~H93l}83^ja6k{H}1{g^YAEkcC+(E`ufWuy!BHuJ6uNEBhtXtZ?c?B_9zn+ z6Y(AoT~w*$fn_-WmszkAbYQ??WE#D4joY#r)7V=x1Nt2tQEl5}TUKuJkco@e`+-6V z_3v{CvtdVpwS{&CF=m&ahouesNt@-dSUH1z3(7wLhnjiuF)<1Hd7+EHxIw~z2o@I< zhP_(lJ2|%%lIhSvVfg0O>ZX4e{qYawY2>f0Cjoh7t0>x47C09MyH$o=kyllW@1@35 ze_L8s6OgKPMBK5_SMxtjj7M-eSJ{ADnytG`l+qAMwZmqe1D2n5R~fQCMD+xbeb&7w5XPA=RZ4+6@L&9RT5v|Cvwf?H_GzFS@V-ogOy|Z<$`CIjZ9KE`3P*Hm1mN zcY_P!H;%ncI0;bM0=aX5C&t(E=!%jZ`r)1aFbgbB751}Rq0;XML=V_KM`xY?z=b`v zm=*hESI#mPi0X>4+NsFwuc#TXiN-mEafY1XK11^a1fBW&i;k0F`qkkh6&f4&vWWzo zQg>YwdTr+Z?j+Cy;orV!v5v3*f)-x%i*CGp|Yr-+`=SgCjRcDIfl zgukagaWDv$U%9OvWm!@%SM-6_;gawaS_ja2(_GPqP66uZyN$VRSc65UTQuNcxvHj$;RD@V$n8lujF`Xic zlfs+6i1b#Lm+Jw||FTvp+4i$tz{qUbDSel5es7`{|I|HIwFT&p>nN!*)p};FP$G}vf;;QP#I2=IIt@+&Pi8{ z#kKfyPJWvj$=F2gY?t!x6O6DW%anUGs(4(t$Bile3b&!P$ z&^BILP<(ni?@>|U?s>e?QGs6NkkwQ@U3ka^?9M*+f)(l9p^=ujnq zEEPx_CxItMwmD=(W4NVgnfTOugZC%}=a^0A_nUrIr`HX}WKCybRHkF}k?micnw_(c ziDOSc>p1T)|7PZ)$CQ^19qSY~scJ=6cVJncgLzpQIlOENkppU5++U;YeWX@9%PJLw-#sCR6 zbWm?EHoE#269&D*hCZ!)N98+!0>S(YoP_l9vNn~byLlo0<^SjIIzaVO?Z4`!yJsk{ z3~ZPL>{Dj;_dJrus`iz;Zc>reY@2pR=+qy5Xr5fYvqJ#lqMAATn1H#sZQ>7|v%7sv zEGki1^cH~$m7Y!V+Zr?!fI6>-vrA4|R0^FpU{rP~xi5h2k+}(X4T6=Im3?;6r^>NC zQ&{XewTB@p|x)f5k;D5qd49xXe)X8+le;M?P%pM-uj=%T607eFAsHLC35mIy0MgNJPd z<&_KnIArZ%6qqtNm`U?I#S5`DP3(F|$6=DJ8CK*WQ2|nAD-xI-k$qCAXJIU1Gur=I&RZEu&^GPGn1W7`5x(?>=2R4JLPX%C=TyP+vZGc*V5+=6?p%EI zpk-@RX#P!cf~c!4-HcSLhJTG^D?{R8tIulJIo_2MWB41{55IrE*x%pJ%Pvm!YRsh% z-~~x;>+=bULNpj<2x2ERANFw5G}aRW=s12xAJtJ(0rj0vY#u(is=|~&n@FSoHyZ^T zJ_7x#YJ#;!`J{en-9Lb8jy979b07N!ohME0X2}Q)z_d+gttyP%P}d|46LF3G&Pjf; zmwaWhB%IQw<9B#pu-uE9rF9k#lDYt`BrW+&2VlncQa0{={`}cJgo&h~EVu|SZLXb! zdPO{qjZ0uXl`=`ej&l1bl@HK&|F?5a`ZFJTN{KwRaLaodw28_!sdyQTW^}iG&#sxq zEYN0{dX9R+Mhh#om5qWY1CKXz&c4vW^{yBc$tj0OG#IP5?Q4S^_(7%%5u;|d8J~Ru zx=Ma7DhB1rHq&EL@1^YN?RYP}s~QM&qFpfurNwS>!U0ynDhCKAH=)R0T#dal24zQ8n^f!7S_9VayZBdB_?NKaWgaG#E~ zUM*}S^tcE1u$R64;tY|raLR_B*4IQtGI08yaz=USbz`BaH<;(DB#&D-!|4LA?F~|7 zgwVb;*OovHiWh}Sw+sfRfY#C{^0q-K7>V09jLI!nlO+{&ww5iu<4S|2u643LPYGmo zRztb~3pXjyyJGWpT$&uV*esBX?;YMSv)jLw=}{2mH}~fn9HO`(rU2Vsd6-kIvZ}{x zmY79;ugJ}eJvRK^Ic1p{!`q7iR3KXOo%;2}NmN0`$1jL8r*o*V%eddkRnONY(f{zT5EpH<2#}6EW^Q|fQ z=khTDBQ_?WHrcuYm zTO8w+33Dm=Zcr)CB#>;mVPIoF;@~=ZsX8vm2Y#{>VLBC?1(XfhlXDn(&gifqKy0X3 z;$g*#+_1jxv}_{CcMqHkb$Oe)g1#c$#j)fyD+F>yQbCMgGk!ZaC zp<&%m+uk3gICym-gusW3^)YijlDpGvCWi6(E8|h?_Y&y$B}dp10TiG?YvV8QmV-M! znk6~{A9vh>FEgfr3{r*O`fU1YAs=;|E*%D4#BV&YsKjGB^+-hLTG%m^*UD#Na38;q zjtxAfM^cz!rYTb`+)5|@qj?zgeGi5f>{s7S3NNp*MnoND^#*d}Kd=xzdx6SoCjGH+ z&z&!MF%3kwDa6;^?PYDvjNs7xbm4mAO|n%WkVej|Ezlz@$+j9V3yLQ*H{H18{YFd; z^p+jn5pFPwO|girMShTp-r<+BRg{Ff+4IR9XV0#DrjfL+csMLeUPEVZ6?)ya298OP z=TLzw(R_bjVqZb|DCNJ7-3<|XuBcQ|q$YE$L%zXukt5p1F{AkFJeqf$v+wSlnDJf+ zleBNoD%q7^`*w{g9BpYNV7yhppR=Q)B|XN!X=yR*F}xjX$G6agmTJC>+`7=%kJ>Q< z0K$fa7X*?!p-|ZJii%~k3s!du@U@(zKW&IpL~*dLyY)K+WrvwC z)t)$NackAeQ%t1BbMdIy+pWUgzXRlMv8PUa+FC4JYZ4+Kl&@eGjD^AC2h_2r9!f9Z z2k#g#ZU;q|)dTgw^T-Dw-ty~q(>me>-i9Qt_wpDW5k~8;EnK5dJmmyLT0U4WL#GQF z-_xT(z|{9ngCmHq4j#Gq1|{SMywUrb6*?m^K^vTGRvpK%mlFp*z9LToPTHQu_X~VD z;;_*iqX#TD4B#ng1p>DH2wn&r6X88ubTl0Lob6!zQF;Ce)~nh^5hT{6-_n~0=ZyS7 z2+kiq60CE$U}n;JuHsgA7-6w3Y`IT_uTV)fFenF5*-xhCOOHO0t&V>du9wFe#lW2e z2ru{KE?MUYHmCS5ENSPIwK+z$tyL=)aqVfqCaV zC`}#L0d!gQ#sh_VKw(W&fUHXFAI}|X`+mxE9f)t7j7qDjqFDH*I=Dsxba}VkpFAlH zYDXzQ92650VyOZ3ZkOja5vU(IsJ81})Tg2r+Lz9FlXSXhFYnwAN!umK@3sK0QkSGv zjP8}|-!fN&LSG@1Gb+8jz4MH7ABO3Qh7ZVj%V~MKdvOF($i>D%H6|ub1m6OKqDeVx ztI-9D+VaEW#=A|w7X9`Fg}R>z!UdU(SDKIMw6+b&I?j_i(5NYpWRV$UXlU3E%QHdB zERm{{4kBSyI0U$@#qJy_SlM;nAa=2PAatszOs zW${!20|o45f6I36uEBHf|9Yf?o4DV25lrUzq?r^}&uA^x89m$LB?Z0A2NO{C9hB)n zYIW4Ml{(+yuaLZrpc zV8cv9^33Kga1%#cR+*|>X{)4spx-Hc!J5=|X;M7OK+(5V!fn%Eegk<7cw`Lxl)onV zG3(<>xYtVZ>d1?frt4iIpF`5FUAPa$DO8A$Ep4NF14!rGcUtunB?cdaR^2uM)v>Db z4^4Iygwg}E9p)m2qmAgRmZ_M$})RwWzoW;!F2%L7vg zUDv~RPIFEFM83~^VMQXycs*RVm^(`q`^?dkROs9+LXHOBOtsfmEofuf^PLyW@LTz% z%p3<7$cK|wx3lQ&zltNUK9w60K;WnqIO9+2Znd3z!k)2?t!#ezj*64J+xyDcbMb;W zb5GN+E(4oo8K>Kg=4PWX31h1xqYbbUVlQIuV&p|H{o;{pKY*64#8TtFI}qTo%hL@D zVlvK;zyA3=Stx8eQrXoAcOg~eQvwJ~6@#?q*4oD*-h|%qtbrF=&_&-AiAcFid12G1 zo)`U_`v>N%nkGvks!Tu6jE>edmK5AzmDe5uaej0bTj$lS9M9x!pJ%K0r7Q{bf7MjWjhi#m6_Z zmax|(J7yz*>H<6)N9%x}Sp)1LrH?bSIy@)F<*LWBzJfv^vtr9hTZeLC--^I|OTy0)yB-bp5?Js#K z{d#v+#QB8$@X*j=@~XDgJZvMxrwsHvmMjC6F~_SQ%rGxs4_zI!c|+6uWG+G2Yl?7e zPQthzj8;mQlg;rYbyNeBWlxxgBX+X74{E#u>-Hw0kf-@*>dlCg`>!~N?vG*lFUgl_ zX1TS;JA_!NKR>a4-D1_<&$|HQjPD+lA2XNRYs4V9!f$-L?iGtWsc4fY&xdzW^jsjL z_hNnM?Uii9E4@IR?)R`sK;%bm+#;k3(emonuDjY zL?o2CqJNrDkxd#>hc3j@OLH=ag@S{b9?3Q=bqDVeBay+pXsQmkj6~- ztsps19eKVV z0A)U%J3!>F=6A^5#od57C*UMn_Q9a1vTAm1O?%JE>t)jx@zq$~S92{-y%8P0ih(MO z;Y`Dr9{SgEwnR<1EJS`4#?UHFoGGn`0@@ z0ojUaU2PV7vh%}rE!mZow+*acN#tEM9$SuC>P&lO_49cD!CH#$|sf2L-Y1+ z;8etx1OOl_>!FVe8*7g=5DCFPfm4z{P9fuf%tZ}}Goxk0a+Jx$w12Ug`&zkqF7YDW z)54uZ+>0ha9&HRKNyi{80yb(DSZN!trW1c4Pv`t75#4T9+b#?SZ**S-$FBq%Sg|H* z@6Xiy^*)(mj36AqM2TGbo#Fqf-ruMQG=k`G)f^QaH$0GHnpoBGcomo-oUJOKtMg@!d#b z5BRi^1Fs=58Ba7Sz$-zW94RG|A<*DjC?|6;;0rp5x_lsS$b9?zj)1jx)+=bfv2DDj zZs?Xlv1Ev|VI2Hh>rSEhTZSp6Re5JCyON7GHzX4RI8s-zR^gL`)K?cVVdgz$)=;B? z*#xjmr^7q3W^a&PO@j!mmxtQH2Lx*NEC`bWa*UofD`mu|8#$pixX5UW zZ)rUP>?5r3c37J%o)OCM_> zZ&U+8+`TEWDA%K_|39JenLcNu;K2;p}ynF0zwPbo*8bg{&Q6I3W9urS4Gf|0mi+(hI z6$r*g%}JHC^7zCKAQ)*?jTNT&VW>PCoed6LkX zA9LmsW!zQY!XL;#GYDfWVq zEY9W8rfp0O03kvgb4v6a)jAJhS0p#Z=sX)S^zV&>C~#XTsM+=~!k`G>2dXrzSA8{M zFh-^Si3RkqZl;JB&n)Y4Fn1JvoGZ(Rz;K|O;~(cSYCXDAs8H~$r~-XpvvWpi?r3!7 z@HilY<+rUq=C>@kV&^pxgWh4xW41FI>{MIccwz5;wt`TFt^i7R_k;fa%NaYg8TahWt@#Q;R zR+9vmX^#p(?RR%{Ea-j#WV&d}@81T<|Ij{RP-ds0!`Y~eoc`vi==;6~W~+gKL<}bYD;o zw%?)Z$({Qgs^(S(1>*ZF_*(xzX79D*v@$X>X!nGr@88d$K5-K(+4_$ZL?0u@<34Mi z3x3Y#Sd6^2ZtL5_sb}uuW_@cmd^5X?vWlcGD{|US=S3wnxN%E}2Y%~SaO}yZ zQm!KvoI%#6>6<&imHQhYfKIQiMVp9z-nv1p0fCxpO{=R#Bqy$lj+1KZ>~*HXtOVZT zFD=l=20AI6(w;dyJP)8(RhOZqBh6)g1>&!bwd2>`#Dz@U|Cb&X^Z@x0Hz%t4egsVi zS#Ok`u(|DGeq7PIIMfHY4?88Qb6z;X;oD;|%RlnX&Rpx|^a_evw(7(?)sglN!iJLu z|Bmj3GkYT-yEkbA;)+gA^HcLz#--^NmYQ9it+#Xnx|<6w%7NKvNn)SIS*ZdV{rfIN zg75rjOQn*;zQ3iNz(0TG4o$e@mG4#O&-_ke?upVf)tget9Qm5*jBZQBa|XA(Ch0C$ zyuN~rGn=8t93UwA8(&(o5^ysQn>-gS z=ra2$g#5mS>!A=n+_7<|Z@sdn#yI@Vd#>3&ytAw|xDh8PP?E`Q;=NcDr^M?P;PA18 z=IQ=5+JOk>u@C(9@A&+X_A*U3X?gg0fQN1J=9~+27wwD9LS&Q4S5JXR88ygB?T!@$FB-^cOzm7$y$7?dXX- z5Qr+<_#JL(zRm`)XpC-y!dVb+z$4U1A;0nfRF}3}_5|rtsF(!sB@bi!A*+59lKkFp zxePtiY0!kiWAssD9E?d~qfD))*YAXM)Rv5RWu5A84Dg{&Yk8ziXQJkoWkh*Rk_~A> zE3@O@2?WzA@ZB>dUl5?Z$$$7R>}ZrkKj z9eO*eyI^e+mp|RJN&W{;`YcG9U0|XJvC5#E`UK?i?qgjOxpXAAtI%{B2nkU*LEG4@ zU4R0ByfP*$yg43`)Z}&ol}He~%sy>&QO+xbK{ed22afy)^JQxjl<8IscQp}6a$#}O z!0_jjes;;maa%^Y#w^n3HzIZ2-*ifCSKO(Yd=)vS;I)#aepD7pqhf0nAiazsP@dR* z-z}0e_?HBw55zo1wclF>mKoBmpve>*nnNHUqne5rCyH8loRMm?1`E=uaa}p|e}8Oq zMJY+jCJLb1qA`Fs@d7aMC$f*X5LBYHnM9jBr^=Fa*&0qXbj2isy`(f=S-IV>slE~&R=F*z9se_OdSylzJ&sPG zZ}Kz(xSh2S#eZHt2hn^2^OERV zI*2>Nj#U>1Zv@@=Gf`Gy7NH)yvCxtIHJS)QoCC!Empk~VA%) zc*5h9>d6vwTkLyTw~fmVT8+Bnf^uyQyvVJ04Ei9GTN1+RXj?5dep#Ksh&bwCtfWqd zyhFr?klv(QkH9~9gXZ6^rgxl{_Z3P)?n`eun@lx*mbMX{u@~WNP!S6hGQSS~gWBso zrWC)QE0D`w0Oy7g7(r@dIY%DdE%(KpipHc1_$-$tkwB(m#cA3&IAn8ia~DWEv??cZ zU)}Gnn(NAXsQ3`$p~Cq8$Vgd#UPEI%vQB zYbhlx9CVi_fa){l;e$UwfC~}%HbaI9vjCbwh@!K{mTdTOEvc3H{DTtLiXNu)~HPhR?V*9dLg-QRQwdl^A&KUrs_i4P}9pf1t15QR4^GYQhZzR(*g zd&lX)xvkRHo0)2aRA6@?EhN+%Fj*wOyFbSD5an&at}6f1_2$*NuiCeMd05^>P^g@L zn|`G!CW-vT%%R_!P9Y!`h{$Sy>sp5fXVkW?=H# z=Xs62urcl$(QGTrB5(jHVT!A&~!w~sZTN6 z9--Ly{KEK%hGl=<3yA(I)gsSr2Gx=hExHgZ#WTB>nHQU3L25ILQwRu2q4#1S(;9i( zHGTn|w(XVXB;w+YZ941|_~X#B{tB0c@zb3r%yO-2PTRdb&aC+1lwIodmS<942?u;^ zP-$g+?!*SH{^i1$IX~1BfsA5Q*`fh5g%wNeXWd6}EltjCUei0Fv`qU9nv#2Pp|Y!4 zv^pVy5axrrO^}LQI1PL2l5cEmfKPh;&u^o1Hfqq0n#Sb-Yq29?Mmq-b8lg$aR~BQp zOSQkx(ktWVM)U~yx(7pHZO$5>K|X;r!WY>Cbav$tAUE$%s3$&X*; zWN1M)ULS8TX>(^xZ2r+EZhx5>ihUMfR=2r3*d#SOB)mZ6R|cwJ{_c*z$$6DVGFHvgp8C13(79O!6!J5$p-I30viqw$E@M>>QXe`Q23Gel0cZ z&pQWv(b%v!;84eBbg>JO)DnPa>R>T7HKK&NTFeZE-xn?K7#TgbG z4OW#_B;?ia!ViULXfvslp;b)-&Dei&BHxNrU`gIk8>uy@hs;Bpqz1D-jhFYSNAJpo z-Bx&+%%wxZ$LxXuUR`N2^WuG*%ORugo}d$?dbc7%#FaarIw^R{9qHav0;J-g8#z2@ zP8HNt8?o_txX+~P*iA*rCByAIO|@pLQ?7)1&2^L^EfSc_^#%m=pY)zR7z3?r`nKq< z*)1hf_N#+~4}DiK@lw}5=yopWZJ?vhm)!nmk-*fn*qSx*q zcXX1Km&o8zCtqadLPW33COYY&CA7G>W187cd(ai|LNnh}AH2Po$bghbBsMqCw&(s& z6aB6{z*V9I2WW=3p2vi5!uBsN7)-^!%Xuh}PXU3Io5T_GUGJO;pI^W1$ayyyopSt> z-E1LvKhR;EZZs6jqMyby06CYWyE$MG!dS)dVAz#idc-Cu1thZaFFtStrx=6bbmw#? zNU{ihaYNkbSox9>AA<0R%b%cLW=K%v1pP8NL>HJiJJzCt?y%g_8Xj z2^Bqyj8TuZT9cZKr9=lcD@DTudjNC((Uw4qL4F}ZJW47VM_wF#jw&lk)BXwOd6mUOhVnkPd4y?ZmJMVQUPe46ZE??8`4z#^melwlE~Yx;C*230M?0u z)y4znS@7MnOrv?Zx!Rom{o41pK);cRoy4as#O>v1@^Ks?`IujIV2R!H;YyVH`Hyoh zXgXt%q8LDs24tEsvNn64!`AMS0ro%y%KrxR)6;hTJh(upB}yOV9f!yGDVVfTOxob) zwHkRMO}W%4<3iU{bPjRFcSowZJvTN@mTkPM9tx%z{@|or_Qjoy20y#5gObrav8{d?+@0Fbj3{_`n0kz7K*_bVU!O12~@@$M%4p z1q-GGMCuEhoW_)6S}V#pb!9?9f8hz#{Fg-8mdW$c-4gP|)?mbm$)a$o0#YKxMpc^h z8dQ2$O6Z{`^Z+3V5R%*#bbsGI-zoS1@3?22ea2u6hl4kHle}xKS)Mi5obxFWsP65o zLVpT#@5@|RO)vl4lL*q)*FX8q816g(8+5Ogpq1cX9wryG-pd=T21cKE-LF0Z!2o>| ztt{_Zvzp|XQ_0QEEh^~?c05h5%5B;N+3XvQo)ouD2si$e$TN3@U(n$6uCP5MOI=H_ zegde*wALMp~tlpFEndRk} z@E+5FfNPg&IWL;ufg{dPR0HwOshJ-x+azK(MKZuCa|Jbz>9MEOELlgYyccI_+N|}< z(}&{ej5u$W%N=UR-}}OB@;f4hx?h)pawP`NtW-O<9$Q4d4rRP@-i0RZfpJiAGy@Ep zn1BKv0TtyR{PHIm*t-S5TwNuIsd^H49L!nF>pD-6+WDf?XtdE6dnE>J^GF{^nt}an z=codObCZrmYC=lS+W(6j2^iuVR$!_|tA`g8j=qjR zstM_*Zp}0tK^b$@z!R`J7|qGfc%PDMY7ns9KSsu4vECsdv+}NbNX{tC1(v@`?CWEO zGn}M7eq#YW)R@{e(-ir=d2`cdSAKKP78Fw_b> zE0ZWF{%gD2QE&nv^YAYjOB?kMquVnN_6m9U!X6}jEF3hYGc`BQYlyg!wO9#ktX61+ z^K$JZ7tIm?oEUvxSG#=U@*{;o>o?ZCWZ|Hk!=2gS`UeZQ9N#c5& zJKS>JZ8{&)43DneJ5L%fMceNW`&Gb^0_!W9;HL2g90UW_L8lC|HY;5%ux@wBW2jIOl@@#z*!Gl>NK?u&a%kp^mzV6eUU%9o?sLFzqoEdEhk8_ z75@viE3kc={fG^kM7suKm^PU1gjhYks}j(Dk*SRD_U8nKZw@1fR-dIVZ=iHzMg&dt z{e}nOK~Tq*9DYh80l53f$3xgEv{M%rHL=9l{lzSBIK8+J)X09EN|3vCGWbK8`}7eA7cTHelw!GG1k!j(HrzX$zNr_kn%O#v2y}G=U9S~~F&infG>p!=&duWnD zRp+~GnHMRJs-V5H@T5VuZUx{@mT95=q-7TO)mY$hiK=1v32T0pHRI?mO}X>3ea*Pr zY1zlSMQ8k#(Qa!7{3lF2`c@fZnm7g2&uB5@&z?;|x+h7Q=WnArINbD`z4CLS7ZfTDLQnW?j|Tr?a#E{=1j<) z5*MXp5X7K~4%i_?nETzciQA2CRBc|fdT7+T@1Om)o8v<}9GUfMV-#dQTTMvI)LUV{ zJlPw_k5^GuK2*D+W-3eH zMpyG1)0PROSd{L$S#W^fh0$rNNr!6mw?M(AB#+?olN|P$)xX7Ppkq&=eukA`x8^^( z=v_#dv$DMKgx1-X`kZH*#HqkTwXAS9pQO)L$2U`&U<-$bY^WU~GkPTFACYwy$Nk1FJX zd10>~!#h3+{E@ispEkY<%CgP8^W?|N1TSmj`cpE8v_)!^@`weCigJTC_vUHZUM6bm z=%{jybssE4i}k#BrTHW?-^>%diluPrw~J$R&kceVJf=`v z1Y6OwX`Rr$5-Bi9hAB^stUX)XG}_a>`4`V$_fvop{=zc*f{&(fbKUQ;kkEF$uwE4% z9v@tq9l{DtAgVxnkUWOD&~G3K;H7PEU5xM3*eskIA={AYXB&q>FW9-VsAlS!&w|sH z5TOQ^A$jvFzZa!D}6#LL63-W;>Iz-0m^8LA0BldY#Bs%I9 zTe#ARZ*hgf*sx=hS|%MO&^PRxU;ZGr5B9q_>BXY2SavaPXz(CPbNMP`7UD};`tX}U z+VqWeU)b4h2X6`fC~oa(Lus1NVJL!#Fdc1e<50h|Gxlm3dLX#%$^dGdzV+ylhx zTfe>UeQ`#PpsE~VeTM#_fM;8h>W|!G8TTdlD1;=wmB`tMwuB1Gx`l0Z5Zb~mLt4M< z@J&Z5u}&6crLgR4+Q2X>HC_o8d4OpxF(WHKeVFnY1b(0O(8f`iaFQv;k@wP##KD0ADwnKKtIr7Am z{Cjw`{ISl&CV3dvAK-qu>Jfb+^7YlQ>g0?rY{GVGCVjk0sf)hav8t;UtG!aP*n+~N z`{SMMToM%}ZZXgV=jb%0^yO*x411h6pPzUtmPwe2U-&MuJn?-lxVh2!R~%UHUpk*j z+PuyFhV5O@wbm`cT$HFCc7C|@sdb%SZPvztG%T9nJVQ zo@c6AJUpg45t|o$8~Zxi#U}=rg5ssHg&`Y^11+dHg_n!Q<&#cRfDU%Bv_fW0(jo(U z#w5{h0Sr8y`OG&@eY|GlpJaXa5@2feS9wY4whB`;L)oKuQw$bKgB zp+MQN5hyOt@9|f*CC_6OfWE!?Whh{!8SZ!A3eSL6>|4En^0ZO85B0dq6C-iE740Kd z_&Z}O>Dj+C5yYH7T#g7|RvJhb(!(C9Ga3=peZmTLb$NQGkR36@p6O>$5Yc{tiQOj` zk;lgpV^m#RYku@2Kab4m$kIYpz%G6AR@nIzO}BKEbD^=@XVHeTmr zv*U{^{7yw>xYX0}JrBod^v&OHkTzpY`tZ|2Z(=q(iT%rUVU~qxjasd#VY%rS?}hA4 zBpSNAqWRtexD@76$@%K5%PH4Ee|8_x3i~+8F)%^utdS>f3}_<@<0H?0x(##F~2^Rqwf#;67KXIS2O)g8os zks3Su%MnXxJ2;nzt8nP?E&9(F2a2H7XUfZc!c{amtFJ(-j09|KCd}Z*+x%DNK|=j|8qTJ^z4Tl_bW$&c%Tky&P>d4 z-&kT{pOzY(UGd#%#f2?nJ;PN_4dr~BybZoYYB5G8`ld*cyCEnQwiH~uf(%?#{G%V2 z92|nHVUv3H_EWyj?O-wbTdpyrsc2!o%ZiGMCQmN0Qf&*tp00v6ABJ{PQ+GrIF4AOn{` zcaFvk3H1No<1hV7olvp_8VR+)feLvQzsv)BU|0ouqb^b=JeaMZ#tu{ZicF!qP1F#_jHYn)~MG0 z?)02p?TwNFk3^$4WrcyV0#5WeGC_zy##gqfE-4wb;kj=JHner95#Ijmr-mCtUrMM; zLlM(}p8z_=c$bqL;jiBwyJ1_MWObvoz78N$!4y{%z0jC8=4zp2e)J6^9(?0-fA-@a z7JnHb>t8G9coSXu+U7TOI~Nz46{Ga*#p%4`P_2$DhKE!&S#Lh)6Je<0CZtc5%goDne#tc8!LDzvwVAP z#yrU6D9y{vjGPN3Imii}D5&uyDjVJGA8bSy`SncwTf2IBaOC^Pzis$;(UuOdWIeBx zz>zQzNN^2fNW4mSv|%}Z^uaK`298INhqeO5%&X6h$)pqS5Vh)M+1V8Po7y>o^KtQV zj~|?(c-$j{T<7B2Rn-}Se#g!4ok?LFO(?#`q91NKBB{>~Y^J12yhsa`JIP&v6{F0+ zl^iWNPs@@=FkMNXo{#$CD$F@;qRHj!@Ao7PcX5Kl#Ooj_I#9n26NlIe345UTJpLrV z+EB{E)`Rw%Fr`FHq~&kxXHLg>=h zu&03Oph`wKp%%e;qo7a_C`#HhE{T>rJy5lhS}Si8iKz)BXVbVDqj|5%1z5X ziOw8z6faRWwa5dfp|`~!Kp?7f?=*uQ6k|LWC@Hm4{o(J$=(KOI%yKXJSqk{%B!n74 zW8^L8HG5*wN9%j6KKHW?<4r37Ev)Kl*~bm+XFm{60GrO$;?|SDVllwb@$%E&7NOO6 zn#Kr;ml==N>foDvC4-DMta~jaD%JrTa{4*?EHu1DrVu(QrTZ1~nOIK?(bthzQ8BlW z%4y0;IeT`f{z+GR+x7Z6{eu;tsxm4nYO>KodL_`;SJv2~yAM6A!n2h9H80I_Y^0IA z2ot6@sE$LMhGh+}BHK+GXR9*GuN-2(VTx{*#utAwGcD-xSUl;}00M+@+^D%%91*gK z-_w+^srjD9JTW=g?bEcsO_;X?u+T4!G#~Uan{Kzjc81g)7J3r;xRA}nDt*Q%Y5vQL z5ke84Rgj-w!6Dl>&Ux{BX$mh%`)a_N&sZPHgpTy;f1&5wH&HC129tP0k=fwX;pj@wlizTKLGsfIeA5Vs7XdcXV#@~lUq%lcq8Vjt)&d*K+mmyyA@#wV_}2Tu1Dw@Cucv8LcG}v7 zy3jH1x8Xg+_~y}{#CW04LP3V+M~>TP42-n}^#yfJMPr-!OC%X5YXxP8hfRr87VNVQ z-x(kqYTqhZE0Ou~oZQ3xiB~LUd4GgZEZ;NbNv;-5g|0#C69E9qlw6Dn0{sDfxU}1=keJvT|6(1SEA+?h7N(? z(Al(qlH#ySFf#8M7I_szNC~Tg$XPIXEJwf9 zfooqDM*UXf9;&;JFgh~Z^QaT>*6zsE8Sw|czKIRC`uhFNxv$KeZDUI6l%_$B3)=js z)3!6cG(U}}exR1FzwrUbsrVC(fTZZF+@O~-2%O_&JoA@cH1*BPmUenMEWVN>DgqJ50#FphgTBf z^x-kV7CJl$FNdbv{1%#T`vIB{kn{b?44S)&StT$Ulyx)+3VS@$@nnOEJ!S7o_7z3J z*0wz%5ZHEIOD$Q3{+jii!|Tbn-71ROI0BoShHBf@nFdAJ>Rm0m`VGazlE6uaLIXW# zT<~kK7cZ3UD@&~vF#j!Fn^G5*w3PnrVX=o+(vtUyu%3&|u?koHpHas;}Q%Yt2CGpNSiJQsZ%8f?R<$-d=$V6DO4T=iCfi0qy_db zpFjNPIf)(b6E15PQ7!LOBhIwP2_r5{208D4e&sB6@7lCZWI$mrg*Roi`nr$G1LEUe ztoxaa!P=uVg65XUlAl)3p}3z;c;A zy)LN%^xR>KTb%Z=tqL;>?%!D*&zt^j9&Pi((36wUchHDJcj=~QR)As}a=_drw31A8C#0 zBkLTr8Rk4Hii*M>x%>IoerGAaRi>%G%^seod?_K| zM9n!$J=+}Kk9e&+`Z8R@HAh-@hHUy0y4AQ`iy<|@H&_L#@U?-eWW~KDoN<97q7}hZ zSc8$xHH~IQ!G!Uq5g=pEED5vk6$}ujNECKr>csZP=q=DJ=pNYHaC2VH zm+^Pa>jEq?4~Q4avU9?kf-9egZfH^K5m7e0Td}>ttRQZKwoz#c)TvNee3fmffFK-!tHBi^}Z=Bm_7UmuQmE1&-VDi z4;G&G9EY)p1Ww^fVR)wbX)? zjnFq5y%PN>N+t<*c@K|g6#(yR8ng@Xla@n4&9&ZxvSUxAW}rW~^97QbdK+Sc#A8%5 zjd)^8xTjvZZ$I|*`30Ze01f$a=%0r?lJ|HG4!~q4YqGrL?9%6&9-}MFL(ALlsP~-p zlh;{!Jy6Oh8bcCV;UUdl9gN827dU>aP4&j})e~v*jkGR}MlPy78grke@Zbv9WQqsu6X601b$paR6)$4in>R8d0Y=TE1TOl$s=OU8algx)^hMK1t`^ zf5D@5g-YAvWEOKiQk?gGv&<4`SU-N3G|D7|q|uS>aMtH6lK$?Din zGR5n!;CO-7O7Lsx^LtZvA-{}3CL5c{2I9_}qRk3>HT{;hr3=BWtz0^T(e}o4ZBv_) zj0T3p=^8WSW-9onn=flSd?*V$b1TLu&+T%z!ksr^a&j_b!z&l?!}5~>F3XB2J5ZvD z!C>Ob+?tE7^ey_uwIw-E8*v4NM+f;<_{U*3RRCyJKi{O%F_Uz#l$#LkeooVz+7UAJ zCwsUwPVSIo{{{r%eMq4}bFNU*fY@Zt6SJVzcL%}T^xF*v{Yz*;3M7em>51j2c#}*u zdL$eUUFvvK8WG5(g_iP{csU&fhPWX8Q+0cS!LCb@xW>J=#OMVB*81M(#}C|JxWFLR zdLfZHjex%Z8HfpX872@?8h#F^Dep(}g(DLY!`sSZjbFNI$!Qj@<4h`F>q2Nfj_Wct zD=mHUlTwG0a~AA80j|s6E3TLaXZ`JmX#!GN%A0IkGi<$L0{C zZ6*b?xzk&y7;IZRL|*UPD1P{7kq4kCf0Ffn0ris&)^+#7=!Q$XW{jE3l?&$~@h?=1 zS7qPC7))Rp$m0yQEiGoACxJenA!Dt^LGz;r%l7={^r3fy@|-&DiJOI=T(1PGon&Sn z09X%h@9TW+ib_;u9B6Xs%JG;l+|7xSsgfyVXD)&4r>Hc!vDtUkjhvpBIhgc%v8&7t z8S@h+9c*^!=;8k1rlwbe*hM+Pm!W_;Jr@g;KppU`mdH})vBye!HW@8^rxz57yiRj^i~#9=I)-d zR-;gZ0&kQsUM;$-gN)kG%K3%LCqSU`jrD48*Zu{J0l>+6>* zmwB=ZsM$E8LB}i;YKN>y&QgqjSTTPYCPr5<=sLc&12V*qu?Ari&xouDn>4)V#{F5N z%*~L`H~IqXHEXXy#Ri}?p0>O>{Q!AHzgC2Do?6(FS1l`zXscjnvko5EE5_N?QAo6= zHo?2706?)V%Z*TFu0`kHlu|E*X>`w9eLj|znWy*4zH+sX?Q&dc>6w(*{2m?0hx(#B z_~db6s@s;Yf<|P-Ttf1W-wdkjn$L2vULwtYoY z!m_mQAG9WU%1=5xM>qP#EU|x4;UwBiEVryy6kp%|jpQ5uC1b6<{bF@a(DP$iW{7jT z7e?u}%WB4golTBw#j3W5yIc3jvxhe<)mBVVrS5D;lNr`f>8k#?KoX0dXJdR|85#qa zhiW{og@>mNB}EB!m|?FXyD&^K8c&z}v=h>YJ%7~S8pzAHzbxImjDKAPxG-_zFErJ; zI5nP;fJ>BR3&=DawXg#2PS*u84E5h?9}dqP9_IScR?_zZXNkqkc<|e; zC{rEDeE{aAi;OhK&r7Xr{Mp4RE|gK0aQECOA$6cPk_oRnm#2*Fqb;utntH~GehJ5wk?$;Y#k_=G7Uyz1cx#lNXkCN}z)CVRM-HLF z{fNz_-(C5RM9n{U$(Ncz>%ZoPW#;8G(7#J5=SDJs_Mo8ga;TtsFM#|W%S=u-?&UcP z<^1%=BFfgLq8UK*$|*@kY@>@C-XYF9BC`p1pmd?s4uPAHyBQNvnLPfxMMY>6)f}ID zed5JF&|+i@w1w~IMJw(vc3n9`rZ#yy9cJZw%BN~w zM>jc!Cu5bS>WeV#{JncOk9+H1Yiw{8y?m?p{>Y{0=2xFb91f|kyFgcO9FnL;5z^6_ zIv50KS4FWHxq*ifTgl_rD!*K~*1wJA);*{58ubgPzRt`XWc8t-@lpt^VM>e0s0H2V zWn)dtRz$d%hi&PQ^eFlQ$1k#ey%_$59}&j;ic(38?HKs1PCEoPihWPOX^IKY#_X%G ztsp!|)b;4E_k6;bRkj)}d$RfQ8Pp}0#wAX==X~$+Irz-MXva*Kt-Ot!CyypQrXj>b zsNNhf4B)*Vt|{NF8>bUTRaeCX+xG*|^}>6wBvxur(8XiLPk3rI(%{I5Wb|CiLFt zp}p*s3q84BPCzA5QD8_Q5p00uB^a^!rA`y|Jp&yPqpya(xCP5aTEwp_4GFPQvuJ@_ zG+KgoykD;zGHMD<;2J!>^O~d0+okvR{5eLC>=m{-AHC1p$NSzv{(U3oL~`SV!v2$m z5pd!BCj%8uo&MXK&FGgO4res2vf~E}y=Xxeg)b_hqhpyGT=s10=DdI>h>40ipE_$K zMMqKfPLdorr_54pL2bT>mMSrqxHjI&b_T zWU~AzsMkm70v>JE!!7Gj9&v2M?4S@%%pvHZw@)J|pz>nY1!+|aG`@>a(7F=yMp458 z5BL;Hi_rE+kgN}!YCH3XttCCI;^bUq)A`2qo-JR{ucJ&w%gNWY=vUJ1bo;}@;NQ*8 zm_C_^gTK`Cg({5;cyeairqe4b$GdO4@Rwp^T~t~TSBHhZlBWe!2cd2XQ*1e0%{!J(Ko`DTfg_hnRHz~5?Q-+mDqc}J)T(lmaoSZD$KEYf+si<4xZkRqH z-|#aQv-IMcaU&1*gh2~I0)=r1t{+>B4pm?q5O|NrKg}d<`&BdIk3b`16(%3^Y#yiC z6lV;(UCLE{CatsS>SX#&3-IVM-lxbSJ+R*?SK{LF|AOL}X!z;_tLILhK73UA6UN1a z?QDw7CcDDsYA_vWv-!}~17ks1fCOhgNV#`P%!8KpnIvO=p4*z1{<|2VZ-qZap^GWF*na(xv{spQ&ItEEB5^ohwd1V6OmkhdymV( zb5yiMjiL3nU(`oCs7n@3^j0_4pzK_+&V}cy1-VbBygoSeU`k4E?%T!cNX4~woshLS zvHG5uo>u&tPaQy|POIJOm#9$NYE}-<)l&l_`+w?`T%GAyHw}A{pRLI~8CY?qhGF1M zheUxAZD~Z-%=jqhg|JcUF`LffYQLBKo18RQ@dv@dq&Y^>)paG6x{zQWxBf8e+-Hyl ztk|C{mBpa2ky<;zEU`i%!zq}YC$Q4PLMjNQuARWdC({rzhC-y6YppVM4fV^c4pJ5< zTX0&^HjIL%RLm2x;0%k>l+)Iwx+y@hk)FQ>d+Hy8Gso&vWfy!28dWB z1$><&vI{~$l6*h8s8W_7L}XqHjDmu1y4_jD#!^;Q$F`XAn}+b%PZlJ{k|i|wIm!a~ zLdraREB_8?89*uZi7<-9kUv1yV^v7?jd!tF3vwc520R>O)nc3V6AG@(V8Ryx4+y_~MZ41CcCKG}>x+oY**H~4< zO_Kx%a&X<=gPz?7v9OzxEjE%&0`5Qm;C#`|r;NXNb5@+6pPg21@rmyB4?Z^W7a7&q z6zB%dLtkFu(B)g)@2oC!8fput>>2!UdhfpHt-R;Ajbb>Ls z{y?tJ)kxdifPAMuk}0m&7;cP9#dzfg6Niz@#(`@s0#*HKev=W}ORG&hu+k>(z|T%g zKd$mD8OI^sd45RQ>@Ta8c41Ijxri7d5s=7^QZ7XwuiUxy#s1|Q!dPBO;BrN#(^6j5 z^1N{swo|dHzf*;{c-Uy2hcX3OtX)qR(|YSsV&YbVBMf!OB1cMmdbDJ{b4UHAqRpIS zM;@plhHwZAWQotHH^MvDEO4nL&X`j{R;+sEa%TP)$`%5y;$lG@LR9vXfn?P>Em4;Y zF?2uCtiJ@GkROx|MTl0;zR#2HdzQk|V4{er##Z)sDicQXq&p0p&~H4o5ksuCD^K%E zx*%k%nG=jSf(G9O)~D2IsR%}L&dkv8FM$)Q;84>AH8hF?xp9R62O^YK8mFz0?X*K>kBIjM3+@3yHTYwx9DC1e4;f}_WOltmoU2jEy&c&&3_>AQ zK?oSK8R05q{i?~4H?FsEdCd)GW_ru=+qHxmx3#U}PiPIen7k5&d_lf?qc&K0dd1fC z##ZVw;X71XY^x?uep?l&$l_2~8hIE(bZeP7fEn~w_<(XS?7!t9CvTcvHoY=g>E1EX ziXG`^CoaU2ysyt(b%Kx(kkJyK`Rq)e+*F?-2Qw#QBu}O2yFhel=a2_d85c%o)EK2xcR6>Q-9?pCqpxD z1H@3Dp~YxPhP1hKF4QU2jA*2Wz7oiMs>dQ9lRwpEf=0kgg7=WBr(zV(=EB{&kvg*& zsdWO+7V!j$6zNZ7-zp=gT^&B^Kl?7M(qGF-w@52cc_Y73?Y!uaJLNQm;Or)tQKG`sjKUg`!ZNzsme*r-N)j= z5AMnL%1xyL*)UQ~tAAi}N*H8SjND*@vHuY=`+}suRMqKZj7@WK$u}eps5xEnl&Xpy zLE3Rw%I1b}EO{?bSeFtE31h^m^{-X_xm}Deg57YbGYp5|Z)*+iypJ|{jTji*xXb-? z^-M$s8+p<;p3xA^dy%Shids7%}m6R_?8% zOvc6C90Wek5B{(N!NjODTiDZezQ=nN_pgi|nqQ)+C23@a@r0B+<-)Jw#ve2I zKl~_(Mdw+2bddetyz`AJmhzJ3p7=VkG)F9vkr8`>8Mmx zp;DZ{B3~P{sIH!j6t3(Sg>SFrmH1y=K9gwrv!p-FgBX>^YWm=-%@6zL5!0a62EExk zN9%^z+;T6K*`<2lK@26^r45$&G_u=Kf90(zHo1%XN4}2yOnhDmHGwHLg{6jAuyk19 zO?NgIWP7#8XT-h9JNNLi!%~uU%$%{}eAb7`{$lo_p(Yi4A^1$tG=Cg~M^db{mvWT66xR>cc-eQQWM0`9R+_+J7a1PkRb(%fA`RPRk=P) ziJ>CH79Qf&Z0&|q8ER&ilqKcYkIkor&GKw9&kgzALh{8{hz654Vkv&NI%dh%W=agW zmz?CsE`&D>xzVY(8}wcIP+$rc1m&y!Twi^ULZ2m2|C4{inURlfnV1&OWyk5z=R@er zyc@||))2GX3-P%mEQH)u)n7dJa|pg=NXcd-f_J^weM>zTi?}k{&YLuCi1$iy8s(>y@nY0C zzBKV8Y{=R%7jo?;#3Bc7(T6WsFM^l#;j6ZY_prS9yDvz~RhVVtg~y9UD_h;c3YSJ) z#0uc514!9Li~yc8qN<;@bba2ABL1E>lR%Kh*>#kJ(C_WBBj^t!S-;Lk@vhT;j5@}4 zTXx7UZS=-6k7UWLcuuL|=)n#TA<+A0_=CxfS4^x zCS@JwGj|vqCI`Oe;*VX+X#$Fo3rBejB9~OmGm9Nuiw{fKxDKHTs`4e)rSsKL8u>l* z@Vf#1|NcrkdlIMUXE$nC#!V>u0n@(3qao>jp!kU!)jrWxF zUka-x^p8CrLmUq8pX;fboSqwW6CW`Ev$5`&`}n9Ix1_AX_LY@lUKDlMK@zwMtYB(Y{08E$^wLs;b}x!!1-=yqK{Nr zUogWD9alf%%KD3@vDRRHDP}+8Ti82zW%VkhJL3*D5j<-fCKa(PCxljt9bJTp-0#gY zkLZuj@x|8k5%tF%Qf+Jcg55q^aO`FF3z(hrKg|r=*U&j8`?LSz>O1q{ z4AiZ+ejkvSZA!23-oZEEN3T@N_ywH1r8G^b!oM?jkg3?>ejgQ(K6pX>`nn`Q-pY60cWT;RZSKO zuM}&CjWG|&V0{m9=hpQ+dLpYw$lDlIxL&zW=cvdS^A>o=-s0Up9R}r_PEs}JiB;a^ zx(CwC@dVeWt6<=Y;(M*s4&C@U?ocRWGU0D=#hT|C_nKTWd_|_C&$;0s!0m2B&XQNj3^Km_q|vK$g5bG zLv;HBj5smEU^pMXC-MSRD8RJwR{qIzKC5?sT&nr$oMZ37u2%CiP@NUV%tf8INbXFD z&`dNuN&J7Hh#AlQl*k(VdfMa>kzvoTIfg*3w zBm-qbHH&sb9MQLL3N<~~P@?VXN27BvtzVjhI`lJD)Qb#n>aY$057l+{{=Ju)qc4u# zJ#tHbz5Mw*bHR%flSS_z11Zq|Q5-3e6M93erntwjpR&qBSX;i2h-@4 zv=+_^=!oU9A5rTeuUBYNa7L<~*m6=@Q(ceKm(92+Unh%dFbk>>dH;Y_fmyF_Uxq3z zi2Igtf<5mndg9|US?wmMJNM;5JJ`~?=jgAmdtBEOnmLE#@l$}t7u$Zho~+fX**Yl8k7#`CDbquDZedM z!DDVJ3to9Jlk%SD%|}8wvzDlJmf@QDXoI!E)$zHyo?@SU)VY)f>x9NId#tTn$vok# z7pT2VY8cXY^b(GKCw^fGo`5=STqYxxgm&|U!Ku%vM?)*k2-f04#Y9I-Pn3n+&hkXx zr+#IEEE~^e^HqfsK5GmAnS|wn@AL~X3iDZ)NMTADPO!S3!H=JNZDj;m5n3{WG)q66 zgbJ>C_+9G=B2*!lyiO3nfzyO*mokBqE8~vOu^)xZTYpf+Q9JH?i>0v?btw!T&r&^` z=BRU`n^ktZR{xnVqPp2o>Vr9q?x(Jf)2|ykrh)W$t6AC)!PkQBx1%l>BvaZU>9qSg zPdzM35D>NQUwFg-R#jnTefv~17)a4F5D=a!Gz?N3i0d;v7)nQ7KT{R*#?)x4@0h&& zOwg!>h3DG(q{1O3Wr!=7JeKTfD;dF}>vyv`JW`ks4E|P-w-5fJAf8}jsJ&-imDHoU zwErlkmbiRlZ{I0Ol*P8y`Q?NiX+}7B$PvC5+ftV!uNN#in8?Z&64i{ZORcOWv%@xkI%Ue;zLMpnR9G5BVttMk5cv1Jql;pF3^gU0bx+3X; z#n;mS=l^`>76EhV%dNusi&RhI1K9|Jl=I!pZSqA3vz~H+DH{Nv+Rby&iPrA^1~4&1{1XZAvvc#ov25H3asrpX`iU~?C8SgiJh^r z-zf3N58i7sWvp8^bZ)7BQqC~IofkD%t zJ6^YdS_-A?xOE%P9jo=qIcL`Wlv*`V)3xg*)c3QvGs(R`jRo@cj{>cYn0KGosd)w7 zzWd`cxDDJ+MFGwpe=&Ohy#Dodz+D4@1r8Ee>5eQ2^=XQ!1{8>39NCglkLEJ~GzZhP zkFoV${KrT*l78P$QQiKo>IFgqmogiY`KI6-4PrhP8{b%Iaa?PDox#9!Z955pd6{eV zhHAC$43y)`$+uZM4|e~BY%Y}RQn_V`^&(nf&kkAv&8f4%+_{eUG1pxcq%f1ri`(fMtsNi-iK$`%a3g2rFb6zS3e=*0v4@5LH_xn+4p*)Hl@&12|}U zI>C{Oj&+r_N!j2wZ53f->TS%*R6vlT_3Pu{dIuo+aG#kYTkF zt^bEX%0KX*`3`of)3s51z3=3&nLKimPTt1!C?Lp28fm&(7ZF#kEA-}NxN9_C+mfoDm(3bU&)zrpb?Bm?nmDq8(7;pMKv z>?+Kz!u+{E+f|sI{n)?T_3!Rsb~pY1UjrDs9%k3W{BQCw^m$5id-hP)ud83u{~wS3 z?JCc%^89a7o?V6cpE#7+g=f3)Y!{yWUz}p@D$K6J>?+Kz!t9><|09>=zb{;DA{h5?C)ficdIY|o>qCc`f|5i`M=#;?n1J^r}FRC z_y0|me^+6473RO)D(^zFzo(GxLbAW9Ro+#YU4{AY?jN-a$^NFwzYEF!p31+gFuMx# z_vz}ln;Ns58nc@kvzr>Tn;Ns58nc@kvzr>Tn;P>!-e`FjlI=pWT}ZYI$#x;xE+pH9 zWV?`T7n1Elvj6vDxZSYw|CX?F4fo2rWY<*X_ny9r%_TR;Fljwgb!iL`X+t7gql+dJ z;oJ8Kaj@-=Oh!r{DYk^o4!01!{)*+5;grywOzZRx%cSzQt$A*|j1Bsz$Fa6OPvGQEBkkIKPg*jZ*dx454$ z#Y6G?;u^BO+Qe{*LS-Rt;`F@fJ9t0w*t~^*ULz+K5PE<9>H!p{|3 z$WCwmtRVn0-wS@Ke$Cc-U%K)`aGx&Kq8yBT{Q{C}+?IOiM?WzKz9s$4{kA`gC8t`Q zx|#aaUZjL?q`Qr>C!x=+tnXnHWBs;eaR^G{G<1viDRG@U*=|g#IrT}VH{UfcvEr-w zN6%Rd&%9nNVZnk@X@ZJ=byTUQ9joY8dqFIq=eAkUgV4-$>qX-rY=>-BqD4qWHRJd` zolibEC59UFXX}9zPgyIe9LuJ%qF~KF+~-~K_r&G81%ImAUTYKJIl(5GSQKnJK9^`7 zko^(z((Y?JmEPPJ(00G(K^#cyf8;b%VRiu(qqsavue4IPOvAHran9Xr+~KQeTisDC z3OyaCvplvMUxoAFG(#ufrEsF3 z1)SN_(Cx1apZBbq^|jBVOzV(sJhYB;k!tIO51b3nC`6;?Kjo*jN1?)I?SrWdnQGE! z!dUa8p&WJpqn9a_EB=! zw=Qg+JgZ-Tv-xGu4oZPo#^($k$UNamNrg9x9qZE|8m8q-G|}OtQJ8k?r?De3 zJH&@O{F`@H-)A;)V%>+M>m_By=vTaGQk(W!&Rp;Ia_W=!eV93O>4hh*F&M9*@3yZv z>D?VtZ{|^@T%GBKjuk3H@lR;VV8xGpT~ylR>z_pN<#j#Aytg>4UDpMD&dVsTHS;t1 zRR-M7JF0Zv-oPdK>7zS}kJ8=XB>Z8y+zk2rk#dE)o_(1+%-5_ILGCTzKfCp{&1J&I zxO_+4M+=*{hYfUKJ`Xd!Pc`j5$l0Ch-pKx;y~9qGIOv*Rga3cHd-r&#^Y?$)S(3C{ zQ53t|mMuvtgkfw}XGRG%LZyhwI5UXRVcC+@$|{E$QpRae$eE;)a-K129EKdn7{m;j zG5oIgw4cvszmNO#$NkU!xbN@%4|90W%=`7auGe*VUeD|G8lUU&Z&fgN%H9am*p_ba zr*6gA<3HohOv!H!XdC)M=Z(Jau`$1Y{fdRY!uwO|(yuyPi^~~3S10|++RaM*qlS|& zW_oN2WYRVm>xPo}Wp)pP=jO*MPAM5sJC%*sz!9Mu#)jA5J+XL6Z__j31oPStVMd>> z{L2=x0)peKb)ENFt@fstA6Xq{5!N{HrWNByz+JMMjIIWoARI^~X8kl@KRc=ghf!+? z`m$NAU#eeK^eHPQ^@Y#(YEdh84M<>!!`d$FKG9?_hEb`lZ)kE5Hyy1JI`+gP_VLU^ z(1YJqoj-;PXyi%K)tN?QY+X*ly%)K&6Gg3)__Beh4p-0(rpZZlSKXu2xpQY_taCjy zi%yCc-j))*x!omGIxb!=#m21F)5R}6Hc#7`W_i5Ww$G4xtZOja&S;gqSasqk0^3}T zvSBFD&*m(3XkY6wr}oB6-Ok4o!zbqUD_wO4c^C$Ds?Qs=s+e>!fbxNXa zmfe`Rj4XZ91oOe~yMNh8Ug15*-|tWL3d zuO~5Xwy6rf^!dv_CW%7Lj?00kLv$zPFj~>XWzkjPv^fZ_$qN3wq%{t(h$MlgkK>5+ z?{L80Ri&W_LKnOT2Ov+~@<&&dxsAzdvE@yo%pA%!`;1PL0&lU%Z5*`-p{tw^EFO|ZKJ2WLhL`Xcst8~@ zbMxAMYQf!UP!GkBBP26R{r_`lmR;2te}FwcIFCu51b^EWU>t%q4E!KLS2Izx=1g^# z``)1BdanrZ&wUo|gC#O*?MB{mhdk69{U|G|Kn;(qqj z)lrk-H}Z9Lai()0*QMa614{5I)+CjlTzjYD4uziUnT|Luj00L}61m(%C&1o1jS)Ee zMmOS_3Ak&P9EJ?0?J4I-R~oI62DdL!QgdM%yddd1O^LMhQr^raNxd|lqMmX_@?i0X zKJ?~N)4#0u=M5EhZyVl|?90V9cBd4RJ860BsGYLEM47pWMk`n9-!(O)Ya7Lej8*Xr zx96%SRFK&gXRX}%0hc_q;1n~x06g;+#p>9wjHlPm<_}5ZM_6zSUS_)gUg1@(XcqQ( zU{li2G-Lml;!9RrH4$S!_-|vsQApB7R&i)t=TxF_U;#PfkSdLFce?N;S|N4Bt(UP? z#(I9hBh}og@m#Dg%<*(UVEE~@vHOQKK5h1U_oMd>3dWfIq@$-S6-71ueTTW|>tI~4 zab3kV7%?L$^r?6PQB-@8!71)+ZzzWv&#+Eqw2o_J!W30(`|DFZy=EG5A_hM2-TZau zDe;tHD=W%D8ZM1HVCYj(N3+X1Tgze(d2mlSgeWw}j$5$GdZH=0tR-&HOu*ok9jpF) z%S&g^jlOryb{jj=QM}y7xv*~=5K#!QFXrzAX^Km6oVAqf1bV@Lw?!69smO^C%0gsUn^3>N=w znlWO2RODsM3<*h6^w1(oGnb);q8k3$xhm)yg@1%2hoQZZ@4TauC}Z9FiYAqo>puvV zXRSbqlt~tJC6Aj-RTWjvyw@fwlNeGif%r}R9>cE^Yj{ud>uBrJA|}g6w^8sbQb3~x zJL<=OqKwcaNsL!R;3IaQ+29>})iR@BP(9xl*1^oC3AX!S6>la~_`+evb*p509AzxV zE`EGpb$ONSI?#$!T|3(j|1ewfQVQT=`7g|Vu!5c2O~3T34$ zj?!HJ%<70(OpKPp;Fgmm=uDO!(e$7ui4j3qzlL!Y-1di=@6~h-9+$uc z&N)>#!NZGU3$nqkpp{edTVgz;UbP__c6FzLc0X^VD9nO9%X2Ikg|pM^vg^yU7@WsB zDDt{18Um9Yx*bKXf|s+;jh*`0a^~~ilK~ko+N+K(SJ5&PMV{Y_oaG4>>?{eolxB$!oN5g5a&XcS;JjrU{vW zjsnYOGZa-8llryrzG3sIf>z3ISrAHii_vI6LDkIrsKMa5ui#mEsUaheV3C*Dn8~Cu zHW~O-C}SKj>pc9yqaJImvc(%p9juwJo@zGT?6BLF|C()JxX$K@`Im!T7DA#9zV&*c zg#^T(HKCL2GPT@gb%Ah{pMe;yQ^|u!hysy_?zo|{wYro|E2*gmt#8+*-oCp#k?>SP zwq0L&7rd@}y(8*Q?;|th$jnE{L(_{TiVb-Xnjl)(w_tqEkFY=TX;({+axn4UCO9tk zu7AAnyf*<`Z~{4SJ0vcx=I$9VYg~VHPd^)pULAv+Ucs``e_GOShSG_|;@+w|B@^d^ ziG~XYU!5UfS^Z`0A5%rOAo^H=$_F!)DGa@jw0g5ge}E<_KccO=IT3=z>R-5~`f$Fz zIn@+%txM{IaGEEYXVvl|V&F~F9yC8IAG2ez_ra!ua_cmGid%rSCV>`L8q-*BIbR7g)s6OYBgt>_d*uUkEQ zQRNzF=%IB{nWDxL=krC(S;x)Ccu&2{UejM%GhFu=m7AZGPIi(0(6?@4!sut+;#tA2 zFlK;DvLdP%f4~jBv9w4%RK!C0gnlxV^ols!?;(gXZ_UOpf(CxL`nKG2qy*IT5B9;oB(tfi*}q8YS_G`W z#mj2DS9Ki+BrPj^4^6hS4-W5;Jk8Do1$&l&laB5#9UJHP$9aQQgL{7ob=>y1Y!0$iE~ zFWI0`LPcKq@)aW7F;mT4$EQO7+OyfKnHgEPi@y0~ghB$hdv zYCt8b-?;x6PL#)xOJJ17xYnVu|5(Z@e3So4T#gbZ~gd~5SgI#bYeohJBsI*MBU z-OCD?8Z%?l(^O>)$7<+w;Ghgx+M#1HI@l-Qqk1LAmwt*7tl*`{Ti1U59CNHT0h?0@y5ZkD1O$1cK=+bkKxV_<)FnWNva_s35c2Q=UCaKrSN z2*xrhLHV~c$s`8Y@1RVW2tVZIaSnsNHaV@3JGJb-)?xQ zY8nNG!PHB+sE#IBfF!MkTd1ukDcF5LngVk@_j^MUEXaz^*!S@!3BuPU_#vJW!P#Y;k@PEW5uK|M=Yz2G{m-?y9g57a5!R z0X(V@htwP4hNT1#KH_m@ROTIh{l?Y|m><1jE(N>c&#^wimzrSQP3 zKi~$%yMmpGKc00K-WD(08x+1R<=3%g5=1kNx7J7@6I0{qvs2=QDH_qP>|KKC0L`=| zPVEwB(rGHR3rR6}00x-6XU};#ZyNCMaFn^zbVEw*-uASlDg$qNqSS3runbo1*s`i5 zefEb>av^wL;1qHk_OAvxu!LpnG|AKCq{^V&b>M}k$WZ!u>tEmI%w>V=!o=w^2mtqEM7QX*+^ zejEO@5O|aF^h87;t-w%L=`RMWn+z)T(OZUTRoeH9l5AYJdfBrYjmz#`*G8TYrAqoH zY9Q{9myLq33p0LqA-g-XaXslTv+zBYFbyuUHjwHlW6-lrd$k{_JAiB3vJAeobuQs@ z*IEB&7~k|1W%NwNB-1X z=6G;wCq2vPp`jtO?Ub4BY~@JoQ|ImPUfk(xCW%$;L(_+<0w&2}Z^zz|?Ze^Ch;>VR zlpNwxQhgc!;Mj-7{18{fuYMS_TCE)I@n^kvvz;Fyq8BWj3kIMh|9AvLc5VP|I*&C( z0yH=wnl+KvSn-;1onp6l^?dzZ#9p7{3`A?189}~T%>3@qCOuhX!LD~Wt0$44eX=B;=IcSctZ>igU_+ zqbT}dbQn^xNofNNjj)Ml){0uW@73H1SkiDUD2*76Km!)3oeUaN(Rn~ROV+9sMle-2 zcsHRLBuy=EmtP}u+>jtn$0Jz6KEnk;wcT0U7BKfMCbGH8n1Yq78B>{J+0QMU4cLKC zCfi9Cc34%POe1{rY@aWUS`NBn09D`pYDa&+*t~ps?q4+ax<50-PP5}49!U|OgJd;U zPc}P)l+j86`I_38;b!D<}fYt0U8Exv40kwnkq05hi18?*#rbO(p zK*8DOIm~LUXl?!CN1X5TK><$O%a<6J!*JY90q&UE5?vPj0s?O6Nq1I!yqp3HqxbV~5MSth4=Zn(I=;pXrrMvjdPk2P#cH1O2a@<5 z)URw`_I~Z=wa;u>^PZ7@AOG=ir0oU45{hrgfneZO&Cl&xV)vqSSQSBP=DpDr^x}YW0WZlf8uTRw0^$Q6 zg6hfS2OaypGfv(c9W|RTPjJQrSo|fm&h=uQ8v+nVe%2eYjMIbn1Cd~aT8#e#w#~Yu z2Ol?WPBb|C_Uq;p{l2Klq1`ur0?;V^t&<+CSE>Scd+wk0o6=fDK9kYGbH-$l zV%Mq3=+RaLN<14fRe0u1eBdo=@Jv8$je)jUUUBA5srwgUa7a z-+j^zpFFtvW~X%;zP}hkd;U_s3Fx|qM4%?gzGbYiTyNa3-4$lJ1Erj%qMh!Y=DbyR zU^NDZ_=Q|lN}8p`k@*cN6ZnK=8{!7mvrUUxf^vGY8R)y-%8el9g)t%USoWt1nx1iQTKD4IN+uQtTw!dUOCt}AVb{9G5O zMu&`M=L}FA-Z-xPE^6i-ph8GqS~`>W2|hh?(1sGJU6&}>e$Ttp=s1$|82%M;twZT( z=SaLkRYNG;sYUh4dtN(5$$Ai8O5ScrQZ=%hTxEx3 zMdh0L^c)k&J+7-FfT;qcBv_op`hts1_E2CMeWU6!(PBE^W36S4bG8r3@k>U}$5WD9 zRGkUuS7JCfXS!U7;lqJNFt^QdbHg$=iX?j_h3i|p4n3VhzTgU`q%^-N($^*FkxQL?2Mdc-*?QFz6VL3GFy6cp)uH9?uG{zYKA_@;qDp%oD2GdT z;ibA4b!Tm7fBI?{+BkS>)U9)xS3neT@OhbrheOw9lSK{mM0n67fP*|m9 z#eaIBiZ=!?EsSE4ma2*}*S0u1j+~mc14B`utej;E(E%+J z!aXjOk|y1f^M5Qsf^1r;lh9 z?a@`U5GTs0D|ClQcM~H*MvZ8>pal$q$GtOGsl)(?)gYhM&Gry zw&O%WK=~{{iC=i4R*sS#WH8>nygbgPLjto6`53tM0*^{#s3Rsz$AF`64f7*>>ACWN zI<$r&>{NQ4|=NVsWZI)mbVKP45`+sLL(F zm2;=OdL7WCR7DO;fO}`0cTmfQBO*HD@VHh%cD(;+$@qXyZWP53yUe<*`|C1mP7|`8 z`;AqXm7JCttb)_L(ea9Mzd(@OW791nVVh|d1H*r6akaQ>pEr>cYuGn7QZjDL${AqC z&TZf?Tg>mVQ?GC<)-Av69ne?nFzS{7Nlj-riMK(>qDH)P)sdda|DLq%Z2i5O9NlLA zy_yI2A5p1xWRz9N!&pJCoWb~BmE*_7A!Vwrn!_cxw~YG@iWv{p2x0%OSxCOMSy*LwpS$+H1@XmN ziaT)}$_6DnR#YB^pdmmuZ_aU<6J306Pk|2h+!>kZ7RRsbdJ^_d7QGo}Fl0jaBVUy- z@EK__x2j9>#J+>F1tc|tsZ1!`8kBmR&n?{~?+=>#F6ogYv zKtCx~TBu@1%^nOIseT^srl3Xa{1RJhqo+9TlX&YS5-R-jEv+DGs?RHhROXikO|)0T z=HRnk%xnq?o3pX=Q%=8!se`8QV?)IaWivttvupdfyYRN92-P1co$0kKnxDdk%~!H| zv$>U1H8e1=jMnhEPwvdQzObqMlIl5TF3zjfbbi8Y`kL~C5k!vG(RlUU$qTbkT;J8K zB)hKhZEsaUN$(w_9MVavs&T;<^ zj%F+id5fn;q6nRX@mG!l82)qb_?h@keVYizMkr!me0>Z@4>ETAyxfq6Z*J|u?+SPt z$O|o^>6RZQ{Nl=Iyfg>;G^2pJf{N5FB4joH(!eRXj@{Z+wPf{7`VJyOhs%>*C z1VTBH=;o4{1#vmOR=1s1u%DAxRKeh-*E3qogcFnL(Hc9N5&huahtRmzo6f?FPwnN; zlLf+)B2mtZ=%-{+HT4B6kV7pEqO$uV898AOb8k`t4+Id&&$y=J?%nb9))$y;GB@p! zWmt+Ryv29ui%MEYpvZ&???;Tmo)74+nNui`6l)6NjSr7>!!}&g0aJ^y+xiCut96Jt7rB7wow)sCCMf$f1os0LvUJv>B?Vqn7 ztfM)a;{4E6;giuwR!8p)yk%EMyRIMD|HiGJW-M9ryruLgK`__tT^cE_m~_C;lWJyL zOmzA^Hp|&%Ba1#M7^)8|6=1xKnsNA;R8}qpvH_BccN=;IzNnwQ!>4vLti?kKc|Yzh z)*$n=j(6@5+K+5?ef2}8-06Q?wQK1wxjS3}R852W*)ABf^HBD7hk6Gq^*7VGMp|dV z(eBmL0qb`nW%`{chZ_s&LIM@9ofYhy4&;IeV=P^)^lZ1cpj z1Nun$d%(9{OARlW2)Dm1*AKrCS!;BjN!_`6^!WrT1hos zUYvEpG2dOb(&Dzf037@Uj=J1wk3E~zMT!ux8>3Zr_!oh1kf{D)XNkP#RUf7Xq5qmS zcS$%!egQaW0JS%m74C$a@A1cUG2wAN#kD01=*8?O6O6k?BM>2-1J=Zt2&gG+dz2OgH(F`8RzOaw8o$RnYx z&z=nG{FCvomy0-=p?_A+c-R&Gq2X9z5QfjuLQ%h0fzo9Jrna*PXlzE)0kGDpwEoUk zdipZ%hrOmQsaaNah!@wxS4e>3<3|__x1Mnh-|Kg}K-*KuEoD6s%`Y#pj&ZK${0=KE z^a@pSzROq0sT2jWUZ~`eMf#s#;Coy!lLdQAOWZ}MftzbHxv$q^Ca{%5e;zw3mdvz= z<7`*!lCycY=Gc__PNw776q+}`RUllCps*E;nk9ZgFx3g@eeu=Q_soSI0|<@GMs<339t!8zQfNH-s=)@rssU z-1snd3b*&rqYTUc(6CmZsuQHc;jZjSsva&|;TaNH{rg$xw`Yg|Y2boEvfjf;R1B9{ zGQmQ7^Wj->DRNoiJA1PF^B<30`%FcNq4WRpuQ8K~((SOW>bSy0>fHbJ-=FZ_ED#&3 zZY6JWMhpi6N%f^Duf_8>f%Eylb%Xk->dw)Fs79N<+pidoA^69pX+Hl~pA@rTKd(s$ z^`**_NNB@r6xFWd{p>gXYYCxWdSpoO+fcV}wd!YIIcYzME;{na>iyB)HM^sv)~z@F zuf*_oBjVnnt)EV{|3A=1#ara(y`;r-El6Q>Ny+eNblWuVbGZV_l!%9y@;9!upeNh1v?#W`eQ)IU8vhMf6!=MI3kfr!@rQ>3!7Mm$YjXkLE0*xZMptEKkbR z&sh5OWEILO8xf)8?b{lOdUT*Cp1o=xfTW*0lFq)hDOi{0^W{wYuhoBE$re9{){5_~ z;lJHm{8H|XD|<^hMx~PON-sa1Jsm> z9a-M}N4BIr2uB)2kz4f**FXltRstn_Nh@}vqyN{8g(%nJR|FiK<;`f-=+pEVDF4|hg z(KdWW1cCX7AV5wbPyZpOi}CTTw^iUWxEztQTl}|=!}IumJ{N!8L$Q*X%Kevnd&E<| z@lfsDe+(qDI>ck?d-=a_EbjeRMzsHiSbl$~=<5KWtEp?TXY&%k9b9^pKm@f%TDBdL z)#lr=#fdx59z}4JU%3vo0y_v8R*6!JL{OY*j;T7*jnHmAgiufF6HU0Iy9*U2y|@Tr z-UH98TKpG}{?|c%Sj0>``FI4lh7EhH^&cDEGZ2}TAXzD1*d7-_Z+?OBwsBVeCPTq3 zbH2Y*o*wi60(4rdZXlWUB30ky7eFh3D8z)dVBN(mtrc(&H~;22|{9`1&us%5At+R@qJsI3Xz)=4uZL$ z1*64xhs-*%l|mxy2BS9zoerj-IHWX=9?~1-pJ+Y{M2fXUq^?_By)2r71Q?L~u9BeX zI{9EKP=bGT!a+9Df3aqV@!0{7OYa-C$fz%0wTLrX@pE^Bt1{^Mz(@s(9{yxsP`6in zxyS7Drk+J{dq6B_+tYgF9p`{{%ii`wn$-clS$#XsWawQ)0Ao9=3OT@!TLU!(ZPCEF zX!f?02Lqh__=qb&gk5@@oI=o)vV8z|EVkGx_W3NhhdlW_Tu@@{GRH|Zf1X%mk_n|yzp2cZTq z&5#NJmEUzA{pA)Wfa(N<@~7qZ<*3P;#KkW+-lD(u-&jjU5aYC^gfO+(_L?3jGCQ} zw6pyAeRj&!Y*tmKuU%B#dPuG^<_IMYOl-F_S+oFPO5m*8CmMNvv6y8tw6MP6hlq2X z$0H-+HrzAQ{ZS*m)w*fv%rd&b?#%a4($O<0O0goBZ=7*|rQFOoNZb7Y{a6%D|NPj@ z)#r>wxK74i*Lv+txg`(cp7?u;Kk64Q=ogNwR$p>{s6O4Ze5Molh{rIpe@;r%iir6w zu~2mFli>ZxHe1!ZJe%xY^T~^z<9!y2+nE$)bSawCjK?8_v92V8F!yeR`kt>}T@LJ2 zb*TJfkFQzLE1yP&+P1Y-q#q6t-_wQ#2ov53r;J|op~#Zn_drp9CjAWsfFpcIK0q-Z zv{&x9>lYnq*IjT9XMHWZm;iS6WqX{@+=J9_RqBCmmNH2rF9ig@-%dH+4ZQ>OlA!0>$b1<`ypAV@Fx2V<32^Th1 z4FYry<93$30I>&2WohdV%3-uIvd2rUhV>#=-p*?R5QzPDbLW%hBiC|Wn~sF7x_0(d zw7?c3khG+f5u~R=HdU3cx*<sm49E@vwDE7*_2V8&Jf5i6xWC2 ztNDz4{pLeBrZlw)QHQzRwJ?i8v)JV}%F=lo<4rmGoA1)SEtHp%QqhJ4Y;RyS&WiG{G9GDZNhGR_{yD1CZXE--HJL^X_4X{7euZTA9FyYuz z#SvqIv2#>&lHrTTKf1mRi;FMs(eU3$FxCr@ayQVRl5tcQC2TKBpD{snZjj< z@Lj3>9=)$yP0oZ?*3)vNs~3{xtvRe5KoQmiU8nHfg4z{20L2N-s5#NnEJRpU_o`HK z)b^XI0HZPKeGf2W410X)f#Tw|ih!lyFY8hEAOsidV#eb#Y(KlS??E`nk)^i&;+3?_ z{`jNK&`W{u`OfNkO2c&*VO)ll%;O9`l)Y_^?<*Hq%P#EE9nV!>o%a}DTxVXAg@zi+ zg&g-Q683#E7i>2}DB5x5rzEh4sw;aDd#HczKEmMH4!|JNCUt(MqE}E+eGtfGxD-?> z)W2y}z8{i$l)a&L2IMe_z(A*03Z$j44QMkb&DAK8y(y>danObj4*`PSad|SkcCDCf zh}QSPq8lP9{IY~{-|G}%*hAM>v`+6L;K|EI;^M+I_EU+u&~^eijvsU-#1L+AS7S=! z%iNWJS%vBq)or+9-_Pbsj@vss>~$+C`3^xctnL@Qcvt#r(f(R@Y*p+RDN|)q-`M;v z0WmS2vVA`!C*wV3FKL3;gD4-UU9JM^!dd%~x9lPITz2;}pxzpI4d46%Wld)jr$*DQGHCmKjjR@oyz+<-+bdN(N`c0QDQ2E3q%c|8- zuT&rsLi09}f@k(o2N*0_lD%l$Ja$R#%LXkBajuB0L!C7O!of=quJ#=T3`c#Bc%&R7 z&oCQTXg)AKt_mnpw%|q2i)f>5C?`|Pk3huLxY9kVv6hs7V7NjK0I}3B--FT`-z)f2 zA4aV!u%Ev)z}}&2iTM+#oU{6J$?FNm6F+2^^EMYaBT=EX(gwOY-|d|H`LZI_(dLyW zpciTm_gg?bg&1FR$=fr>p92i_R;P7HA}OjmmC9~fkD`OENt*&-{JAxTh2rg zzcG6#rtg9maB(5wLe}i*kyd#45X-2*8GZA%8C$Pyq~Ef zQ8mZJ^&YfJ3#heI&E1F4-fR4PR7f?`uM{f*UMi%qzY}=ANw?C{EPf$o6eAVDA&#n0 zQ5?QdWc^Kpxtz}emmj;9{fPtKbJ9wk2uF0(D{F;D`>j|hDH$6($3XlqX@?CbqsEg; zp8;3F!PkEDx4|3=@30{doL-1Ce97C{ElA`D7gYe_n=sqKq7AiM=pp_b|2 zTVIj6MckUSGUetGjEq2eqF+EFu;}Nau*@*rA;}QQ=VG zIL-*}c78CoH<(dex~Drw-qi!4!*;&iiK222>Zh4DF^aX0VAo&oL>3%#l=M5L#3g$) z++EnGE+Ws(<%SGOU}82t8s2(JE4lF)Tr<1+m!2zczp>G=K^KdVR);SVJx#-m)DMwG za{X6!rBTBqWBEPY9fZ)G)twE#k(AN!gm*he$SwD!Q94Q@L+Q{hg-z<4jOBqT_>u+$ zE}h3?u}}J{yn=dRMnZ4oSt4vDLy;?L{sSsX8XmS|Q^tY3K)G}^R*xY`ikNBCqEHTu z-CU8Xl1@~wXYGlD)-Uqkl=0n5JQ~JZY(a|DyO~!*p{vqR-1BfPr^tWH<bS@KlcD(m;l?zx4Eq4?`AbHw#wneHbJ=?yx@;MjyogT+|NeZO}wUy z>jj01D5Q2&t3*P`SteYZJ&E(|EsUKR(~)ipgai5fJHyWe`vEPU+s)s^htkOxd(&9` z7DO8jTHmw=P69&E8saeU$ii8-dR2j3Z<1&ohX?Ni2LqCj+0jG&41Fq4)Ueb?y{ z`g4NR8;d|~m!#EouRf%_G~)&1>2XMERlluNuubf~YrEt9B0K5F~kXv%&Vj3qH#adM?fS z=Y0Vp6TU~Oo&6uh-2@pht-{x+-0%7RFPIN&JuR-_<2~xR`Jyhl?n*dp- zEt(>0>p&WO)_|ymq31LYY6EyZ-6VIUjPy{66vV6#h@{AFHzY8J)f4TIBu7zqUe9H~}X2gK~B}?@NCCH#`0PRLn&O6gs!8e605H)JkQGi0Jx8UHP%QYi}D1y#m)+ps&vycfUAO$57 zlejR%=^L8G4^>qO6~qG>ifxC*3@x6fctVwfrm=4Rd_8y-gs1U4#fcFOQRfHU+N&!u#Mxw4@o5w2dk^eozh=wX ze4E-Nb<;yqS*c>15YF*Ky5JVuR=He4N5@kFnZ@e50}#&R)y%mk_aY5P(TvJ1$@-_l z1hNpE%8*__{rwMx3@`;abHj6Q{`0;Z_hf0_1RHTh*AG({fFuA?Cx(L=Fy z9|MnQLW_UIaDqeTbg@A~cAOnA_!@-_K6pEdO8d)UAi*pX%uzEK<6)V!B|!ap%XbDq zQ(jUsQWflmre-ZJphTZ|NY|q`bR-i)M`z^Yx9vZmrQR;ktgKbs*@31WqTGz-7R?ZL zB|o_9KUjDqdz9`wNWSv!rwlE8SdX$yJr|46oU)2%SSlO5J<9topD?3W}<_r8%Zs zYw&D8`xA{~sJ;v46i|!7Um~&IKL`3vy0Jc%!4%5tVA;0ao(NmiFdPX!n`2dS0tCe> z`5FMCS&`KtiRo%Xno-#oMqw+CVzxMc;{map6L6nGjtI3KMH&qGPu5b@>4Op!abL_( z;9G8PLbgMNZ>dYi|*bOdRC*)~PSx|GhZ1UW=gy4f`BmY~y`&}SQH z#r{DBkdB})Z&ba3=EZ~2bv%~7TCDt3l%+)?T%d{Xvj+w7x_uaIHc)(8Ve^K}aBnVA zaYKGpORNzLgJc>WS`?xHV{Q#xzW2&*)AhrgTVO%Z?SpUKzT>hv3cQh!Pl2t+UY>!C zs$^?9EBgZTf&cNLfEH7X2*_3I93i2#?p}pIbgbE7nO*H;u^nzTKl)q;ivgQBFnP9R z$x1emi5ovV8}pk|THi#0EHq2;4=%(09}gqOiA9p0$duDSwNXxE^b4b4{>rlz_reX2 zjNIAV@(>!8JTC5)^wdIV$Sfb`mB=Ki+w0Oc_${{yF7K^We&uPAct^7mG}&QtN^Xi;@C?O}_R=IxIYw8?)pyv=s$_5e^Zq0HSH9bBXGaFd1{ zWjNw1H3W!F`5Q}N3y{L)=+iv41O%&?SHL6flRe5bHG2dWcsdMGJ+WI-ok`3lsJY&O zwjeZ6=gDc_FM6&-G8IS+XF7OAGm!zkSuQKJpS`3p5)3Gl60|{& zB1!7amcsY@L{}%CH5hn(se_yD;bCs?AAVq40okn&Z3I5V=-CuVKM^nM7jBs$F^&)| zZATEytM|^@-OtZx_1Pk*kyn6=zHnK}Mg3|H8NM5V5I>50tYOw;1oP=DfVte6(6G^U z6+*ndU1Rq1&Pc2a%_$`1QiHg%Sp&#~B^_+fLtx-CBIh1hH4dEDWI&$kIE(DJLlXj0 zG(m6qiu;)5#S7mPapX&@>Qn=IPR3 zo1wL?wE7o7E|_i_r{T%`{BBu{w}CkqTJG>nLtRBu-^5ywkw>!?Iq~E1NTw3$FAcr5 zR4pc*4V^hid?ebSP;)HS+*=Ydno>-&V!u`jk4RBC_a9r{!i?6k0xpQ`f`9lwjJc`_@f%w663Qb97rwAXUG=0-YbbJgiGR zgPryR??GHznEd2UPA#5G@VHJvR^BAT(L1%+5nq0ZiLY50$u9wdc{Yv<4bT%6mS)=S$C~izw#Q~4 zmqT!8R|$27Gr%S-di&i}PGb53hRe)I z=f4J~&d)#N2kSROO~%`k)U{#}1zejyOpWsJoQrh{{him+s{x8(A<|fJ)37{T+@3t zcH?3Z`DX5cj-s{zVHh7#o8j}ntm>7|U_s3gpJ5+9_!%MtrE_7lkMd+8k&)cX;5E)O z0)865LOb%B%6_J;KMA3N-ocnvF+W_^G0rvxHWe*1K5?`J%79OBTRp}Kp#R7tX zpi8RR0xgUp36DfaY;5ItPHqJwANu(7OSJRL9^<&sBr`=X-fjB`NvygKI(}TYuNNAP z__GKn_r+Qa>KTrEbFK9l{Vwo?J-*dkWH1hOesFnzu}WRZ4}H)`sbWr@%MY%`F#$9o z1KD=IuM+2lY*>WthO(%MBt0Ps;xhO2{w0MEAC}5P59~z>Exm1{IF+kTL1u!%Ku@~O zW)a{xvbUo>{Zhm&Wfr5BTJmvcv_uUZDGrz$u%(F&Dga*`Qu)4oV@?JO!5#LUvISW8 zLW(PJ|EkXey54gBEkHmENOwYXjoZA?s0QHmk3w|`U`2ivzZ_pbD7n(LPk9P9no4C( zR!*6DT+hnod=GXhjr~HS$7KuTt6_b!r2LY3*ucOEC$i9AZ8UT*zXS<=a8{b%7to(U z+x>KTEg8~2j0v|mzhtRSo4Z>@G@;q?SUJ2nv;J%j%V0zb1E49F=8fi(QN8Gghr%qV zeB~Jr`Nd*sBF`2`Rdc`o4HtJ@vw{X%Wz76?51N-v+;Ga|hoflqw7v-Y`Nt47fB8;i zIvFB=jm;9sN0-#Ub|V_JRn#$H7ZDZ{B2Dkt$C{q66V;E7h{~0*4$4Xpc)Xe1c>%Yh zEr58u#7zu^G}k`+2t1y_Uhmc6lO4ML`QP?48oAK)=Cmw6*xtmo+FYKr)a!6}=k=%V z%hiIjawHO2x?7TzFZBxkGSp|^7$e zhaM@3anIF2s9F|OZ|-NS6&%ZE4PF?GElElOL1&DF z`g>!{bjpj?=5#OfF_ioWYCKdj3~}7}q8PI?-OD^Lpl+H`yO;NQ^H1R+7Wtt-<<>~a zT=vA0R*8_@?;LQU#7x0iB$E$O4vo^uYg}ds$o<~rai9cJL`WwwXct?8OOG7aV#0WI zQpU99JCX9J2Tehl8oo43p2Bj-E84AZkC8Qe1#C(%70NK4s{sjMeSK?Kf#gR>S#-Y_ zLLZ>a0hxh_|K^Jr=<~m+dsEqTHbTD?gZWjLGXmFBD?a^Jw*gQ<>aZOYL7T?i*8XSj zFhrK2&JJe`Edrb7wm|x7a4LT0oSna|>`w@N$zl|O`Puk6dpm?eS0L(yPNA@RU{4~O zh5hYfyzL4wp*;}abY;Ewf2vomNg}3>Mi6|L0Lv0MhhNq)aRW4A>bp{8v$CHd1OD)k z^F6W$lZigGi!p6B133iJ?Y);Al_v|n!!@7g{;}om!xu=SyAw{_A;1tfNtGb`&N<=8 zz#BQnsfXyf=6(?@+PMw9Ss;F-8eDS4jgpBG;BNBqT-bMOQq0QS#v);}k)Cxfy2Nz5 zc)x{AF4zfp7rJLF8St>i8u3N6AZR)WrrOO1BSBZ+rDfijn19QK3IGR7t3HHlMK519 zEMRw^f3xD$_*=k+wcA-!lVlCkus@6Ecy7rxb|KDNre~|lw+y~1yj)OR(ONTBPMgiC zAYa<{j#cu_@2n>|&lR%`oKc4p5|Mov&^$F|lGk&Wz}Mj?3&N`d5!>5C+F4dq~gH;=+y1f{5Y02l<1VY{#t^+cVOT{+@Y5mf_H&i%(i*mOM46SeT&rmWmZNMQ(SU!ELFpR8bU4kE>ke1D664zOtN`meQeOw?@h zFXfQI`(LBjO3MK;Dm3D22`1SqS~VI$7JdK{qWX=0&=IQ%h$SK?!v`#P%%2BV;m(u) zZ?XzQmqMYe`i65A2%ZR=^WOx|zd4_)kX(i$d~o>_-`s2Ds<0_UoKULv%$4`6n06rq zJ%j;f<)Zj6q9pPXt>PQG?~cGde$RBkQ~Wz*L$Dp8|D;L&i|2SU^3@x6gqHI>`*DK@ zjh|4EPi*Yj^*CuR$B^#0quJt1y*O_#{M z_MbKNud&E&`)}805&u6t7PPecUYAD&-;naxXaD&B(}KwbGRMPQ&(1Vtd&ur=CB873 zwU}v}yHsO2m~_`}G0!s)uBcjsUJNyQQeQe6$dfLKq(i2Mps5a1iEJO)_SI!+Cb1U- zq)6*?zWnlshSsw)u*dGiJU~DPxy^>Gi6BM}0_5RpkgbD%=N>`}PLvkSK|&Br^{7iL zLH4R$+!O(uL`(%ud~8<`Y&t0qJD?DP{||TX9o1C&whP<4GWLRi9UCA@Cj=er2tlxc z1ja!?h=3Sc0s-5IihzoUQi3QFsS)X+sVFEdN+6NYRGLZXNhpE9xu4+7Z{Bm>^{wyE z^PV+pjmgeV*iXO9bzOI7jJK(?%i)PQ3iTfs+mT9g;ATE$a}k&_Yp}4FLXYAgJ=e6X zUjL!|7_QmE3@OBPMtkz<7Kq8+kqyUaKA}4;zlU#?-x{K?mj$3d3Ri|i$^xub4n|vP zCMx!O1^kvoG zN1=6{I9`{|mI1gwn3+}!&+I~xY>WGBtWPgz)GZipKrUhaEL9SO~*)O;>asf*tQ$l zk?9BuhXqFRhvj$zg0BLYPW0IbP>oyujcROCFI&6k%h|jDOO1o0DnRecE_)iwm5byc zdXKBkC_AU9E-QhQ*t0MXMai>g*A2!y1n|WkHmtAp$PxAclB!Ynj-R?;{G0BTeO^;Y z4T?k1#fB_K8j)hFt6b-92BIdvp?#t0Rpz>yPFjjpn9<-7AZS56}uF zaZKh#Lwt}C{gC^@q=@tUY_XVP5C}}&}s3eg2ii7COCE{CiU&U&6+ZFt|jJb@wI@u%n|(tz$q$uq%Nc^)j_I& znJdoE5c+rOw>-&q3m9#{6L6UR{j@E6^RmhmeN&=1GO`R}zvdPk218~3Kq1JIDt9vk zX;Ik(dSN!<-yXCiC`$SlGg~L=1GQWe^6LV|aHS)5k5eq9CTQIcElhb<`n>osfG1-5 z-=}3QHPwl;VEyhw_z$Bsa%I&ENGr{3pT0=vDav2ax|@3ay=mFKMWdz-`2bu@%YwiY zjo<^MH~@(qU<8d=Pv`TU%UWz5k!o)lo3)e!0|WS|gv@2}uGq{0&eBnNU0d#D6bvBH0Z) zIJpqA03q!n7sI%2^`D(iv=}WHfI}bTa3S?ppXJ52bOc(Oky0v($l=9iD?o0e(t zGF$mYEbMu$nNDsmvjD!DN9cZZ)|FQ)39P+flA8sRqTrKt9v%$`CtJZxV>Bb+*71pZ z&&Q`8mKr?}IbMAqj+FOSO#jl-YU45IloF=5q$B7J@oGn1my(~alL z$z1T@NuWl{TKbY(FR55iVY%|>FBO(J;p?LpaAfhYQI=?QG0Gi+@BHfU@}0EFo$BQ8 zAk}PmW=^+qU|B5eTZ4Z=LN5WpKXHY#}d4aJ8XwMDn+-TPv|PtLuC{ud%0VXdx+1p+;F5H_?-P>hgeyxy9CZ z4}GC7i6P-Npy)#JHD~m9mR3M-rG5x3Csc!+zAlN}{|O;3P9!?O^Wd9qJ%%g_6t)v`aZg^TS(Hpt6@Hm`n0Xi;pAoY8s$zXu< ze{=`9u)1PbQtB*5y;W2p#$y+jK(DUxsWbKbvQNmH%w#q}T3+8tN*6xp1On?w-Ng1? z(h*>Pc6t1Gi?o0c_1A#uuq+)})hl)A_-;eaFH28bcEk4(GKc zB9_6lfyl;uHxLqCJBaPqfVAcRkG&G2<-jqi?g1XQEgAm~T6(qI#UQu%V2Y+)7D~*! zvJ|gzkq|mkQf49CyD7W6ZaXKZ6t*6&8?RS6(_oggG2O1Laq!J_IbfWCf=H94V~nN~ z<4}!VAru{m8BChA?p(QwCmO;crHSGfBon(VFGS)ujqEV@y~qtrQoQ?O#N#T>J3DX$ z#8|yj6RSHlM=p%|MDFMwa-MiS@2xWeHt;@AJrys$`iIm~*^Mx+;U>xG*uUT}z#eiQ z0v_c?#06@5zNYkSK-zorofQazxDd0&y6R#}%UFjto&fr@x|?L6Ap&X6s=|!2NCn`x zCfF)Gs%o_FkxVkDs}4K~y1X2u_dM&0r>-%24&@iR8ZW3yrg(~z9VL2eLF*jErZiVl zu2{uBv+jV>2t0dCW;(pR*SR}r5gCBCM17oOWWPUYJ_y(|S?t7*$>TXsv!ercZ#$J?FU2+tziuVj~vP~bo8v+nnoKf>-! z?ONaJIBE{Q+oLZB%eH`=Bjs)koT~w6ZrB)RNMXa76y27|>D2ET`~{Ia8eAD7yX2|i z^@|;o6Gj^X3;9K}Z?Y>yILpO*^bh*ZgT{byn2$J8##InnF(_@+xPr9Nb_X6h4C=7( z{->yWmRSo}7{&@GHX1;xX%!K$LR0zEUPkDS9U)J4%dsm@d9z{!-(J}(fwHs>`+Dj8 zMLn^Y!UY%UWe`p-dn})>1SwR@Stu1gv5FdqJvfDb>ADeNv$T{R{|f<_7eAF zSI96vUSZtXFFnI@Duu7W`s(;&#Z#M(g>UsB$qEuz^QmlQZp74H&~%nO2X#zRAFrK7%{Xd{9fqGG)mIR$^W8Inxhc(s2zAoY4j?W1!?*eLg0Ijy#$76p$c>@!c6 zhV%yyg||ehx0d8SVWn!)Cb^IrX9dqJKM30w;3|60OcBGMbFrB*D<`{>^Em}&)9=T6 zf$-qglLV9yuL?F@1O$n}@*VVtE&J0zi?!2=W(xX@Q#>@?IFLvCaosd##o82lyDx0i z6-202kjXc))&Ba;NqS=8Nr3}O6TbyUoc3Qdq{TMCkwZU{hxAYrXzx^~BRq&}<4HoE zo+-US8g>I&s{`WwAS9Rj?*YoG)Nx4=;Vc0Xlw2XhC-|%mtxHFegdVOQE<7W{@Umwz z#;c0{7NAfpJYNa!mz&&~{hU7`2}F#UB-*s?qNpedH37;k2pI54rX#C$58~Z42>gMD zH0@bR+?3grnMOt+gKv21z>_o_?`-7kKcb7!(*QZ$de*1F<=p~?uymvGodhd4oNt-`W zH=8IuwYixd2N7J`)OP#QnNJrP<{p!tznJR(pOoQ2r_(cSAyzmwS^L34s!hkT?=Wjl zMoIJ{XPF6t_r0*;fXbtCj2>vFp{VwYJWF_3ah!JDLh6bcXAQDs<5>kHHl4p{o=KO) zkyh|IapTK^skMkr$RpRZqSoNR%^9BYv>9@9`NfjtVo-n416BG}qxW8!zE3JRI5}V% zD*6cldS7?HQU)EUJNA@QrJ?K}`mh@Mu9Vj0H7j1U0*wtMhF+Ko;9UEDAiX6LD@IbAL?fo-iJc0q_K6AROPI*-zSw4hPq-A}Uq`T^fkG9wS z18bFC=G$6B3WM=|KeB8W#D z#ne;5SuEO2T+5}Bg)cOtK=O1F4VhYajBJuL0i7~XXGpPn>Spf>H?NoiI;B%37A9EShmv~4LA03K!QsC0LfH4=4*F zX!rQ3piT|vh}3PKLqfXnn+yw|A#aoNHl$A(Bw#g9$Eo>9yGsyp!h=^OM)~1O2k1w{pz0$^ z&1trZL{lScN&!TWKytQj`}`PqN$)KZnW%i&894Lo}gw1|+?gLrnxA zJgeYw$lJ9!-|p!Op69h2u%^NAnzsq2nujU1SyvgTMQhJ(KC~y{luY?yQcpoNHiZzY zL0sh1RNLOh8)3#auL4k^p|)D_*yV3L-TGw0kxP#HB)vY-S@ClL4;5daLYP33ns>bL zXDtsZmjDs5qXXa<^8l%hrOT1|%y8d@vYKCa670(mJ-bUnTza85Rp6A8XL-m+@+?S| zEWd6oTw#nrF|cL8c)H8BE-hapb4=?wQRTzwXtPl2dO19~7d-O;a|bw3-}`$<9+Wi9 zmoZXz&^s+79@}c$H3>Hd=GZPmMhy#~(zs&rSU`V}?E*~UANU{Oge}4tIJ$~VJ`I6Y zDQW}yiW?qR3@SDGD4R;2Y`{Tf6T$Tr-Lg-sf(MI6l_8zwPjFo`KRy;htp+*%mjX5-zMrmi>4!f=7_G3@PlM4TZxy3)lxEOY@@kx5=gZ{G! ziSPo8!N4PCuoT7Q~IpbUlHg@TTD1Qx55t1C#B6a$@YDPo$`(F?4~ z!e~lB?C!aX}q1#i4hy4b(Iv-~H16V)wT>#cZuH zoSQvw4t*m4>{p#!|JAcGek}nuM6MuqUIHQcGUIiR=A5U8adyoohjvR_o|9HhzlB6S zTuK2B5jg!I_?AisUm^gwyL7Wmn+S5SpE+yEp}DK+$6=fhKwJi~*c7E@XCu-FjD(>w z>?}HlM8oLO??-Kd#cQ%cba&z?AnE0sh91*ivNU%1{W2>^gaYuYFudcVS>vaVKY_Q8H@~-Qp)NR1)gGB+#PY>9h7?K3C249v% z`vX3UfhzsMzUwoPS>&E4hYN~*S=#}HDpacXb^Q)CmoAn;pQ+#=1kth-GMl(FpFZDB zCL%AR<)MKrDV8pHF;k3X-{FfFFahQhF4t z0D2{|p{s&SnNUQAyb;vFz=S@zk$cY->8}U=F3b>uxP0a}3FI5xrh%3(Z@Wh^Tv+ke z^YTHY9E=phYsB$utN@bno6&UI5D@TP|D_HHHf?)v*TFEYAIb8w1v7#!s9S5Y0?VPW z*R~;ed)Gi)ka;9=>7c{D`}|Mg<>#iAn%zsiOwX!ZwJ!$BN;M}b)uMq^X$=c80a3uw zRj8U?LukGn0u5%P9AvB8`qF%nBEsPCZ^p>SXxSk9s5JAXTKDW|9& zP(`PQc7drojubSUOpM=n6!ThkaBAUe*RIOzG=1YaS6xtr8&({unGLRtoWWtY>@+qQ zlzYC+rU9>_p?CV|x6D0i zYav3R{g*J(^d_5jzfRNvn&%)lRuQH>TUN-ZeV_6l^Shi(l-E>h#gLn<0^Ik}TCpfP z1+vz$AeT{7a6uKEPNd)gr83Ln=U#C&q5YQW1u6jEsel*6;|WmSz|k938M11-h`K6y z*K?FOc%(*Au_?v_^9Vl)2jPM*10os_uae1e9F)bU0OU5#Usll>k8ZwTb(GD3Y01^Ee|Wg z*MiPA0H>UnjNzYzX*o0(?3_+M3RKsrcm73no%;yL&2v6((34PZ%eZ19$g;U5lDQigD0~Y%!6nt&FzcotC=eWre9B|rq=sOkrmKPhMW8lWTnH!z zdP6?DNKyyEB)tDfJIhUY(N_-Cso4$Rc*d%E@qSYtf)~$v)-Z0-5LUy4l6CUAA?S5ERXp`1 z>eBfqTz#8dCd^m)$PEjT-BTHL0MmNBv)JxabROE2H?$=hHPI3h3k z)3m@<^t--j0*HsN^%TB2ksi;C3ml^;?rfUK0mN2jXD#Hal-@FfBL8S2rB1C0^5>gO zO>9CeWGlBR@>5jK_}H)j(`f^k?6j;|$A$v3!)VE|N7-v3OJ}w7^Sit2ezJ=o>kA;R zFupUSTZc0>&yWWluhTTM$yRx2z7|2Ul!3xkHONVITKb;L)#O;Svb^dbjFCE5)fLrG zNzWWMD@=gc9bP_mq|$LqH)v^yGGm~)4xXSl^Lf_4fSr;ckW1CB@xw>5qfYt<}si!Uy6~6K#PWY_T>xtxyMmG8NWtPcgK>p{A zf|He1yUlbbnPh-Urk|A+BSrMb-m&*T|2t%-(N4sI1S~TuL(xBYxvW45<}pa~*^*R@ znNUqo8cz4KfBjE#^d#7pc zW2FK;iY_8J{fW}kUteq<^e$fhc3amAdb!8f4ed-l@VrCSZ?P`uFI`Jj+D ziR-+)END@5)L*pZ*;P&`mkLuWH*)syw!*&3>Z)|LvTR> zD(Geia-M+mJi3Ukg_KOCAR#0Zl1GQ5;VRG?uPZv3+8_m9!%S1PeAV)IfdZMjdjG9h%$#$c@jM?+EiCyoq7j74HpIr$cee99*5s=$5Bd z0ys(~?_WIS@lP{F1Q&?)Q2spaHdgu=86^$MMOlCBI>?3-8CAG5F`e~Uxw%aCktg}a z3jc@^e(rcQLVsEu6lsQ3M6Ydx>Wbf-T;4dfdev7gtCEuy?Gr^RO$X)0mvh%DNPNB7 zldDNn@9)*L8v=ME%QwBQoZB;5dLyv9>I{l}*v>5On!Pwa3Sp%^TuZN^6xa7BV0EB^ zX@ghbt$twIWy7&iL}H8KR6hQd-x_7I)rLSF^=9)ZYlv?YyQ6HV83CAN=MjmH^{^?m zj<^?_&fW|`Ni2x{C@ZTIdhLG-U%zZ&fIs@yN}yTcH|i9X41M4Y@R7v1i)^wTH2h;A z5_X%ryJcQOEDJuGSwBD0rejmjI5JsMbr(;l&oX`;0x@dgGcF318EVLtfrP<$wP?QM z<29@3v;K|@yx9&B<_t4ZoXyFPkfI%6dz|pk9$D)f0(%~*Mp-E^Ga*O1>{xRLu@V6v zXX-Qk7lw|jULieC(5;CMLFHduVSUC7HcW66>J8N-Ny<{+(eh{ZhVJ@jiEyB;*h_^p z^ATULY!gi8JEw|^2a+dF{$X{=OrG#It_mq7s33hK(1jZE1P%kU@!z(FUjN?Df>qxH z7ZFdq#slhHdx8hg%j6}msqBak5!DoRm;lsg6pOHP9t2 z5ayI5#9JvWuhM!^A{^Qqi%6?>f9HkGC)z9o$x^RmZGgeL*uJt-V@nJWeDc|cR74v6RHgRC z2-GJx!0k)mT+j%`XV$lL5pbe_cp-|OHAIQ@0a}R`yd%)_3xkm z1Rtl<0W_I7$h6N*oaB2=5H`Xn0K!i)o}~k5>q_< zL5+c+fiInQlCV~`(s6``Ym#>>P`@ySaF{*Hh9gImU*!&req04-+P-4rZm4?R>kmFO z@FNlAupXr$JyLn@AA4OWG2fl)4hx5#D@kAa0x*n$QdnFnhzO-=s;E{G;25%Yve7VX z0eWRVk}|)B4Xo_`N1xM5z9!%aR-!;DM#qvj1OjkeC?&7yQp4f`54kxDBrkK&$&It5 z?Z8OEe)j^t#@PF)$c|R1FBh8$viNRXS)rQlAltDYnK1wvz!MOtp@Jy9ALG3P8Jx&Y6K-jDV1i6%yIjC6AT@J7U~F6NS(ma`8tHMr$Mn%vIrGqg{tH{kygH_ z&_V3~kMKDK)K+&E4n4AgxcR6;PBWR{EwgfL{YTEHjzPqr8!Jx4nKXbXQA?KBySd69 zsi>MWo=``b+3oApWF2bQMxuaHX}(>EuER6$!xQ!rB#zL8Snu_6=J}Au%Lm<!B48+J?Ktg3`F&p~ zKDqne2l<~ul_%K+KdbB}7Mfqb;TSNF-1w5`zMa@Q2Uii)Y=)v@ZUiU?7ul?llW?H8 zeh>smo)S{$b%EVYPHVFWI?w_#yR`1x%G95=Gq&bHkSx?wXm$497o3XE$(2VMzd708 zDwuzzsD~lg@*9BwWduz)T+jvqyztC?c3SR_J844LdG~Q3b~SdQ_0@fX$_J-pcc0%TzWG#Y>+xs;K*%; zCFzx_;MeyS;HzsdALejj)`1}__9Ekxi#I~(bE}>m3ItIn{nopC7?ZJt ztc+^CWT27my=W2`-UvWkSXJ3L`ye-F_{Psh^j zhg0g)moHph@}|D>bjOZ=3SmMS0(;xXos?-QNeD<0xc;+;AY-vc(|EBS($vhXY#tW$ zg0Nv7jsg=CvG05Lu@*i{dk45z1Z0WAsw|-(ICf?_s7Wg4fMZzrh4XtjgqBJni9sNNj<+yKLCDzg?HB0G_4gF%D1btA34L4QCxc7dc}o! zRIqR(C)afL>iPcN*Pl%z-U>lBC=?BE(~@OwKNY;BoBnQdpy@(&;tDY$6(1A-ygtWn zAFJCv=?@fpx{VSp{d51;itMvWkxB6E1sLrICK84^W}8n0Ix!h38AHrMIWULJK9B#C z@P=jA*0&QXhs)B3vP!%tHtj+2+3CX|U%3IO7&pgSf2fQLYMq|<7FHREy`j3#$`{l9 zW|-v28u(-faZ-)}%5F`Ch9${lY?=De z_ukcHNI5g9HjhA9wa9$W>ZSj+bRBS6gQvOOUD-+M6CEt}{xSj4_hr$|AUetPSAcS= z{(=K1-L8op+y~$DK!TFbE0?*Xly^`h5-Ex8+=w+x-qq${nrYBu*qx8Pw1|Ip-1i#kz)5li)dE zc}qR+G=9DGN|8Qq4rp+2k6C|cOaWjaTEMOdUU?rXx32}Mm#~uj6<-nTR?Z33&NSho z>7uY96}~`p?%ky4>IS6;$5l4k6%0WoQJUwsTkIPR-`7k&oCkFKO<5+dMplWv@a13? z?LR7=kEJECr2raU2PiWYo!oBs(78A|JcZF)UboMH=$V_REzo`k%tf1=xUr3+!EYUA zQXSsxAo_t>5@j-M#bIE7G*R`cIjU4$%`)h!>$ zNa#^EkiN+Gkc9Pyg=Z&h6IUb2NwN5XOI#9c)o*G}mpg^jPC{~DjmM70`f#l0pQ0nu zW$J-y(3G_e^@ieHxQu~M@kehgdDE=FWWde%7S)5(>(nz4&OF5nJ`hm-_BL3N{3~yJ5Ezt z{F{@w;$%AYEt^+i<=NblL~G_|aV$nXY%I17h!XU#)iG)lT)j=4ZKS;NYDR7JGLoLV zi~0HcCQ;)bs*Dr|?#1LLa-1XNH{7wf`lqW&gH|@dk zMZ<(VJ2_%k`pFJ%tHL4oBHl}bD3dbPYnSd|@OkWZp4D}jAcR)im*f4R0R;^9@ljs; zA`h}j)sbAG$Hu#Z|rv+e&N&3Z@+r`fN&aut80Ejgy3OAv2oTcx{?+U#M@F*x4D zVbr$XTiRT1*5c6Z|J?IzlfXMBO>U~VW$;ND_S7AWL64pRbfi*3Xrb1&0gu}S79@sh zk;lZ^rR{bN!^=qIMo)(7sc;Mh1o^w9p2nP=`>1I_uC4Dna=!xNr%3p+FoCTO*YZsb zzfG0PeMnQ1{yao)9?Z^B@N_Y6-}@UQIlW$13%UmmT+1sS za)$44qdM3Zl$!nDs-|MMf=3&09HK@z%{US}sOWGFgZFJ@Mg3Jb?meX1`Y<>-7D9v*y-$C# zP5Vafroaa@j+@z;3I93F+q(|fU(oY+RcTfygXUEau~jEFN_WB5>RGokh}5lR)-7s3 zjiOJ)-)4Qd1V5~Kta$3PKor=lyn#?5&*=-Rj{341v(n|MUNenns?6e z#rh2~z6RzLQ_VT0!tWPJT;X@pk`&5UET)tAkm55c*e3SV*+=z%x0HIxox@v?Q?^zz zy-yRUoeqgge|X*|px?8a(0|WvNgIzFv?gFA)BlkUh(S!}X_6Jm zYSy$>Uvn@qTTMCgM$a%Y12@CBKsR2$k#*!A&3J>-4-e_F+;tXA6Bw6(8mH|znqRId z%$<pK< z6Xh7+aV9e`M;N^hXUIPu%6Le#NRw0k$U0drNqX%!cg}_VQ#-Ppwaxc9yzD+_ z+Rl4uP&tFd*wnrr$1y3<>m>$0hPefTl7=?0xD6RP=6<(lkjPSczH;M*v{muDH53d3 zg(tKLrrjaB3VEDP{wrqqkBe7ycC-clHMWd>nnd0+eRFEhXipi-Q2BLlS@U068YBk4 zelxSEV+-_`N_ismZute<5ie5o43Y%NYP`VUSKjvel)=n{^{N0a(l%VcPa~x}mQiqn zC*8LRZkQ-wcKSz@9Sx%in2UNIINB1I!j7Xaj-T`HgjxKw57C{M$TaGvxsMzmX-X^5 z(%7;1PG^d7@RMc})dRO)G7g{GzKn^FO3aM7e8~0hE~a{yX^i%T3m!@o?Uk?!Brdi< znntC{wG?V47XCAZVTz+EoNP z10p1Peakb^6LtxL9PuOTi0@flG}C%TH3JZF#{S(bk+@1JK_b+ww9$aIMFEF`-kwWmO&zw45 z0Vn(|w{Ke-Neml9+X)FrZ3bVB!ZS{OXh;f&jfDG9!(aygp<(zV_%h>_ZW}M6u*-fi zoklJKs{?6u4VY6+M>6hi8}N8`*r>M$o7 zP3h6+UwGS5^R+e~3)M5kxPN?Fq6_ECMua@l^^ayFCL)!R}pN8+i*zoYVqj2x!zP&wjhLM z^whLI=qOkgsU`-8Hk&DTpF<6ezOo({037sCAA>q!IJE1sR*m0nIN&8Kn=8KmEa3F` zE|OUn;w=nIBEdVl=-Gm?iH2*c*VJaL*1=8>d_SDu_?p5_`h*|C+H8h6 z<9JG!ol?*lSk#Ph4A%}+XX=>aT}+#`DE=8*Bs-d+e&3Gs%o}R2&GrESqKYCl>`)KQ zvyKYHw5>nnn7~i4Ox){UK6zI4FluuXublMu+d!1ld^Bl12nx2C?0#nH#%vn6S1-(&b`L(fj>$&T@r*KBGd}Fi8D&DRhqXITaovp<9l^S>Q*heXi)a z9!Ha%PbfwUjokc1xpA@90cw$SutBYzch;>?^M{@-Of?W37==kvUvdl%3rfM@^P79| zuP=43{iB5lU3hs4iq+cAtDRf4{|+X`C+zp_My!6~>ezDDT*Xg~3kcmpZ>Ln9VC2ZPP>wUfN_%tEC-k+PV+E@& ztgA%WLv}_}pCpI#gR8KsywX=_9qIFO_?c%YO@Z=AN4qpNc66J_cJPrDtT*1AZVWq{ ztlgf$I^QM8(k57F+b2qI=rzN8#{68AmfbY_<1YoXd{($%N;fqdtat+Gx0;rh9W5Cv{q(E zTSZC2?ixLnnMt*tM;_3M0UKuHe&{PB@y0<+opHW${}&J9OUXJ~SzR zpLL`@J|*i(vqH+I?{*X6GoU))%z5P_CzhlIBnXFesc2+sot3EG%D>Cbyg@kgsv`!I z)anDZ4RM3#Mt6}~6z#fUjOKt_7A`ms1I6+WV-pHg?@z3pvV@L|tSl{06CZ1(G{Ogw zW*)Qu$bd(k`DM~4mM8HM*Y2z1DajdVQytzHmnC;{^tk6*U$yPfWm!*p&iRj2*kUD$ zXeJUYe4Da*@dF-(MdeXX>DMVCxcjsn77x_!Z^F@A;$K;ZP4AQVX<~=4O*oG5;lR~(N2rQQ4{u`d zhP|Lf@SL*l@B@$p;4j)}^mM${aJ==oji2mFQQu!`*5xeE{v>|>DV$F21nvQCN2|zm zF)Db&e>~0e0S14!7Jq<0jU>HY$~fbnqL#7g6YE2z#Gz4rrt|i#HNpDa*$ErE(mxnP zhTpmP%jn=SYKIe-xnDeY19l{Je3YkdDw}u4`)LAfO3z^9rz-zZA4s>R#Sd*A?Q{Cb z8vH);;8kadH~pkJ>`cQN@djYUO^$~&+N`y^gw}8QR`|V}X#vl}IbSCJ&Nw*M23esC zoWL+RF*PC2vJ&TLivk2#x4?(AGP}^& z%?Fvfa2TWoG;29h8nXj>)2i0D^T=J2Fq6~zN6XI|f})sGYH2^Dr-tuL*iM*+NSb1@ zbmK7wioSlo<#U;EE(a#N`@#8CTrlb}w^rOMe*0Q=or@lSg06(Nuv4u+=iP_DCP*(9 zr@FaddJ2Tqbvi{rK}#z={wMPURrRVw9(3B;j|XhIxE7u1r(0hJ8ApZCDcyu_!^NJ5 z!CF_WjnNehgXlGdQK_DR2;jyo_YPr!LthYfae>+mZ5<3Mh%E7Rq2j;K7x~sf{)&XE6iDg{M)3RxN~P z(CrjaW!++VHDg1dr%{di4qon4*+}on9Z;6K=i8IDyu%#YjsFa?S{PwL&u0C_-pN~v4 zmJ&PF6Nr3WenG-vHremByEmU;^qS56EpGS-j%+T_ZRr&7*2|F&2d$0-*YD&?ia}RC zr&h=!8GK2J*RBVW!*2#+8!%}?G98tgi+TpXSIMlIP~!BT*lT1{`hCjr}?>1cQ3$wtdS|T7G!w4UxM*Un~I~in$mki2u0!y z*JtV>i5TgX*_dxXta?SDWv?g99ke!#?Z4}2t;qE2*mJto2h@97tfHVZJvj~TRmJ|M zL(@tX_u+*~WnWLflwMgvD?3gs8!z#&{gYwa9~dwUbFQFW#mzFfHPwwGNJ=a8NUL9K zPEC`qs#W~r(Q<(it1H(*c$supAID)LCCiA>lXh~+@Y~s?{v6=uFxHfH!WmVk z;~biop@rsX=_wYalsDe$VS8D^gvb!s;6z4Co(nGX@c4W~RpIpAS9X>YEP#dvoLTc6gp%2Rr@apZN zTp*G8g_ZpTwwsxcW|>3N_xi!!_YsjrJ>WN#s&)Ti5@1Wq(K$3?IZLu&D^zEz*gIu9 z5jQKOe^w!HMnZ#!+%}vyuC3$f7o?*@Tz}a{@H4F{u8n6Uba@~Yoe!usj4;dRy_DO> z(c|~)6NoS%>~?2!q3HS#Q7p}yN*yOszA7BT*RDF`ax+ay-1)lnxYCTt8&zl2Dmaw( zBgCj?2=A}Ic!qNB4Dj0xLKuR}3gX})cR$cuv~|%-Sa}{hTF*@58+kdpEZTBMgjd|WnR9&Q^HTM~!&$1hh`snS6t5u_bi25n4SufrHMkYZUkaVqw*G1R zX-Kx8;8*TKJD;uMWMnHe8rr(E6@d^k*L`on5D6 z(|2uUCi?|@zzqE0r@r7~l1Z51w{kYYtykszzB#Y;oa(o>6}LM?9~Xh4K(eTJAh(y& zo4=+ErJ3t=dZ6Sby|kZYC}kzPHwtWqtity;o$I(aAVMlpJ2w6F?DtL#=$#jQ-YA<* zz1V2dJFB2p@1n#&Y#+QxdlSI`yLhW9#pN*DO8)hcJk?Rf>oG}jB@3#K-uOZ8#g8!RT{tTCpi)tpK~H*8D=J*67J?AG-GU_$rXnc4NUJxMtu<5nDBJU! z`!V|BP(dk60Kun9mVhbe`?cg$tIzw5is@?(;oW+zBXr_1cxo5t?YecUjW1bY)9Jnr}EPrUu1M4OYj201h4VV~FeeXniua%YN%q@4 zzRLBHcUn&cUBN%Pl`J=Fnd9{LmbUkb*5OR3)U&;M4V~+7-kc6{1WjOcn8t~@j%f=2 zg9$#a+@OMMHT*jt6O)zK4E~SspuxoC%h?7$_oaI3&L%OmX6RS`wl7^V_G06Ui{_6$ zvn=Hwh)-y5%O?;ED>iIi5V48#{pPJ7`?s3jA7uQg`^idRO18KI+wA!5)N27b5d37G zMpGfc0JdRzFa#wUN1j;@8I*%{(0iN;b3`7t+&xCs(n3{tG39Vgl&Lgw;U}xcd(98m z!J47~cq1##*t&PXV`$bAXi7wUW8NdrMG(Na58D}B;Mnf$NvRT}1=v(rG5{~UC(LsO zM1w+|4MrS6_}$Y~cB??-(;2-*yPQ>yyp<`7T_gO%u;Kc+(=o)NJhn8uF{?+%U_4-` zst=Y~|Lbn%=g)398#iz0rLEw&_B7*1U<4^b9f%Y(=|563r>Z*X!fVg|IF=jQ7-HeT zXH5)G-)C?dRX8dXif#&1IYon6i@mRRyGoQUhYhHQl8(LSdTjn`bkvttq!rj0f9&g< z?mJFU&}Pmu&zx|(i{Gg4VgN$Ypw{-^E~5TCYV<~`#CO@Jqs+yys?JI5e}^o@SFE2) z-qnw+vY#b16cBh$Xj;wH$4%HZAN9cH3K2h(%XI^51|JZkiJsq}61MKm9?IZ$?|7#O z8$HFX7b6YzH5plw)rslBcE`|qZJs~W$1SyWW*C~DztGMj#145jPVdZ3<4m8lwZu~% zto9<@Y6`n*adyb4nM986+NlyjpD5}fUfxO~lb7TkxnavM_FQe)76Vh4vD@H@w@QCe zMJ1g1Nr_mIn_fbI&4p@1TLE*q%jg8&o#AWE3}^dLJVMQv*?}={qp@i z^3c;CGo5JTh;Ua;CP-t6uac!}S9*7hjRAbAaLu71)1EA9AVcV3;MES!l)h>RFV-tL zC^-uLy(T|V&^>=4g!Z(hVZ>JE=Q^fD=;3+4sHAFrF9t7u8138jJVVvPD7)0+e)m^F z&2pOl-yZt|lXAXhnN1Y`WD^Iv9fAuqzTO}gYzs^I$Z-Gk)~|xs#`HML?vl}^cW_VS zNBai6FjeJ?#h(;L?i2Ko0=Z*9YX6d$b!endVu9H%8byXaM zCzz}l@8-Nno3fa3p@`9&Ab2Nk(ZU;1Z5$OjR6(b9QGamop&`}0zyhHt*ULYPaz%G^ zZo}Y)gwf><<9WVK1;08ltMGXqlqGDe8_eQQgl{V3{S-ZkM8~6IKy`C#v_Yl1Lj(0^ z-MFteI?-?k&b*E&WTty&-=$?2p!qTg>-W~IQF$R1niLd%Zp9+ z^`!g}Vp^;qM`QZ0)%2!OTlhl$eNjM5Tsx1#$!}{_FsIt=E-MP){BywyE>!&cg6xj< z{v4PNMz#61c8YMv_OkW<%sB0Ba>s?{p`;6z=M&ck4jaHa!R#+I#%@_`_tLE3dVtv2SgoN$EnTL zccUJbL5k3Itszn z$lVsL;gBHQ20Qan--l1u^lo`kV-qNk!T@(n4bby#H;ZrUs@(?zR-3rZeo@uYnRgyW z$LL)+RTHHr-c+?7hURGb5kegDRK=TuX|nf6|MmU|d|sdU*ZCKPi59r>wW#HjKfsOB z(|l2hzIgOged2Y8&FF@jwe3#rPUX)BG6yv56H0f0WoqGya+urpgcur-IGtU&%qJ*! zAPD-Ft9I>%_%t~9=)Z)*!~a`@%2|^Q8u0kPwLzSH52u!1fu?3In6c~LcHC)v>(@g> z=_n{voKzqR$D3NDO-Uhm{~@d(maYD}mWk)f6C)ncl6$~(DlYWF4j!NIxl1m%|3Wc< zQ>Dg%O(WWK%a75ik<5@=)rR zw9vv#i~+kn5TrM4_^}-m%|k^wk{<8k{G|D_kl~*p zXlDCL%W+Kdn2Zg|2j}RletHhiyT!dsAsH!!t}H{OwJd%VwxJs$AFDS7SGG z2sn@P4m8O+O#vKx)exR@6YY8p+^>NX{U5R{_?m;?qt((; zynf~2_hgx|b*y^5DNkl>(J9P8ZF+ELiLNal`@*Jm1WfQYipO0%h~N;$-AEN@Pl2>! zLp5hn0!fesG0GzGk71}u|5|7_F%mm5^1)L`1Jzj=T0}AAqsRnbZv2~^LvV}8`s7U{ z%I;hINQ?@h0UngX|G^-3Z}il8Qf&!;fKu8E!Tsd&^j_&Bt55_qURj16+(odB8{~DQ z5h-3pV)j|b-r00R@2o&ef_XvD^Ko7)r}

RCao3Ve1Pp8uA8L3clv7?1LbU&&fmS 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

Tn2R`4`+dL!w^!@^$qnkZH;Wpx~8;l=zYxLHK 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 aJ@m^z=fC?n@Gbb?>Xp7L7+-Ju{{I7t#qEm# diff --git a/docs/internals/object-graph.vsd b/docs/internals/object-graph.vsd index 3d74cbb8d228a000fa2d381a0b8663937340a624..70989e1dd3cd87167ef82bd511e9dcc639057656 100644 GIT binary patch delta 69400 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 PFkqM0DX%H`M(qCqLhErB delta 69240 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 From cf77aa53d76595228deb5cdd7ee655fea81066e2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 4 Jun 2017 22:50:17 +0200 Subject: [PATCH 0968/1387] docs: internals: fix ASCII art equations --- docs/internals/data-structures.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 785c095a..888a4da1 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -506,7 +506,7 @@ The chunks cache is a HashIndex_. Indexes / Caches memory usage ----------------------------- -Here is the estimated memory usage of |project_name| - it's complicated: +Here is the estimated memory usage of |project_name| - it's complicated:: chunk_count ~= total_file_size / 2 ^ HASH_MASK_BITS @@ -520,13 +520,12 @@ Here is the estimated memory usage of |project_name| - it's complicated: = chunk_count * 164 + total_file_count * 240 Due to the hashtables, the best/usual/worst cases for memory allocation can -be estimated like that: +be estimated like that:: mem_allocation = mem_usage / load_factor # l_f = 0.25 .. 0.75 mem_allocation_peak = mem_allocation * (1 + growth_factor) # g_f = 1.1 .. 2 - All units are Bytes. It is assuming every chunk is referenced exactly once (if you have a lot of From 19b425a5c840d8a1442e6af076ccf59ff18f3088 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 4 Jun 2017 22:58:15 +0200 Subject: [PATCH 0969/1387] docs: internals: more HashIndex details --- docs/internals/data-structures.rst | 33 +++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 888a4da1..e6e52fe6 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -499,7 +499,11 @@ The chunks cache is a key -> value mapping and contains: - size - encrypted/compressed size -The chunks cache is a HashIndex_. +The chunks cache is a HashIndex_. Due to some restrictions of HashIndex, +the reference count of each given chunk is limited to a constant, MAX_VALUE +(introduced below in HashIndex_), approximately 2**32. +If a reference count hits MAX_VALUE, decrementing it yields MAX_VALUE again, +i.e. the reference count is pinned to MAX_VALUE. .. _cache-memory-usage: @@ -598,9 +602,32 @@ outputs of a cryptographic hash or MAC and thus already have excellent distribut Thus, HashIndex simply uses the first 32 bits of the key as its "hash". The format is easy to read and write, because the buckets array has the same layout -in memory and on disk. Only the header formats differ. +in memory and on disk. Only the header formats differ. The on-disk header is +``struct HashHeader``: -.. todo:: Describe HashHeader +- First, the HashIndex magic, the eight byte ASCII string "BORG_IDX". +- Second, the signed 32-bit number of entries (i.e. buckets which are not deleted and not empty). +- Third, the signed 32-bit number of buckets, i.e. the length of the buckets array + contained in the file, and the modulus for index calculation. +- Fourth, the signed 8-bit length of keys. +- Fifth, the signed 8-bit length of values. This has to be at least four bytes. + +All fields are packed. + +The HashIndex is *not* a general purpose data structure. +The value size must be at least 4 bytes, and these first bytes are used for in-band +signalling in the data structure itself. + +The constant MAX_VALUE (defined as 2**32-1025 = 4294966271) defines the valid range for +these 4 bytes when interpreted as an uint32_t from 0 to MAX_VALUE (inclusive). +The following reserved values beyond MAX_VALUE are currently in use (byte order is LE): + +- 0xffffffff marks empty buckets in the hash table +- 0xfffffffe marks deleted buckets in the hash table + +HashIndex is implemented in C and wrapped with Cython in a class-based interface. +The Cython wrapper checks every passed value against these reserved values and +raises an AssertionError if they are used. Encryption ---------- From 36bdc9d15e239879cffbb75f099d7ad5de1a2e6e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 4 Jun 2017 14:13:04 +0200 Subject: [PATCH 0970/1387] internals: rewrite manifest & feature flags --- docs/internals/data-structures.rst | 121 ++++++++++++++++++++++++++--- docs/internals/security.rst | 2 + 2 files changed, 111 insertions(+), 12 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index e6e52fe6..c31863ce 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -294,22 +294,119 @@ More on how this helps security in :ref:`security_structural_auth`. The manifest ~~~~~~~~~~~~ -The manifest is an object with an all-zero key that references all the -archives. It contains: +The manifest is the root of the object hierarchy. It references +all archives in a repository, and thus all data in it. +Since no object references it, it cannot be stored under its ID key. +Instead, the manifest has a fixed all-zero key. -* Manifest version -* A list of archive infos -* timestamp -* config +The manifest is rewritten each time an archive is created, deleted, +or modified. It looks like this: -Each archive info contains: +.. code-block:: python -* name -* id -* time + { + b'version': 1, + b'timestamp': b'2017-05-05T12:42:23.042864', + b'item_keys': [b'acl_access', b'acl_default', ...], + b'config': {}, + b'archives': { + b'archive name': { + b'id': b'<32 byte binary object ID>', + b'time': b'2017-05-05T12:42:22.942864', + }, + }, + b'tam': ..., + } -It is the last object stored, in the last segment, and is replaced -each time an archive is added, modified or deleted. +The *version* field can be either 1 or 2. The versions differ in the +way feature flags are handled, described below. + +The *timestamp* field was used to avoid a certain class of replay attack. +It is still used for that purpose, however, the newer replay protection +introduced in Borg 1.1 includes all reply attacks. Thus it is not strictly +necessary any more. + +*item_keys* is a list containing all Item_ keys that may be encountered in +the repository. It is used by *borg check*, which verifies that all keys +in all items are a subset of these keys. Thus, an older version of *borg check* +supporting this mechanism can correctly detect keys introduced in later versions. + +The *tam* key is part of the :ref:`tertiary authentication mechanism ` +(formerly known as "tertiary authentication for metadata") and authenticates +the manifest, since an ID check is not possible. + +*config* is a general-purpose location for additional metadata. All versions +of Borg preserve its contents (it may have been a better place for *item_keys*, +which is not preserved by unaware Borg versions). + +.. rubric:: Feature flags + +Feature flags are used to add features to data structures without causing +corruption if older versions are used to access or modify them. + +The *config* key stores the feature flags enabled on a repository: + +.. code-block:: python + + config = { + b'feature_flags': { + b'read': { + b'mandatory': [b'some_feature'], + }, + b'check': { + b'mandatory': [b'other_feature'], + } + b'write': ..., + b'delete': ... + }, + } + +The top-level distinction for feature flags is the operation the client intends +to perform, + +| the *read* operation includes extraction and listing of archives, +| the *write* operation includes creating new archives, +| the *delete* (archives) operation, +| the *check* operation requires full understanding of everything in the repository. +| + +These are weakly set-ordered; *check* will include everything required for *delete*, +*delete* will likely include *write* and *read*. However, *read* may require more +features than *write* (due to ID-based deduplication, *write* does not necessarily +require reading/understanding repository contents). + +Each operation can contain several sets of feature flags. Only one set, +the *mandatory* set is currently defined. + +Upon reading the manifest, the Borg client has already determined which operation +is to be performed. If feature flags are found in the manifest, the set +of feature flags supported by the client is compared to the mandatory set +found in the manifest. If any unsupported flags are found (i.e. the mandatory set is +a superset of the features supported by the Borg client used), the operation +is aborted with a *MandatoryFeatureUnsupported* error: + + Unsupported repository feature(s) {'some_feature'}. A newer version of borg is required to access this repository. + +Older Borg releases do not have this concept and do not perform feature flags checks. +These can be locked out with manifest version 2. Thus, the only difference between +manifest versions 1 and 2 is that the latter is only accepted by Borg releases +implementing feature flags. + +.. rubric:: Defined feature flags + +Currently no feature flags are defined. + +From currently planned features, some examples follow, +these may/may not be implemented and purely serve as examples. + +- A mandatory *read* feature could be using a different encryption scheme (e.g. session keys). + This may not be mandatory for the *write* operation - reading data is not strictly required for + creating an archive. +- Any additions to the way chunks are referenced (e.g. to support larger archives) would + become a mandatory *delete* and *check* feature; *delete* implies knowing correct + reference counts, so all object references need to be understood. *check* must + discover the entire object graph as well, otherwise the "orphan chunks check" + could delete data still in use. .. _archive: diff --git a/docs/internals/security.rst b/docs/internals/security.rst index 6535ef5b..66a7ea5e 100644 --- a/docs/internals/security.rst +++ b/docs/internals/security.rst @@ -63,6 +63,8 @@ in a particular part of its own data structure assigns this meaning. This results in a directed acyclic graph of authentication from the manifest to the data chunks of individual files. +.. _tam_description: + .. rubric:: Authenticating the manifest Since the manifest has a fixed ID (000...000) the aforementioned authentication From f2fd6fc6999a7b4ef7b87c9b274aae84a521a488 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 4 Jun 2017 18:56:45 +0200 Subject: [PATCH 0971/1387] docs: internals: cache feature flags --- docs/internals/data-structures.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index c31863ce..34cdbaa9 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -392,6 +392,36 @@ These can be locked out with manifest version 2. Thus, the only difference betwe manifest versions 1 and 2 is that the latter is only accepted by Borg releases implementing feature flags. +.. rubric:: Cache feature flags + +`The cache`_ does not have its separate flag of feature flags. Instead, Borg stores +which flags were used to create or modify a cache. + +All mandatory manifest features from all operations are gathered in one set. +Then, two sets of features are computed; + +- those features that are supported by the client and mandated by the manifest + are added to the *mandatory_features* set, +- the complement to *mandatory_features*, *ignored_features* comprised + of those features mandated by the manifest, but not supported by the client. + +Because the client previously checked compliance with the mandatory set of features +required for the particular operation it is executing, the *mandatory_features* set +will contain all necessary features required for using the cache safely. + +Conversely, the *ignored_features* set contains only those features which were not +relevant to operating the cache. Otherwise, the client would not pass the feature +set test against the manifest. + +When opening a cache and the *mandatory_features* set is a superset of the features +supported by the client, the cache is wiped out and rebuilt. +Since a client not supporting a mandatory feature that the cache was built with +would be unable to update it correctly. + +When opening a cache and the intersection of *ignored_features* and the features +supported by the client contains any elements, i.e. the client possesses features +that the previous client did not have, the cache is wiped out and rebuilt. + .. rubric:: Defined feature flags Currently no feature flags are defined. From c427d238f4b3fe81b4b850d0f2b0cc03f34cf834 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 5 Jun 2017 00:21:04 +0200 Subject: [PATCH 0972/1387] docs: internals: amend feature flags --- docs/internals/data-structures.rst | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 34cdbaa9..bc703972 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -310,7 +310,7 @@ or modified. It looks like this: b'item_keys': [b'acl_access', b'acl_default', ...], b'config': {}, b'archives': { - b'archive name': { + b'2017-05-05-system-backup': { b'id': b'<32 byte binary object ID>', b'time': b'2017-05-05T12:42:22.942864', }, @@ -392,6 +392,10 @@ These can be locked out with manifest version 2. Thus, the only difference betwe manifest versions 1 and 2 is that the latter is only accepted by Borg releases implementing feature flags. +Therefore, as soon as any mandatory feature flag is enabled in a repository, +the manifest version must be switched to version 2 in order to lock out all +Borg releases unaware of feature flags. + .. rubric:: Cache feature flags `The cache`_ does not have its separate flag of feature flags. Instead, Borg stores @@ -414,13 +418,18 @@ relevant to operating the cache. Otherwise, the client would not pass the featur set test against the manifest. When opening a cache and the *mandatory_features* set is a superset of the features -supported by the client, the cache is wiped out and rebuilt. -Since a client not supporting a mandatory feature that the cache was built with +supported by the client, the cache is wiped out and rebuilt, +since a client not supporting a mandatory feature that the cache was built with would be unable to update it correctly. When opening a cache and the intersection of *ignored_features* and the features supported by the client contains any elements, i.e. the client possesses features -that the previous client did not have, the cache is wiped out and rebuilt. +that the previous client did not have and those new features are enabled in the repository, +the cache is wiped out and rebuilt. + +While the former condition likely requires no tweaks, the latter condition is formulated +in an especially conservative way to play it safe. It seems likely that specific features +might be exempted from the latter condition. .. rubric:: Defined feature flags From da04aba5c5ba87134aedd6e4175ea17defed8ab1 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 5 Jun 2017 00:41:30 +0200 Subject: [PATCH 0973/1387] docs: internals: feature flags introduction/rationale --- docs/internals/data-structures.rst | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index bc703972..8f593055 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -339,10 +339,31 @@ the manifest, since an ID check is not possible. of Borg preserve its contents (it may have been a better place for *item_keys*, which is not preserved by unaware Borg versions). -.. rubric:: Feature flags +Feature flags ++++++++++++++ Feature flags are used to add features to data structures without causing -corruption if older versions are used to access or modify them. +corruption if older versions are used to access or modify them. The main issues +to consider for a feature flag oriented design are flag granularity, +flag storage, and cache_ invalidation. + +Feature flags are divided in approximately three categories, detailed below. +Due to the nature of ID-based deduplication, write (i.e. creating archives) and +read access are not symmetric; it is possible to create archives referencing +chunks that are not readable with the current feature set. The third +category are operations that require accurate reference counts, for example +archive deletion and check. + +As the manifest is always updated and always read, it is the ideal place to store +feature flags, comparable to the super-block of a file system. The only issue problem +is to recover from a lost manifest, i.e. how is it possible to detect which feature +flags are enabled, if there is no manifest to tell. This issue is left open at this time, +but is not expected to be a major hurdle; it doesn't have to be handled efficiently, it just +needs to be handled. + +Lastly, cache_ invalidation is handled by noting which feature +flags were and were not used to manipulate a cache. This allows to detect whether +the cache needs to be invalidated, i.e. rebuilt from scratch. See `Cache feature flags`_ below. The *config* key stores the feature flags enabled on a repository: @@ -396,6 +417,7 @@ Therefore, as soon as any mandatory feature flag is enabled in a repository, the manifest version must be switched to version 2 in order to lock out all Borg releases unaware of feature flags. +.. _Cache feature flags: .. rubric:: Cache feature flags `The cache`_ does not have its separate flag of feature flags. Instead, Borg stores From e80c0f7c5e741f9cfb3ca922d47e60eba586c8a8 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 5 Jun 2017 01:00:41 +0200 Subject: [PATCH 0974/1387] docs: fix way too small figures in pdf --- docs/conf.py | 1 + docs/internals.rst | 2 ++ docs/internals/data-structures.rst | 9 +++++++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1baf39a7..30171b3b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -207,6 +207,7 @@ latex_logo = '_static/logo.pdf' latex_elements = { 'papersize': 'a4paper', 'pointsize': '10pt', + 'figure_align': 'H', } # For "manual" documents, if this is true, then toplevel headings are parts, diff --git a/docs/internals.rst b/docs/internals.rst index 35ab0018..973d1e6e 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -27,6 +27,8 @@ chunk is checked against the :ref:`chunks cache `, which is a hash-table of all chunks that already exist. .. figure:: internals/structure.png + :figwidth: 100% + :width: 100% Layers in Borg. On the very top commands are implemented, using a data access layer provided by the Archive and Item classes. diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index e6e52fe6..c9ecf792 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -204,8 +204,9 @@ commit is written to the new segment. Then, the old segment is deleted A simplified example (excluding conditional compaction and with simpler commit logic) showing the principal operation of compaction: -.. figure:: - compaction.png +.. figure:: compaction.png + :figwidth: 100% + :width: 100% (The actual algorithm is more complex to avoid various consistency issues, refer to the ``borg.repository`` module for more comments and documentation on these issues.) @@ -288,6 +289,8 @@ by their chunk ID, which is cryptographically derived from their contents. More on how this helps security in :ref:`security_structural_auth`. .. figure:: object-graph.png + :figwidth: 100% + :width: 100% .. _manifest: @@ -640,6 +643,8 @@ and both are stored in the chunk. Encryption and MAC use two different keys. Each chunk consists of ``TYPE(1)`` + ``MAC(32)`` + ``NONCE(8)`` + ``CIPHERTEXT``: .. figure:: encryption.png + :figwidth: 100% + :width: 100% In AES-CTR mode you can think of the IV as the start value for the counter. The counter itself is incremented by one after each 16 byte block. From 50bcd7843d89b881978fd84075f2709f871ce45d Mon Sep 17 00:00:00 2001 From: TW Date: Mon, 5 Jun 2017 09:59:17 +0200 Subject: [PATCH 0975/1387] recreate: keep timestamps as in original archive, fixes #2384 (#2607) the timestamps of the recreated archive (in the archive metadata and also in the manifest) are now as they were for the original archive. they are important metadata about the archive contents and should therefore be kept "as is". note: when using -v --stats, the timestamps shown there for recreate are about the recreate start/end/duration. --- src/borg/archive.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index b72bbda4..fb680d9b 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1699,11 +1699,14 @@ class ArchiveRecreater: def save(self, archive, target, comment=None, replace_original=True): if self.dry_run: return - timestamp = archive.ts.replace(tzinfo=None) if comment is None: comment = archive.metadata.get('comment', '') - target.save(timestamp=timestamp, comment=comment, additional_metadata={ + target.save(comment=comment, additional_metadata={ + # keep some metadata as in original archive: + 'time': archive.metadata.time, + 'time_end': archive.metadata.time_end, 'cmdline': archive.metadata.cmdline, + # but also remember recreate metadata: 'recreate_cmdline': sys.argv, }) if replace_original: From 909f099b1abf8bc97330a4cd994b10332c0f3285 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 5 Jun 2017 14:21:26 +0200 Subject: [PATCH 0976/1387] algorithms.checksums: work around GCC 4.4 bug by disabling CLMUL Also disabling this code path for 4.5; 4.6 was tested iirc. --- src/borg/algorithms/crc32_dispatch.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/borg/algorithms/crc32_dispatch.c b/src/borg/algorithms/crc32_dispatch.c index 19c7ebe9..30700bc0 100644 --- a/src/borg/algorithms/crc32_dispatch.c +++ b/src/borg/algorithms/crc32_dispatch.c @@ -3,6 +3,12 @@ #include "crc32_slice_by_8.c" #ifdef __GNUC__ +/* + * GCC 4.4(.7) has a bug that causes it to recurse infinitely if an unknown option + * is pushed onto the options stack. GCC 4.5 was not tested, so is excluded as well. + * GCC 4.6 is known good. + */ +#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) /* * clang also has or had GCC bug #56298 explained below, but doesn't support * target attributes or the options stack. So we disable this faster code path for clang. @@ -66,6 +72,7 @@ #endif /* if __x86_64__ */ #endif /* ifndef __OpenBSD__ */ #endif /* ifndef __clang__ */ +#endif /* __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) */ #endif /* ifdef __GNUC__ */ #ifdef FOLDING_CRC @@ -75,6 +82,7 @@ static uint32_t crc32_clmul(const uint8_t *src, long len, uint32_t initial_crc) { + (void)src; (void)len; (void)initial_crc; assert(0); return 0; } From fec0958b7f203bbd5624041de0ccc5ac1ba1ab28 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 5 Jun 2017 15:39:55 +0200 Subject: [PATCH 0977/1387] docs: internals: columnize rather long ToC --- docs/borg_theme/css/borg.css | 13 +++++++++++++ docs/internals.rst | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 6c82f9b1..9b8074ee 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -28,3 +28,16 @@ #usage dl dl dd { margin-bottom: 0.5em; } + +#internals .toctree-wrapper > ul { + column-count: 3; +} + +#internals .toctree-wrapper > ul > li { + display: inline-block; + font-weight: bold; +} + +#internals .toctree-wrapper > ul > li > ul { + font-weight: normal; +} diff --git a/docs/internals.rst b/docs/internals.rst index 973d1e6e..af951a8a 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -39,7 +39,7 @@ hash-table of all chunks that already exist. (Repository) or remotely (RemoteRepository). .. toctree:: - :caption: Contents + :caption: Internals contents internals/security internals/data-structures From 4e6c56538a2682f748d094328eaab10182a5c370 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 5 Jun 2017 15:43:13 +0200 Subject: [PATCH 0978/1387] test: suppress tar's future timestamp warning in this case, it is expected as we archived a file with such a ts. --- src/borg/testsuite/archiver.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 5c0c52a1..1ce661ef 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2431,7 +2431,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.cmd('export-tar', self.repository_location + '::test', 'simple.tar', '--progress') with changedir('output'): # This probably assumes GNU tar. Note -p switch to extract permissions regardless of umask. - subprocess.check_call(['tar', 'xpf', '../simple.tar']) + subprocess.check_call(['tar', 'xpf', '../simple.tar', '--warning=no-timestamp']) self.assert_dirs_equal('input', 'output/input', ignore_bsdflags=True, ignore_xattrs=True, ignore_ns=True) @requires_gnutar @@ -2447,7 +2447,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 assert 'input/file1\n' in list assert 'input/dir2\n' in list with changedir('output'): - subprocess.check_call(['tar', 'xpf', '../simple.tar.gz']) + subprocess.check_call(['tar', 'xpf', '../simple.tar.gz', '--warning=no-timestamp']) self.assert_dirs_equal('input', 'output/input', ignore_bsdflags=True, ignore_xattrs=True, ignore_ns=True) @requires_gnutar @@ -2463,7 +2463,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 assert 'input/file1\n' in list assert 'input/dir2\n' in list with changedir('output'): - subprocess.check_call(['tar', 'xpf', '../simple.tar']) + subprocess.check_call(['tar', 'xpf', '../simple.tar', '--warning=no-timestamp']) self.assert_dirs_equal('input', 'output/', ignore_bsdflags=True, ignore_xattrs=True, ignore_ns=True) @requires_hardlinks @@ -2472,7 +2472,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self._extract_hardlinks_setup() self.cmd('export-tar', self.repository_location + '::test', 'output.tar', '--strip-components=2') with changedir('output'): - subprocess.check_call(['tar', 'xpf', '../output.tar']) + subprocess.check_call(['tar', 'xpf', '../output.tar', '--warning=no-timestamp']) assert os.stat('hardlink').st_nlink == 2 assert os.stat('subdir/hardlink').st_nlink == 2 assert os.stat('aaaa').st_nlink == 2 @@ -2484,7 +2484,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self._extract_hardlinks_setup() self.cmd('export-tar', self.repository_location + '::test', 'output.tar', 'input/dir1') with changedir('output'): - subprocess.check_call(['tar', 'xpf', '../output.tar']) + subprocess.check_call(['tar', 'xpf', '../output.tar', '--warning=no-timestamp']) assert os.stat('input/dir1/hardlink').st_nlink == 2 assert os.stat('input/dir1/subdir/hardlink').st_nlink == 2 assert os.stat('input/dir1/aaaa').st_nlink == 2 From fc599befc9402b20a97fb5262df465757b7ae2fc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 5 Jun 2017 16:04:03 +0200 Subject: [PATCH 0979/1387] docs: internals: columnize rather long ToC [webkit fixup] --- docs/borg_theme/css/borg.css | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 9b8074ee..bb5a4d0d 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -31,6 +31,7 @@ #internals .toctree-wrapper > ul { column-count: 3; + -webkit-column-count: 3; } #internals .toctree-wrapper > ul > li { From f5e7d964cf22b7d3758c85f2d49070cfdb6dbf62 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 5 Jun 2017 22:26:04 +0200 Subject: [PATCH 0980/1387] docs: internals: feature flags set theory --- docs/internals/data-structures.rst | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 8f593055..1e50e012 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -321,10 +321,8 @@ or modified. It looks like this: The *version* field can be either 1 or 2. The versions differ in the way feature flags are handled, described below. -The *timestamp* field was used to avoid a certain class of replay attack. -It is still used for that purpose, however, the newer replay protection -introduced in Borg 1.1 includes all reply attacks. Thus it is not strictly -necessary any more. +The *timestamp* field is used to avoid logical replay attacks where +the server just resets the repository to a previous state. *item_keys* is a list containing all Item_ keys that may be encountered in the repository. It is used by *borg check*, which verifies that all keys @@ -337,7 +335,9 @@ the manifest, since an ID check is not possible. *config* is a general-purpose location for additional metadata. All versions of Borg preserve its contents (it may have been a better place for *item_keys*, -which is not preserved by unaware Borg versions). +which is not preserved by unaware Borg versions, releases predating 1.0.4). + +.. This was implemented in PR#1149, 78121a8 and a7b5165 Feature flags +++++++++++++ @@ -362,8 +362,9 @@ but is not expected to be a major hurdle; it doesn't have to be handled efficien needs to be handled. Lastly, cache_ invalidation is handled by noting which feature -flags were and were not used to manipulate a cache. This allows to detect whether -the cache needs to be invalidated, i.e. rebuilt from scratch. See `Cache feature flags`_ below. +flags were and which were not understood while manipulating a cache. +This allows to detect whether the cache needs to be invalidated, +i.e. rebuilt from scratch. See `Cache feature flags`_ below. The *config* key stores the feature flags enabled on a repository: @@ -403,7 +404,7 @@ Upon reading the manifest, the Borg client has already determined which operatio is to be performed. If feature flags are found in the manifest, the set of feature flags supported by the client is compared to the mandatory set found in the manifest. If any unsupported flags are found (i.e. the mandatory set is -a superset of the features supported by the Borg client used), the operation +not a subset of the features supported by the Borg client used), the operation is aborted with a *MandatoryFeatureUnsupported* error: Unsupported repository feature(s) {'some_feature'}. A newer version of borg is required to access this repository. @@ -428,8 +429,8 @@ Then, two sets of features are computed; - those features that are supported by the client and mandated by the manifest are added to the *mandatory_features* set, -- the complement to *mandatory_features*, *ignored_features* comprised - of those features mandated by the manifest, but not supported by the client. +- the *ignored_features* set comprised of those features mandated by the manifest, + but not supported by the client. Because the client previously checked compliance with the mandatory set of features required for the particular operation it is executing, the *mandatory_features* set @@ -439,7 +440,7 @@ Conversely, the *ignored_features* set contains only those features which were n relevant to operating the cache. Otherwise, the client would not pass the feature set test against the manifest. -When opening a cache and the *mandatory_features* set is a superset of the features +When opening a cache and the *mandatory_features* set is a not a subset of the features supported by the client, the cache is wiped out and rebuilt, since a client not supporting a mandatory feature that the cache was built with would be unable to update it correctly. From fed5873e293188d00bd731e161e7878e7f083cca Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 6 Jun 2017 04:26:48 +0200 Subject: [PATCH 0981/1387] remove attic dependency of the tests, fixes #2505 attic.tar.gz contains a repo + corresponding keyfile - all the upgrader module tests need. .tar.gz because the .tar was 20x bigger. --- requirements.d/attic.txt | 5 -- src/borg/testsuite/attic.tar.gz | Bin 0 -> 2822 bytes src/borg/testsuite/upgrader.py | 113 +++++++++++++++----------------- tox.ini | 1 - 4 files changed, 53 insertions(+), 66 deletions(-) delete mode 100644 requirements.d/attic.txt create mode 100644 src/borg/testsuite/attic.tar.gz diff --git a/requirements.d/attic.txt b/requirements.d/attic.txt deleted file mode 100644 index b5068ffd..00000000 --- a/requirements.d/attic.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Please note: -# attic only builds using OpenSSL 1.0.x, it can not be installed using OpenSSL >= 1.1.0. -# If attic is not installed, our unit tests will just skip the tests that require attic. -attic - diff --git a/src/borg/testsuite/attic.tar.gz b/src/borg/testsuite/attic.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..5c0f8dfa43c5a297f25571a8c45904becad902dd GIT binary patch literal 2822 zcma)+dpy(oAICRxiEt>DTtoHyVa*YZEm@bjeAnag`}h3L_mB7c^ZC5r-}m?VdVk-Kk8!FhNLl^h zd642fSv}KpO1DFCJPBkPd<$J#R+!kHwcbfM?q~g$Ss#e~82Hxta%=7gKaiFi81;pD zrs(qVE3b>VDgBK+)8A3g3}@NiMV`h_tuH-G>W6y=S9u;mh|5pBN0+YL{MKUG9?3c| zA9%LQf%f>_UY5*ZRUliPau4FP!k%jBhq=Ti(T;kUi{Dx5M1P_gYWh#cx?uu6vJG!f zJ8I@&rspkCtbl7uIb8vo>6~W***)jvuh`zDCYu2h~Za1U!OYGbo0?X)f3m+MwjwD{BpEM?jFRtSZvd1 zC~K|oZAnklZmUFb6Q6 zNk`GCdnHJL)xf`ZqatC+@FYR@3x6y{tGq_Z?Tt zov>9(&l}%~T{Bo6SJy~KgNTBiT=BQSlXFyjp4c!EWA)p&?-fj>b5+IuYZ?w%>K{Aq z>ehtTNKmA-Kp|8HS_iHh0*=O}5+Yr_5ubMO={(aXf zll4t6?A?vgcgw0i9Mp)%XuTjc->#j$bu={e#UQTZNnR21q6ubj(75{`RBJ`Far9Uj z4EpAV&PU~tszWztVlqzLd!{CS@3ePLwvQ++U`TmAMD+d3+JmxZzh}wr`ZgZmdo}L5 zw_LDwQ_s~o*0K`v^)k_F9Bu=;_x!Qt7U8Htuevil2k-SFhEf#0{OLR|xM%LpN+1L2 zZyw}uqoHU;OL!f|Me*Z~=^RR|{FQ^_uFVjoMz^YZb~;~`Qn=agQ@alG&n*>feVEZr zRGN`Jdfn}_wMo#DZ9N#i5wUVA=4Jj`mEqez1&8k0xBzd^+z7#W(@xM#D99Kq`*T(H z=(uQ=NVV~PB6?t;IT&lzuHucfF(VoL+PNDr5$REBMz?pT+G>rq3>*VR@CS}w%{gK*SG^Qrdv0jgtyy#^C6e_GhF6_Wn!)@{rD z%iomCZzMz{Fjg-JKW{?vCqE9@gZlUTbgE(>8P>jQHGvVLs#5NQ=pP5+DQMcXc41gHQ1sQe?JDIrUX5dr{0}%F zxAm(6U(7dA?u8mD0= z<$K~WAQjPAPR9*#Ft(k*a9!lJCWgh$vUAv58QJTc^?qyDu37 ziy`KOn1px|grnFAdmtGEi(7*ubxefoB`nR%FOd}XZbh-7DIqE$ez**7pk^XG%+*rq zq~G!-YGj7*6GbUb`k-p7lsoBnBz0hMDHW>mPtkxpM3Q}eB-{Vh$k8>~P8es8Z>B^B z7Eig#j7mH7q2l>sVpeYLbQ!=2>O zTx4yOO6RJre0}4AyjJMG4Prt(>xUJ#NuBT!Md*ua zC&--maS(jWRPmK-ZqqAjR100>>35GOGPmx&k*TU9WiM=ozS9K=g4EK8vvfvvG%&zC~Fa`jgkO221eG^AV_CQms(u1(Mtt0oAiJyMipzWv*kIazr><%DORii zi=&rSl|Exua(DdML>E4>t+p7z7?2$@aszQ$ht+?|l5&{|^G2-zcV_oa&MkST)VYcP+quBzd zszkM&-ouIER({d{{jpNYcUwIJW?%DJS(*zWDE!v%yYJ5jB96xK%F+)dERj!Krr_Z0As<0C)>hif+=+EGiR}{#cnyJ}^Q2icEvzX( z-Gb?qVdOpFz-YWEO(Ka#mlVkU&F3lXVRani%b=_{DFcuA$!C$vPQt~|IL3`i%h2Ci n?JRK1HoFzjA_(*sNG_Wk literal 0 HcmV?d00001 diff --git a/src/borg/testsuite/upgrader.py b/src/borg/testsuite/upgrader.py index 982d01e8..3fd7500c 100644 --- a/src/borg/testsuite/upgrader.py +++ b/src/borg/testsuite/upgrader.py @@ -1,14 +1,8 @@ import os +import tarfile import pytest -try: - import attic.repository - import attic.key - import attic.helpers -except ImportError: - attic = None - from ..constants import * # NOQA from ..crypto.key import KeyfileKey from ..upgrader import AtticRepositoryUpgrader, AtticKeyfileKey @@ -17,6 +11,28 @@ from ..repository import Repository from . import are_hardlinks_supported +# tar with a repo and repo keyfile from attic +ATTIC_TAR = os.path.join(os.path.dirname(__file__), 'attic.tar.gz') + + +def untar(tarfname, path, what): + """ + extract tar archive to , all stuff starting with . + + return path to . + """ + + def files(members): + for tarinfo in members: + if tarinfo.name.startswith(what): + yield tarinfo + + with tarfile.open(tarfname, 'r') as tf: + tf.extractall(path, members=files(tf)) + + return os.path.join(path, what) + + def repo_valid(path): """ utility function to check if borg can open a repository @@ -48,15 +64,10 @@ def attic_repo(tmpdir): create an attic repo with some stuff in it :param tmpdir: path to the repository to be created - :returns: a attic.repository.Repository object + :returns: path to attic repository """ - attic_repo = attic.repository.Repository(str(tmpdir), create=True) - # throw some stuff in that repo, copied from `RepositoryTestCase.test1` - for x in range(100): - attic_repo.put(('%-32d' % x).encode('ascii'), b'SOMEDATA') - attic_repo.commit() - attic_repo.close() - return attic_repo + # there is some stuff in that repo, copied from `RepositoryTestCase.test1` + return untar(ATTIC_TAR, str(tmpdir), 'repo') @pytest.fixture(params=[True, False]) @@ -64,52 +75,36 @@ def inplace(request): return request.param -@pytest.mark.skipif(attic is None, reason='cannot find an attic install') -def test_convert_segments(tmpdir, attic_repo, inplace): +def test_convert_segments(attic_repo, inplace): """test segment conversion this will load the given attic repository, list all the segments then convert them one at a time. we need to close the repo before conversion otherwise we have errors from borg - :param tmpdir: a temporary directory to run the test in (builtin - fixture) :param attic_repo: a populated attic repository (fixture) """ + repo_path = attic_repo # check should fail because of magic number - assert not repo_valid(tmpdir) - repository = AtticRepositoryUpgrader(str(tmpdir), create=False) + assert not repo_valid(repo_path) + repository = AtticRepositoryUpgrader(repo_path, create=False) with repository: segments = [filename for i, filename in repository.io.segment_iterator()] repository.convert_segments(segments, dryrun=False, inplace=inplace) repository.convert_cache(dryrun=False) - assert repo_valid(tmpdir) - - -class MockArgs: - """ - mock attic location - - this is used to simulate a key location with a properly loaded - repository object to create a key file - """ - def __init__(self, path): - self.repository = attic.helpers.Location(path) + assert repo_valid(repo_path) @pytest.fixture() -def attic_key_file(attic_repo, tmpdir, monkeypatch): +def attic_key_file(tmpdir, monkeypatch): """ create an attic key file from the given repo, in the keys subdirectory of the given tmpdir - :param attic_repo: an attic.repository.Repository object (fixture - define above) :param tmpdir: a temporary directory (a builtin fixture) - :returns: the KeyfileKey object as returned by - attic.key.KeyfileKey.create() + :returns: path to key file """ - keys_dir = str(tmpdir.mkdir('keys')) + keys_dir = untar(ATTIC_TAR, str(tmpdir), 'keys') # we use the repo dir for the created keyfile, because we do # not want to clutter existing keyfiles @@ -120,44 +115,42 @@ def attic_key_file(attic_repo, tmpdir, monkeypatch): # about anyways. in real runs, the original key will be retained. monkeypatch.setenv('BORG_KEYS_DIR', keys_dir) monkeypatch.setenv('ATTIC_PASSPHRASE', 'test') - return attic.key.KeyfileKey.create(attic_repo, - MockArgs(keys_dir)) + + return os.path.join(keys_dir, 'repo') -@pytest.mark.skipif(attic is None, reason='cannot find an attic install') -def test_keys(tmpdir, attic_repo, attic_key_file): +def test_keys(attic_repo, attic_key_file): """test key conversion test that we can convert the given key to a properly formatted borg key. assumes that the ATTIC_KEYS_DIR and BORG_KEYS_DIR have been properly populated by the attic_key_file fixture. - :param tmpdir: a temporary directory (a builtin fixture) - :param attic_repo: an attic.repository.Repository object (fixture - define above) - :param attic_key_file: an attic.key.KeyfileKey (fixture created above) + :param attic_repo: path to an attic repository (fixture defined above) + :param attic_key_file: path to an attic key file (fixture defined above) """ - with AtticRepositoryUpgrader(str(tmpdir), create=False) as repository: + keyfile_path = attic_key_file + assert not key_valid(keyfile_path) # not upgraded yet + with AtticRepositoryUpgrader(attic_repo, create=False) as repository: keyfile = AtticKeyfileKey.find_key_file(repository) AtticRepositoryUpgrader.convert_keyfiles(keyfile, dryrun=False) - assert key_valid(attic_key_file.path) + assert key_valid(keyfile_path) -@pytest.mark.skipif(attic is None, reason='cannot find an attic install') -def test_convert_all(tmpdir, attic_repo, attic_key_file, inplace): +def test_convert_all(attic_repo, attic_key_file, inplace): """test all conversion steps this runs everything. mostly redundant test, since everything is done above. yet we expect a NotImplementedError because we do not convert caches yet. - :param tmpdir: a temporary directory (a builtin fixture) - :param attic_repo: an attic.repository.Repository object (fixture - define above) - :param attic_key_file: an attic.key.KeyfileKey (fixture created above) + :param attic_repo: path to an attic repository (fixture defined above) + :param attic_key_file: path to an attic key file (fixture defined above) """ + repo_path = attic_repo + # check should fail because of magic number - assert not repo_valid(tmpdir) + assert not repo_valid(repo_path) def stat_segment(path): return os.stat(os.path.join(path, 'data', '0', '0')) @@ -165,8 +158,8 @@ def test_convert_all(tmpdir, attic_repo, attic_key_file, inplace): def first_inode(path): return stat_segment(path).st_ino - orig_inode = first_inode(attic_repo.path) - with AtticRepositoryUpgrader(str(tmpdir), create=False) as repository: + orig_inode = first_inode(repo_path) + with AtticRepositoryUpgrader(repo_path, create=False) as repository: # replicate command dispatch, partly os.umask(UMASK_DEFAULT) backup = repository.upgrade(dryrun=False, inplace=inplace) @@ -181,8 +174,8 @@ def test_convert_all(tmpdir, attic_repo, attic_key_file, inplace): if 'BORG_TESTS_IGNORE_MODES' not in os.environ: assert stat_segment(backup).st_mode & UMASK_DEFAULT == 0 - assert key_valid(attic_key_file.path) - assert repo_valid(tmpdir) + assert key_valid(attic_key_file) + assert repo_valid(repo_path) @pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported') diff --git a/tox.ini b/tox.ini index b033630d..2e04bf2e 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,6 @@ envlist = py{34,35,36},flake8 [testenv] deps = -rrequirements.d/development.txt - -rrequirements.d/attic.txt -rrequirements.d/fuse.txt commands = py.test -n {env:XDISTN:4} -rs --cov=borg --cov-config=.coveragerc --benchmark-skip --pyargs {posargs:borg.testsuite} # fakeroot -u needs some env vars: From f5d2e671292b51a9b2ccf07d639c9af679fa67e4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 6 Jun 2017 04:46:15 +0200 Subject: [PATCH 0982/1387] whitespace changes by coala --- src/borg/compress.pyx | 2 +- src/borg/upgrader.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index c226d494..16f7bcb5 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -59,7 +59,7 @@ cdef class CompressorBase: This exists for a very specific case: If borg recreate is instructed to recompress using Auto compression it needs to determine the _actual_ target compression of a chunk in order to detect whether it should be recompressed. - + For all Compressors that are not Auto this always returns *self*. """ return self diff --git a/src/borg/upgrader.py b/src/borg/upgrader.py index 6c88412f..be5c34a7 100644 --- a/src/borg/upgrader.py +++ b/src/borg/upgrader.py @@ -131,7 +131,6 @@ class AtticRepositoryUpgrader(Repository): @staticmethod def convert_keyfiles(keyfile, dryrun): - """convert key files from attic to borg replacement pattern is `s/ATTIC KEY/BORG_KEY/` in From 95064cd241df15bdc7fac436ad9984a3bef0160a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 25 May 2017 18:51:10 +0200 Subject: [PATCH 0983/1387] repository: truncate segments before unlinking --- src/borg/repository.py | 44 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 597d3ca5..6ef75f8c 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -83,6 +83,30 @@ class Repository: dir/data// dir/index.X dir/hints.X + + File system interaction + ----------------------- + + LoggedIO generally tries to rely on common behaviours across transactional file systems. + + Segments that are deleted are truncated first, which avoids problems if the FS needs to + allocate space to delete the dirent of the segment. This mostly affects CoW file systems, + traditional journaling file systems have a fairly good grip on this problem. + + Note that deletion, i.e. unlink(2), is atomic on every file system that uses inode reference + counts, which includes pretty much all of them. To remove a dirent the inodes refcount has + to be decreased, but you can't decrease the refcount before removing the dirent nor can you + decrease the refcount after removing the dirent. File systems solve this with a lock, + and by ensuring it all stays within the same FS transaction. + + Truncation is generally not atomic in itself, and combining truncate(2) and unlink(2) is of + course never guaranteed to be atomic. Truncation in a classic extent-based FS is done in + roughly two phases, first the extents are removed then the inode is updated. (In practice + this is of course way more complex). + + LoggedIO gracefully handles truncate/unlink splits as long as the truncate resulted in + a zero length file. Zero length segments are considered to not exist, while LoggedIO.cleanup() + will still get rid of them. """ class DoesNotExist(Error): @@ -1111,6 +1135,8 @@ class LoggedIO: filenames = [filename for filename in filenames if filename.isdigit() and int(filename) <= segment] filenames = sorted(filenames, key=int, reverse=reverse) for filename in filenames: + # Note: Do not filter out logically deleted segments (see "File system interaction" above), + # since this is used by cleanup and txn state detection as well. yield int(filename), os.path.join(data_path, dir, filename) def get_latest_segment(self): @@ -1132,6 +1158,9 @@ class LoggedIO: self.segment = transaction_id + 1 for segment, filename in self.segment_iterator(reverse=True): if segment > transaction_id: + # Truncate segment files before unlink(). This can help a full file system recover. + # We can use 'wb', since the segment must exist (just returned by the segment_iterator). + open(filename, 'wb').close() os.unlink(filename) else: break @@ -1207,12 +1236,22 @@ class LoggedIO: if segment in self.fds: del self.fds[segment] try: - os.unlink(self.segment_filename(segment)) + filename = self.segment_filename(segment) + # Truncate segment files before unlink(). This can help a full file system recover. + # In this instance (cf. cleanup()) we need to use r+b (=O_RDWR|O_BINARY) and + # issue an explicit truncate() to avoid creating a file + # if *segment* did not exist in the first place. + with open(filename, 'r+b') as fd: + fd.truncate() + os.unlink(filename) except FileNotFoundError: pass def segment_exists(self, segment): - return os.path.exists(self.segment_filename(segment)) + filename = self.segment_filename(segment) + # When deleting segments, they are first truncated. If truncate(2) and unlink(2) are split + # across FS transactions, then logically deleted segments will show up as truncated. + return os.path.exists(filename) and os.path.getsize(filename) def segment_size(self, segment): return os.path.getsize(self.segment_filename(segment)) @@ -1258,6 +1297,7 @@ class LoggedIO: if segment in self.fds: del self.fds[segment] with open(filename, 'rb') as fd: + # XXX: Rather use mmap. data = memoryview(fd.read()) os.rename(filename, filename + '.beforerecover') logger.info('attempting to recover ' + filename) From ed0a5c798f593f1addd0ef1f01e3c34e6797cd0a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 6 Jun 2017 18:03:21 +0200 Subject: [PATCH 0984/1387] platform.SaveFile: truncate_and_unlink temporary SaveFile is typically used for small files where this is not necessary. The sole exception is the files cache. --- src/borg/helpers.py | 6 ++++++ src/borg/platform/base.py | 6 ++++-- src/borg/repository.py | 12 ++++-------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index db66b822..ee3ced59 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1995,6 +1995,12 @@ def secure_erase(path): os.unlink(path) +def truncate_and_unlink(path): + with open(path, 'r+b') as fd: + fd.truncate() + os.unlink(path) + + def popen_with_error_handling(cmd_line: str, log_prefix='', **kwargs): """ Handle typical errors raised by subprocess.Popen. Return None if an error occurred, diff --git a/src/borg/platform/base.py b/src/borg/platform/base.py index 0d2fb51b..be4b694e 100644 --- a/src/borg/platform/base.py +++ b/src/borg/platform/base.py @@ -1,6 +1,8 @@ import errno import os +from borg.helpers import truncate_and_unlink + """ platform base module ==================== @@ -157,7 +159,7 @@ class SaveFile: def __enter__(self): from .. import platform try: - os.unlink(self.tmppath) + truncate_and_unlink(self.tmppath) except FileNotFoundError: pass self.fd = platform.SyncFile(self.tmppath, self.binary) @@ -167,7 +169,7 @@ class SaveFile: from .. import platform self.fd.close() if exc_type is not None: - os.unlink(self.tmppath) + truncate_and_unlink(self.tmppath) return os.replace(self.tmppath, self.path) platform.sync_dir(os.path.dirname(self.path)) diff --git a/src/borg/repository.py b/src/borg/repository.py index 6ef75f8c..2416abbe 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -18,7 +18,7 @@ from .helpers import Location from .helpers import ProgressIndicatorPercent from .helpers import bin_to_hex from .helpers import hostname_is_unique -from .helpers import secure_erase +from .helpers import secure_erase, truncate_and_unlink from .locking import Lock, LockError, LockErrorT from .logger import create_logger from .lrucache import LRUCache @@ -1159,9 +1159,7 @@ class LoggedIO: for segment, filename in self.segment_iterator(reverse=True): if segment > transaction_id: # Truncate segment files before unlink(). This can help a full file system recover. - # We can use 'wb', since the segment must exist (just returned by the segment_iterator). - open(filename, 'wb').close() - os.unlink(filename) + truncate_and_unlink(filename) else: break @@ -1241,9 +1239,7 @@ class LoggedIO: # In this instance (cf. cleanup()) we need to use r+b (=O_RDWR|O_BINARY) and # issue an explicit truncate() to avoid creating a file # if *segment* did not exist in the first place. - with open(filename, 'r+b') as fd: - fd.truncate() - os.unlink(filename) + truncate_and_unlink(filename) except FileNotFoundError: pass @@ -1297,7 +1293,7 @@ class LoggedIO: if segment in self.fds: del self.fds[segment] with open(filename, 'rb') as fd: - # XXX: Rather use mmap. + # XXX: Rather use mmap, this loads the entire segment (up to 500 MB by default) into memory. data = memoryview(fd.read()) os.rename(filename, filename + '.beforerecover') logger.info('attempting to recover ' + filename) From 1135114520005f04c57a8065bd984277d269e42f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 6 Jun 2017 19:46:57 +0200 Subject: [PATCH 0985/1387] helpers: truncate_and_unlink doc --- src/borg/helpers.py | 11 +++++++++++ src/borg/repository.py | 8 +------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index ee3ced59..1e79f63a 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1996,6 +1996,17 @@ def secure_erase(path): def truncate_and_unlink(path): + """ + Truncate and then unlink *path*. + + Do not create *path* if it does not exist. + Open *path* for truncation in r+b mode (=O_RDWR|O_BINARY). + + Use this when deleting potentially large files when recovering + from a VFS error such as ENOSPC. It can help a full file system + recover. Refer to the "File system interaction" section + in repository.py for further explanations. + """ with open(path, 'r+b') as fd: fd.truncate() os.unlink(path) diff --git a/src/borg/repository.py b/src/borg/repository.py index 2416abbe..a5806373 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -1158,7 +1158,6 @@ class LoggedIO: self.segment = transaction_id + 1 for segment, filename in self.segment_iterator(reverse=True): if segment > transaction_id: - # Truncate segment files before unlink(). This can help a full file system recover. truncate_and_unlink(filename) else: break @@ -1234,12 +1233,7 @@ class LoggedIO: if segment in self.fds: del self.fds[segment] try: - filename = self.segment_filename(segment) - # Truncate segment files before unlink(). This can help a full file system recover. - # In this instance (cf. cleanup()) we need to use r+b (=O_RDWR|O_BINARY) and - # issue an explicit truncate() to avoid creating a file - # if *segment* did not exist in the first place. - truncate_and_unlink(filename) + truncate_and_unlink(self.segment_filename(segment)) except FileNotFoundError: pass From aa7fbff55ff0d92831556852584ba74b019dc5c1 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 6 Jun 2017 21:29:31 +0200 Subject: [PATCH 0986/1387] docs: quickstart: delete problematic BORG_PASSPRHASE use --- docs/quickstart.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index ffc42e35..43d62fc9 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -169,8 +169,8 @@ may be surprised that the following ``export`` has no effect on your command:: export BORG_PASSPHRASE='complicated & long' sudo ./yourborgwrapper.sh # still prompts for password -For more information, see sudo(8) man page. Hint: see ``env_keep`` in -sudoers(5), or try ``sudo BORG_PASSPHRASE='yourphrase' borg`` syntax. +For more information, refer to the sudo(8) man page and ``env_keep`` in +the sudoers(5) man page. .. Tip:: To debug what your borg process is actually seeing, find its PID From 3faafb1dee59093b03ee3e84860094d4b1e069b5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 6 Jun 2017 21:30:58 +0200 Subject: [PATCH 0987/1387] docs: faq: specify "using inline shell scripts" --- docs/faq.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/faq.rst b/docs/faq.rst index cb9c3a2a..71cdc014 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -266,6 +266,7 @@ See :ref:`encrypted_repos` for more details. .. _password_env: .. note:: Be careful how you set the environment; using the ``env`` command, a ``system()`` call or using inline shell scripts + (e.g. ``BORG_PASSPHRASE=hunter12 borg ...``) might expose the credentials in the process list directly and they will be readable to all users on a system. Using ``export`` in a shell script file should be safe, however, as From 52afd5f4ff480274dc54e9277cb188b97809e25f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 7 Jun 2017 00:17:05 +0200 Subject: [PATCH 0988/1387] docs: fix broken changes listing of 1.1.0b6 --- docs/changes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changes.rst b/docs/changes.rst index 6e3394cd..51170348 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -172,6 +172,7 @@ New features: - add --debug-profile option (and also "borg debug convert-profile"), #2473 Fixes: + - hashindex: read/write indices >2 GiB on 32bit systems, better error reporting, #2496 - repository URLs: implement IPv6 address support and also more informative From 8fb7db71bc0ad9991917e5884ad501ceaa898ee0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 7 Jun 2017 00:06:36 +0200 Subject: [PATCH 0989/1387] docs: split usage --- docs/borg_theme/css/borg.css | 6 +- docs/usage.rst | 830 ++-------------------- docs/usage/benchmark_crud.rst.inc | 2 +- docs/usage/break-lock.rst.inc | 2 +- docs/usage/change-passphrase.rst.inc | 2 +- docs/usage/check.rst | 1 + docs/usage/check.rst.inc | 2 +- docs/usage/create.rst | 70 ++ docs/usage/create.rst.inc | 2 +- docs/usage/debug.rst | 34 + docs/usage/delete.rst | 16 + docs/usage/delete.rst.inc | 2 +- docs/usage/diff.rst | 36 + docs/usage/diff.rst.inc | 2 +- docs/usage/export-tar.rst.inc | 2 +- docs/usage/extract.rst | 29 + docs/usage/extract.rst.inc | 2 +- docs/usage/general.rst | 21 + docs/usage/help.rst | 4 + docs/usage/info.rst | 23 + docs/usage/info.rst.inc | 2 +- docs/usage/init.rst | 17 + docs/usage/init.rst.inc | 2 +- docs/usage/key.rst | 42 ++ docs/usage/key_change-passphrase.rst.inc | 2 +- docs/usage/key_export.rst.inc | 2 +- docs/usage/key_import.rst.inc | 2 +- docs/usage/key_migrate-to-repokey.rst.inc | 2 +- docs/usage/list.rst | 27 + docs/usage/list.rst.inc | 2 +- docs/usage/lock.rst | 3 + docs/usage/mount.rst | 44 ++ docs/usage/mount.rst.inc | 2 +- docs/usage/notes.rst | 226 ++++++ docs/usage/prune.rst | 35 + docs/usage/prune.rst.inc | 2 +- docs/usage/recreate.rst | 33 + docs/usage/recreate.rst.inc | 2 +- docs/usage/rename.rst | 13 + docs/usage/rename.rst.inc | 2 +- docs/usage/serve.rst | 31 + docs/usage/serve.rst.inc | 4 +- docs/usage/tar.rst | 18 + docs/usage/umount.rst.inc | 2 +- docs/usage/upgrade.rst | 30 + docs/usage/upgrade.rst.inc | 2 +- docs/usage/with-lock.rst.inc | 2 +- setup.py | 2 +- 48 files changed, 832 insertions(+), 809 deletions(-) create mode 100644 docs/usage/check.rst create mode 100644 docs/usage/create.rst create mode 100644 docs/usage/debug.rst create mode 100644 docs/usage/delete.rst create mode 100644 docs/usage/diff.rst create mode 100644 docs/usage/extract.rst create mode 100644 docs/usage/general.rst create mode 100644 docs/usage/help.rst create mode 100644 docs/usage/info.rst create mode 100644 docs/usage/init.rst create mode 100644 docs/usage/key.rst create mode 100644 docs/usage/list.rst create mode 100644 docs/usage/lock.rst create mode 100644 docs/usage/mount.rst create mode 100644 docs/usage/notes.rst create mode 100644 docs/usage/prune.rst create mode 100644 docs/usage/recreate.rst create mode 100644 docs/usage/rename.rst create mode 100644 docs/usage/serve.rst create mode 100644 docs/usage/tar.rst create mode 100644 docs/usage/upgrade.rst diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index bb5a4d0d..ae807b46 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -21,14 +21,10 @@ color: rgba(255, 255, 255, 0.5); } -#usage dt code { +dt code { font-weight: normal; } -#usage dl dl dd { - margin-bottom: 0.5em; -} - #internals .toctree-wrapper > ul { column-count: 3; -webkit-column-count: 3; diff --git a/docs/usage.rst b/docs/usage.rst index 7997bcb0..28da5e71 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -5,782 +5,54 @@ Usage ===== -|project_name| consists of a number of commands. Each command accepts -a number of arguments and options. The following sections will describe each -command in detail. - -General -------- - -.. include:: usage_general.rst.inc - -In case you are interested in more details (like formulas), please see -:ref:`internals`. For details on the available JSON output, refer to -:ref:`json_output`. - -Common options -~~~~~~~~~~~~~~ - -All |project_name| commands share these options: - -.. include:: usage/common-options.rst.inc - -.. include:: usage/init.rst.inc - -Examples -~~~~~~~~ -:: - - # Local repository, repokey encryption, BLAKE2b (often faster, since Borg 1.1) - $ borg init --encryption=repokey-blake2 /path/to/repo - - # Local repository (no encryption) - $ borg init --encryption=none /path/to/repo - - # Remote repository (accesses a remote borg via ssh) - $ borg init --encryption=repokey-blake2 user@hostname:backup - - # Remote repository (store the key your home dir) - $ borg init --encryption=keyfile user@hostname:backup - -.. include:: usage/create.rst.inc - -Examples -~~~~~~~~ -:: - - # Backup ~/Documents into an archive named "my-documents" - $ borg create /path/to/repo::my-documents ~/Documents - - # same, but list all files as we process them - $ borg create --list /path/to/repo::my-documents ~/Documents - - # Backup ~/Documents and ~/src but exclude pyc files - $ borg create /path/to/repo::my-files \ - ~/Documents \ - ~/src \ - --exclude '*.pyc' - - # Backup home directories excluding image thumbnails (i.e. only - # /home/*/.thumbnails is excluded, not /home/*/*/.thumbnails) - $ borg create /path/to/repo::my-files /home \ - --exclude 're:^/home/[^/]+/\.thumbnails/' - - # Do the same using a shell-style pattern - $ borg create /path/to/repo::my-files /home \ - --exclude 'sh:/home/*/.thumbnails' - - # Backup the root filesystem into an archive named "root-YYYY-MM-DD" - # use zlib compression (good, but slow) - default is lz4 (fast, low compression ratio) - $ borg create -C zlib,6 /path/to/repo::root-{now:%Y-%m-%d} / --one-file-system - - # Backup a remote host locally ("pull" style) using sshfs - $ mkdir sshfs-mount - $ sshfs root@example.com:/ sshfs-mount - $ cd sshfs-mount - $ borg create /path/to/repo::example.com-root-{now:%Y-%m-%d} . - $ cd .. - $ fusermount -u sshfs-mount - - # Make a big effort in fine granular deduplication (big chunk management - # overhead, needs a lot of RAM and disk space, see formula in internals - # docs - same parameters as borg < 1.0 or attic): - $ borg create --chunker-params 10,23,16,4095 /path/to/repo::small /smallstuff - - # Backup a raw device (must not be active/in use/mounted at that time) - $ dd if=/dev/sdx bs=10M | borg create /path/to/repo::my-sdx - - - # No compression (default) - $ borg create /path/to/repo::arch ~ - - # Super fast, low compression - $ borg create --compression lz4 /path/to/repo::arch ~ - - # Less fast, higher compression (N = 0..9) - $ borg create --compression zlib,N /path/to/repo::arch ~ - - # Even slower, even higher compression (N = 0..9) - $ borg create --compression lzma,N /path/to/repo::arch ~ - - # Use short hostname, user name and current time in archive name - $ borg create /path/to/repo::{hostname}-{user}-{now} ~ - # Similar, use the same datetime format as borg 1.1 will have as default - $ borg create /path/to/repo::{hostname}-{user}-{now:%Y-%m-%dT%H:%M:%S} ~ - # As above, but add nanoseconds - $ borg create /path/to/repo::{hostname}-{user}-{now:%Y-%m-%dT%H:%M:%S.%f} ~ - - # Backing up relative paths by moving into the correct directory first - $ cd /home/user/Documents - # The root directory of the archive will be "projectA" - $ borg create /path/to/repo::daily-projectA-{now:%Y-%m-%d} projectA - - -.. include:: usage/extract.rst.inc - -Examples -~~~~~~~~ -:: - - # Extract entire archive - $ borg extract /path/to/repo::my-files - - # Extract entire archive and list files while processing - $ borg extract --list /path/to/repo::my-files - - # Verify whether an archive could be successfully extracted, but do not write files to disk - $ borg extract --dry-run /path/to/repo::my-files - - # Extract the "src" directory - $ borg extract /path/to/repo::my-files home/USERNAME/src - - # Extract the "src" directory but exclude object files - $ borg extract /path/to/repo::my-files home/USERNAME/src --exclude '*.o' - - # Restore a raw device (must not be active/in use/mounted at that time) - $ borg extract --stdout /path/to/repo::my-sdx | dd of=/dev/sdx bs=10M - - -.. Note:: - - Currently, extract always writes into the current working directory ("."), - so make sure you ``cd`` to the right place before calling ``borg extract``. - -.. include:: usage/check.rst.inc - -.. include:: usage/rename.rst.inc - -Examples -~~~~~~~~ -:: - - $ borg create /path/to/repo::archivename ~ - $ borg list /path/to/repo - archivename Mon, 2016-02-15 19:50:19 - - $ borg rename /path/to/repo::archivename newname - $ borg list /path/to/repo - newname Mon, 2016-02-15 19:50:19 - - -.. include:: usage/list.rst.inc - -Examples -~~~~~~~~ -:: - - $ borg list /path/to/repo - Monday Mon, 2016-02-15 19:15:11 - repo Mon, 2016-02-15 19:26:54 - root-2016-02-15 Mon, 2016-02-15 19:36:29 - newname Mon, 2016-02-15 19:50:19 - ... - - $ borg list /path/to/repo::root-2016-02-15 - drwxr-xr-x root root 0 Mon, 2016-02-15 17:44:27 . - drwxrwxr-x root root 0 Mon, 2016-02-15 19:04:49 bin - -rwxr-xr-x root root 1029624 Thu, 2014-11-13 00:08:51 bin/bash - lrwxrwxrwx root root 0 Fri, 2015-03-27 20:24:26 bin/bzcmp -> bzdiff - -rwxr-xr-x root root 2140 Fri, 2015-03-27 20:24:22 bin/bzdiff - ... - - $ borg list /path/to/repo::archiveA --list-format="{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}" - drwxrwxr-x user user 0 Sun, 2015-02-01 11:00:00 . - drwxrwxr-x user user 0 Sun, 2015-02-01 11:00:00 code - drwxrwxr-x user user 0 Sun, 2015-02-01 11:00:00 code/myproject - -rw-rw-r-- user user 1416192 Sun, 2015-02-01 11:00:00 code/myproject/file.ext - ... - - -.. include:: usage/diff.rst.inc - -Examples -~~~~~~~~ -:: - - $ borg init -e=none testrepo - $ mkdir testdir - $ cd testdir - $ echo asdf > file1 - $ dd if=/dev/urandom bs=1M count=4 > file2 - $ touch file3 - $ borg create ../testrepo::archive1 . - - $ chmod a+x file1 - $ echo "something" >> file2 - $ borg create ../testrepo::archive2 . - - $ rm file3 - $ touch file4 - $ borg create ../testrepo::archive3 . - - $ cd .. - $ borg diff testrepo::archive1 archive2 - [-rw-r--r-- -> -rwxr-xr-x] file1 - +135 B -252 B file2 - - $ borg diff testrepo::archive2 archive3 - added 0 B file4 - removed 0 B file3 - - $ borg diff testrepo::archive1 archive3 - [-rw-r--r-- -> -rwxr-xr-x] file1 - +135 B -252 B file2 - added 0 B file4 - removed 0 B file3 - -.. include:: usage/delete.rst.inc - -Examples -~~~~~~~~ -:: - - # delete a single backup archive: - $ borg delete /path/to/repo::Monday - - # delete the whole repository and the related local cache: - $ borg delete /path/to/repo - You requested to completely DELETE the repository *including* all archives it contains: - repo Mon, 2016-02-15 19:26:54 - root-2016-02-15 Mon, 2016-02-15 19:36:29 - newname Mon, 2016-02-15 19:50:19 - Type 'YES' if you understand this and want to continue: YES - - -.. include:: usage/prune.rst.inc - -Examples -~~~~~~~~ - -Be careful, prune is a potentially dangerous command, it will remove backup -archives. - -The default of prune is to apply to **all archives in the repository** unless -you restrict its operation to a subset of the archives using ``--prefix``. -When using ``--prefix``, be careful to choose a good prefix - e.g. do not use a -prefix "foo" if you do not also want to match "foobar". - -It is strongly recommended to always run ``prune -v --list --dry-run ...`` -first so you will see what it would do without it actually doing anything. - -There is also a visualized prune example in ``docs/misc/prune-example.txt``. - -:: - - # Keep 7 end of day and 4 additional end of week archives. - # Do a dry-run without actually deleting anything. - $ borg prune -v --list --dry-run --keep-daily=7 --keep-weekly=4 /path/to/repo - - # Same as above but only apply to archive names starting with the hostname - # of the machine followed by a "-" character: - $ borg prune -v --list --keep-daily=7 --keep-weekly=4 --prefix='{hostname}-' /path/to/repo - - # Keep 7 end of day, 4 additional end of week archives, - # and an end of month archive for every month: - $ borg prune -v --list --keep-daily=7 --keep-weekly=4 --keep-monthly=-1 /path/to/repo - - # Keep all backups in the last 10 days, 4 additional end of week archives, - # and an end of month archive for every month: - $ borg prune -v --list --keep-within=10d --keep-weekly=4 --keep-monthly=-1 /path/to/repo - - -.. include:: usage/info.rst.inc - -Examples -~~~~~~~~ -:: - - $ borg info /path/to/repo::root-2016-02-15 - Name: root-2016-02-15 - Fingerprint: 57c827621f21b000a8d363c1e163cc55983822b3afff3a96df595077a660be50 - Hostname: myhostname - Username: root - Time (start): Mon, 2016-02-15 19:36:29 - Time (end): Mon, 2016-02-15 19:39:26 - Command line: /usr/local/bin/borg create --list -C zlib,6 /path/to/repo::root-2016-02-15 / --one-file-system - Number of files: 38100 - - Original size Compressed size Deduplicated size - This archive: 1.33 GB 613.25 MB 571.64 MB - All archives: 1.63 GB 853.66 MB 584.12 MB - - Unique chunks Total chunks - Chunk index: 36858 48844 - - -.. include:: usage/mount.rst.inc - -.. include:: usage/umount.rst.inc - -Examples -~~~~~~~~ - -borg mount -++++++++++ -:: - - $ borg mount /path/to/repo::root-2016-02-15 /tmp/mymountpoint - $ ls /tmp/mymountpoint - bin boot etc home lib lib64 lost+found media mnt opt root sbin srv tmp usr var - $ borg umount /tmp/mymountpoint - -:: - - $ borg mount -o versions /path/to/repo /tmp/mymountpoint - $ ls -l /tmp/mymountpoint/home/user/doc.txt/ - total 24 - -rw-rw-r-- 1 user group 12357 Aug 26 21:19 doc.txt.cda00bc9 - -rw-rw-r-- 1 user group 12204 Aug 26 21:04 doc.txt.fa760f28 - $ fusermount -u /tmp/mymountpoint - -borgfs -++++++ -:: - - $ echo '/mnt/backup /tmp/myrepo fuse.borgfs defaults,noauto 0 0' >> /etc/fstab - $ echo '/mnt/backup::root-2016-02-15 /tmp/myarchive fuse.borgfs defaults,noauto 0 0' >> /etc/fstab - $ mount /tmp/myrepo - $ mount /tmp/myarchive - $ ls /tmp/myrepo - root-2016-02-01 root-2016-02-2015 - $ ls /tmp/myarchive - bin boot etc home lib lib64 lost+found media mnt opt root sbin srv tmp usr var - -.. Note:: - - ``borgfs`` will be automatically provided if you used a distribution - package, ``pip`` or ``setup.py`` to install |project_name|. Users of the - standalone binary will have to manually create a symlink (see - :ref:`pyinstaller-binary`). - -.. include:: usage/key_export.rst.inc - - -.. include:: usage/key_import.rst.inc - -.. _borg-change-passphrase: - -.. include:: usage/key_change-passphrase.rst.inc - -Examples -~~~~~~~~ -:: - - # Create a key file protected repository - $ borg init --encryption=keyfile -v /path/to/repo - Initializing repository at "/path/to/repo" - Enter new passphrase: - Enter same passphrase again: - Remember your passphrase. Your data will be inaccessible without it. - Key in "/root/.config/borg/keys/mnt_backup" created. - Keep this key safe. Your data will be inaccessible without it. - Synchronizing chunks cache... - Archives: 0, w/ cached Idx: 0, w/ outdated Idx: 0, w/o cached Idx: 0. - Done. - - # Change key file passphrase - $ borg key change-passphrase -v /path/to/repo - Enter passphrase for key /root/.config/borg/keys/mnt_backup: - Enter new passphrase: - Enter same passphrase again: - Remember your passphrase. Your data will be inaccessible without it. - Key updated - -Fully automated using environment variables: - -:: - - $ BORG_NEW_PASSPHRASE=old borg init -e=repokey repo - # now "old" is the current passphrase. - $ BORG_PASSPHRASE=old BORG_NEW_PASSPHRASE=new borg key change-passphrase repo - # now "new" is the current passphrase. - - -.. include:: usage/serve.rst.inc - -Examples -~~~~~~~~ - -borg serve has special support for ssh forced commands (see ``authorized_keys`` -example below): it will detect that you use such a forced command and extract -the value of the ``--restrict-to-path`` option(s). - -It will then parse the original command that came from the client, makes sure -that it is also ``borg serve`` and enforce path restriction(s) as given by the -forced command. That way, other options given by the client (like ``--info`` or -``--umask``) are preserved (and are not fixed by the forced command). - -Environment variables (such as BORG_HOSTNAME_IS_UNIQUE) contained in the original -command sent by the client are *not* interpreted, but ignored. If BORG_XXX environment -variables should be set on the ``borg serve`` side, then these must be set in system-specific -locations like ``/etc/environment`` or in the forced command itself (example below). - -:: - - # Allow an SSH keypair to only run borg, and only have access to /path/to/repo. - # Use key options to disable unneeded and potentially dangerous SSH functionality. - # This will help to secure an automated remote backup system. - $ cat ~/.ssh/authorized_keys - command="borg serve --restrict-to-path /path/to/repo",no-pty,no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-user-rc ssh-rsa AAAAB3[...] - - # Set a BORG_XXX environment variable on the "borg serve" side - $ cat ~/.ssh/authorized_keys - command="export BORG_XXX=value; borg serve [...]",restrict ssh-rsa [...] - -.. include:: usage/upgrade.rst.inc - -Examples -~~~~~~~~ -:: - - # Upgrade the borg repository to the most recent version. - $ borg upgrade -v /path/to/repo - making a hardlink copy in /path/to/repo.upgrade-2016-02-15-20:51:55 - opening attic repository with borg and converting - no key file found for repository - converting repo index /path/to/repo/index.0 - converting 1 segments... - converting borg 0.xx to borg current - no key file found for repository - -.. _borg_key_migrate-to-repokey: - -Upgrading a passphrase encrypted attic repo -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -attic offered a "passphrase" encryption mode, but this was removed in borg 1.0 -and replaced by the "repokey" mode (which stores the passphrase-protected -encryption key into the repository config). - -Thus, to upgrade a "passphrase" attic repo to a "repokey" borg repo, 2 steps -are needed, in this order: - -- borg upgrade repo -- borg key migrate-to-repokey repo - - -.. include:: usage/recreate.rst.inc - -Examples -~~~~~~~~ -:: - - # Make old (Attic / Borg 0.xx) archives deduplicate with Borg 1.x archives - # Archives created with Borg 1.1+ and the default chunker params are skipped (archive ID stays the same) - $ borg recreate /mnt/backup --chunker-params default --progress - - # Create a backup with little but fast compression - $ borg create /mnt/backup::archive /some/files --compression lz4 - # Then compress it - this might take longer, but the backup has already completed, so no inconsistencies - # from a long-running backup job. - $ borg recreate /mnt/backup::archive --recompress --compression zlib,9 - - # Remove unwanted files from all archives in a repository - $ borg recreate /mnt/backup -e /home/icke/Pictures/drunk_photos - - - # Change archive comment - $ borg create --comment "This is a comment" /mnt/backup::archivename ~ - $ borg info /mnt/backup::archivename - Name: archivename - Fingerprint: ... - Comment: This is a comment - ... - $ borg recreate --comment "This is a better comment" /mnt/backup::archivename - $ borg info /mnt/backup::archivename - Name: archivename - Fingerprint: ... - Comment: This is a better comment - ... - -.. include:: usage/export-tar.rst.inc - -Examples -~~~~~~~~ -:: - - # export as uncompressed tar - $ borg export-tar /path/to/repo::Monday Monday.tar - - # exclude some types, compress using gzip - $ borg export-tar /path/to/repo::Monday Monday.tar.gz --exclude '*.so' - - # use higher compression level with gzip - $ borg export-tar testrepo::linux --tar-filter="gzip -9" Monday.tar.gz - - # export a gzipped tar, but instead of storing it on disk, - # upload it to a remote site using curl. - $ borg export-tar ... --tar-filter="gzip" - | curl --data-binary @- https://somewhere/to/POST - -.. include:: usage/with-lock.rst.inc - - -.. include:: usage/break-lock.rst.inc - - -Miscellaneous Help ------------------- - -.. include:: usage/help.rst.inc - - -Debugging Facilities --------------------- - -There is a ``borg debug`` command that has some subcommands which are all -**not intended for normal use** and **potentially very dangerous** if used incorrectly. - -For example, ``borg debug put-obj`` and ``borg debug delete-obj`` will only do -what their name suggests: put objects into repo / delete objects from repo. - -Please note: - -- they will not update the chunks cache (chunks index) about the object -- they will not update the manifest (so no automatic chunks index resync is triggered) -- they will not check whether the object is in use (e.g. before delete-obj) -- they will not update any metadata which may point to the object - -They exist to improve debugging capabilities without direct system access, e.g. -in case you ever run into some severe malfunction. Use them only if you know -what you are doing or if a trusted |project_name| developer tells you what to do. - -Borg has a ``--debug-topic TOPIC`` option to enable specific debugging messages. Topics -are generally not documented. - -A ``--debug-profile FILE`` option exists which writes a profile of the main program's -execution to a file. The format of these files is not directly compatible with the -Python profiling tools, since these use the "marshal" format, which is not intended -to be secure (quoting the Python docs: "Never unmarshal data received from an untrusted -or unauthenticated source."). - -The ``borg debug profile-convert`` command can be used to take a Borg profile and convert -it to a profile file that is compatible with the Python tools. - -Additionally, if the filename specified for ``--debug-profile`` ends with ".pyprof" a -Python compatible profile is generated. This is only intended for local use by developers. - -Additional Notes ----------------- - -Here are misc. notes about topics that are maybe not covered in enough detail in the usage section. - -.. _chunker-params: - ---chunker-params -~~~~~~~~~~~~~~~~ - -The chunker params influence how input files are cut into pieces (chunks) -which are then considered for deduplication. They also have a big impact on -resource usage (RAM and disk space) as the amount of resources needed is -(also) determined by the total amount of chunks in the repository (see -`Indexes / Caches memory usage` for details). - -``--chunker-params=10,23,16,4095`` results in a fine-grained deduplication -and creates a big amount of chunks and thus uses a lot of resources to manage -them. This is good for relatively small data volumes and if the machine has a -good amount of free RAM and disk space. - -``--chunker-params=19,23,21,4095`` (default) results in a coarse-grained -deduplication and creates a much smaller amount of chunks and thus uses less -resources. This is good for relatively big data volumes and if the machine has -a relatively low amount of free RAM and disk space. - -If you already have made some archives in a repository and you then change -chunker params, this of course impacts deduplication as the chunks will be -cut differently. - -In the worst case (all files are big and were touched in between backups), this -will store all content into the repository again. - -Usually, it is not that bad though: - -- usually most files are not touched, so it will just re-use the old chunks - it already has in the repo -- files smaller than the (both old and new) minimum chunksize result in only - one chunk anyway, so the resulting chunks are same and deduplication will apply - -If you switch chunker params to save resources for an existing repo that -already has some backup archives, you will see an increasing effect over time, -when more and more files have been touched and stored again using the bigger -chunksize **and** all references to the smaller older chunks have been removed -(by deleting / pruning archives). - -If you want to see an immediate big effect on resource usage, you better start -a new repository when changing chunker params. - -For more details, see :ref:`chunker_details`. - - ---umask -~~~~~~~ - -If you use ``--umask``, make sure that all repository-modifying borg commands -(create, delete, prune) that access the repository in question use the same -``--umask`` value. - -If multiple machines access the same repository, this should hold true for all -of them. - ---read-special -~~~~~~~~~~~~~~ - -The --read-special option is special - you do not want to use it for normal -full-filesystem backups, but rather after carefully picking some targets for it. - -The option ``--read-special`` triggers special treatment for block and char -device files as well as FIFOs. Instead of storing them as such a device (or -FIFO), they will get opened, their content will be read and in the backup -archive they will show up like a regular file. - -Symlinks will also get special treatment if (and only if) they point to such -a special file: instead of storing them as a symlink, the target special file -will get processed as described above. - -One intended use case of this is backing up the contents of one or multiple -block devices, like e.g. LVM snapshots or inactive LVs or disk partitions. - -You need to be careful about what you include when using ``--read-special``, -e.g. if you include ``/dev/zero``, your backup will never terminate. - -Restoring such files' content is currently only supported one at a time via -``--stdout`` option (and you have to redirect stdout to where ever it shall go, -maybe directly into an existing device file of your choice or indirectly via -``dd``). - -To some extent, mounting a backup archive with the backups of special files -via ``borg mount`` and then loop-mounting the image files from inside the mount -point will work. If you plan to access a lot of data in there, it likely will -scale and perform better if you do not work via the FUSE mount. - -Example -+++++++ - -Imagine you have made some snapshots of logical volumes (LVs) you want to backup. - -.. note:: - - For some scenarios, this is a good method to get "crash-like" consistency - (I call it crash-like because it is the same as you would get if you just - hit the reset button or your machine would abrubtly and completely crash). - This is better than no consistency at all and a good method for some use - cases, but likely not good enough if you have databases running. - -Then you create a backup archive of all these snapshots. The backup process will -see a "frozen" state of the logical volumes, while the processes working in the -original volumes continue changing the data stored there. - -You also add the output of ``lvdisplay`` to your backup, so you can see the LV -sizes in case you ever need to recreate and restore them. - -After the backup has completed, you remove the snapshots again. :: - - $ # create snapshots here - $ lvdisplay > lvdisplay.txt - $ borg create --read-special /path/to/repo::arch lvdisplay.txt /dev/vg0/*-snapshot - $ # remove snapshots here - -Now, let's see how to restore some LVs from such a backup. :: - - $ borg extract /path/to/repo::arch lvdisplay.txt - $ # create empty LVs with correct sizes here (look into lvdisplay.txt). - $ # we assume that you created an empty root and home LV and overwrite it now: - $ borg extract --stdout /path/to/repo::arch dev/vg0/root-snapshot > /dev/vg0/root - $ borg extract --stdout /path/to/repo::arch dev/vg0/home-snapshot > /dev/vg0/home - - -.. _append_only_mode: - -Append-only mode -~~~~~~~~~~~~~~~~ - -A repository can be made "append-only", which means that Borg will never overwrite or -delete committed data (append-only refers to the segment files, but borg will also -reject to delete the repository completely). This is useful for scenarios where a -backup client machine backups remotely to a backup server using ``borg serve``, since -a hacked client machine cannot delete backups on the server permanently. - -To activate append-only mode, edit the repository ``config`` file and add a line -``append_only=1`` to the ``[repository]`` section (or edit the line if it exists). - -In append-only mode Borg will create a transaction log in the ``transactions`` file, -where each line is a transaction and a UTC timestamp. - -In addition, ``borg serve`` can act as if a repository is in append-only mode with -its option ``--append-only``. This can be very useful for fine-tuning access control -in ``.ssh/authorized_keys`` :: - - command="borg serve --append-only ..." ssh-rsa - command="borg serve ..." ssh-rsa - -Running ``borg init`` via a ``borg serve --append-only`` server will *not* create -an append-only repository. Running ``borg init --append-only`` creates an append-only -repository regardless of server settings. - -Example -+++++++ - -Suppose an attacker remotely deleted all backups, but your repository was in append-only -mode. A transaction log in this situation might look like this: :: - - transaction 1, UTC time 2016-03-31T15:53:27.383532 - transaction 5, UTC time 2016-03-31T15:53:52.588922 - transaction 11, UTC time 2016-03-31T15:54:23.887256 - transaction 12, UTC time 2016-03-31T15:55:54.022540 - transaction 13, UTC time 2016-03-31T15:55:55.472564 - -From your security logs you conclude the attacker gained access at 15:54:00 and all -the backups where deleted or replaced by compromised backups. From the log you know -that transactions 11 and later are compromised. Note that the transaction ID is the -name of the *last* file in the transaction. For example, transaction 11 spans files 6 -to 11. - -In a real attack you'll likely want to keep the compromised repository -intact to analyze what the attacker tried to achieve. It's also a good idea to make this -copy just in case something goes wrong during the recovery. Since recovery is done by -deleting some files, a hard link copy (``cp -al``) is sufficient. - -The first step to reset the repository to transaction 5, the last uncompromised transaction, -is to remove the ``hints.N`` and ``index.N`` files in the repository (these two files are -always expendable). In this example N is 13. - -Then remove or move all segment files from the segment directories in ``data/`` starting -with file 6:: - - rm data/**/{6..13} - -That's all to it. - -Drawbacks -+++++++++ - -As data is only appended, and nothing removed, commands like ``prune`` or ``delete`` -won't free disk space, they merely tag data as deleted in a new transaction. - -Be aware that as soon as you write to the repo in non-append-only mode (e.g. prune, -delete or create archives from an admin machine), it will remove the deleted objects -permanently (including the ones that were already marked as deleted, but not removed, -in append-only mode). - -Note that you can go back-and-forth between normal and append-only operation by editing -the configuration file, it's not a "one way trip". - -Further considerations -++++++++++++++++++++++ - -Append-only mode is not respected by tools other than Borg. ``rm`` still works on the -repository. Make sure that backup client machines only get to access the repository via -``borg serve``. - -Ensure that no remote access is possible if the repository is temporarily set to normal mode -for e.g. regular pruning. - -Further protections can be implemented, but are outside of Borg's scope. For example, -file system snapshots or wrapping ``borg serve`` to set special permissions or ACLs on -new data files. - -SSH batch mode -~~~~~~~~~~~~~~ - -When running |project_name| using an automated script, ``ssh`` might still ask for a password, -even if there is an SSH key for the target server. Use this to make scripts more robust:: - - export BORG_RSH='ssh -oBatchMode=yes' +.. raw:: html + Redirecting... + + + +.. toctree:: + :hidden: + + usage/general + + usage/init + usage/create + usage/extract + usage/check + usage/rename + usage/list + usage/diff + usage/delete + usage/prune + usage/info + usage/mount + usage/key + usage/upgrade + usage/recreate + usage/tar + usage/serve + usage/lock + + usage/help + usage/debug + usage/notes diff --git a/docs/usage/benchmark_crud.rst.inc b/docs/usage/benchmark_crud.rst.inc index d47e8d62..6f2d78c8 100644 --- a/docs/usage/benchmark_crud.rst.inc +++ b/docs/usage/benchmark_crud.rst.inc @@ -14,7 +14,7 @@ positional arguments PATH path were to create benchmark input data -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/break-lock.rst.inc b/docs/usage/break-lock.rst.inc index 1b8e5915..f0bb1eac 100644 --- a/docs/usage/break-lock.rst.inc +++ b/docs/usage/break-lock.rst.inc @@ -12,7 +12,7 @@ positional arguments REPOSITORY repository for which to break the locks -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/change-passphrase.rst.inc b/docs/usage/change-passphrase.rst.inc index b0a6c2bb..b7addf3d 100644 --- a/docs/usage/change-passphrase.rst.inc +++ b/docs/usage/change-passphrase.rst.inc @@ -12,7 +12,7 @@ positional arguments REPOSITORY -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/check.rst b/docs/usage/check.rst new file mode 100644 index 00000000..143b0b4c --- /dev/null +++ b/docs/usage/check.rst @@ -0,0 +1 @@ +.. include:: check.rst.inc diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index 56bc42c8..08aef086 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -24,7 +24,7 @@ optional arguments ``--save-space`` | work slower, but using less space -`Common options`_ +:ref:`common_options` | filters diff --git a/docs/usage/create.rst b/docs/usage/create.rst new file mode 100644 index 00000000..71215735 --- /dev/null +++ b/docs/usage/create.rst @@ -0,0 +1,70 @@ +.. include:: create.rst.inc + +Examples +~~~~~~~~ +:: + + # Backup ~/Documents into an archive named "my-documents" + $ borg create /path/to/repo::my-documents ~/Documents + + # same, but list all files as we process them + $ borg create --list /path/to/repo::my-documents ~/Documents + + # Backup ~/Documents and ~/src but exclude pyc files + $ borg create /path/to/repo::my-files \ + ~/Documents \ + ~/src \ + --exclude '*.pyc' + + # Backup home directories excluding image thumbnails (i.e. only + # /home/*/.thumbnails is excluded, not /home/*/*/.thumbnails) + $ borg create /path/to/repo::my-files /home \ + --exclude 're:^/home/[^/]+/\.thumbnails/' + + # Do the same using a shell-style pattern + $ borg create /path/to/repo::my-files /home \ + --exclude 'sh:/home/*/.thumbnails' + + # Backup the root filesystem into an archive named "root-YYYY-MM-DD" + # use zlib compression (good, but slow) - default is lz4 (fast, low compression ratio) + $ borg create -C zlib,6 /path/to/repo::root-{now:%Y-%m-%d} / --one-file-system + + # Backup a remote host locally ("pull" style) using sshfs + $ mkdir sshfs-mount + $ sshfs root@example.com:/ sshfs-mount + $ cd sshfs-mount + $ borg create /path/to/repo::example.com-root-{now:%Y-%m-%d} . + $ cd .. + $ fusermount -u sshfs-mount + + # Make a big effort in fine granular deduplication (big chunk management + # overhead, needs a lot of RAM and disk space, see formula in internals + # docs - same parameters as borg < 1.0 or attic): + $ borg create --chunker-params 10,23,16,4095 /path/to/repo::small /smallstuff + + # Backup a raw device (must not be active/in use/mounted at that time) + $ dd if=/dev/sdx bs=10M | borg create /path/to/repo::my-sdx - + + # No compression (default) + $ borg create /path/to/repo::arch ~ + + # Super fast, low compression + $ borg create --compression lz4 /path/to/repo::arch ~ + + # Less fast, higher compression (N = 0..9) + $ borg create --compression zlib,N /path/to/repo::arch ~ + + # Even slower, even higher compression (N = 0..9) + $ borg create --compression lzma,N /path/to/repo::arch ~ + + # Use short hostname, user name and current time in archive name + $ borg create /path/to/repo::{hostname}-{user}-{now} ~ + # Similar, use the same datetime format as borg 1.1 will have as default + $ borg create /path/to/repo::{hostname}-{user}-{now:%Y-%m-%dT%H:%M:%S} ~ + # As above, but add nanoseconds + $ borg create /path/to/repo::{hostname}-{user}-{now:%Y-%m-%dT%H:%M:%S.%f} ~ + + # Backing up relative paths by moving into the correct directory first + $ cd /home/user/Documents + # The root directory of the archive will be "projectA" + $ borg create /path/to/repo::daily-projectA-{now:%Y-%m-%d} projectA \ No newline at end of file diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 6b7006cb..dc6dc7f6 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -26,7 +26,7 @@ optional arguments ``--json`` | output stats as JSON (implies --stats) -`Common options`_ +:ref:`common_options` | Exclusion options diff --git a/docs/usage/debug.rst b/docs/usage/debug.rst new file mode 100644 index 00000000..0dc54ae3 --- /dev/null +++ b/docs/usage/debug.rst @@ -0,0 +1,34 @@ +Debugging Facilities +-------------------- + +There is a ``borg debug`` command that has some subcommands which are all +**not intended for normal use** and **potentially very dangerous** if used incorrectly. + +For example, ``borg debug put-obj`` and ``borg debug delete-obj`` will only do +what their name suggests: put objects into repo / delete objects from repo. + +Please note: + +- they will not update the chunks cache (chunks index) about the object +- they will not update the manifest (so no automatic chunks index resync is triggered) +- they will not check whether the object is in use (e.g. before delete-obj) +- they will not update any metadata which may point to the object + +They exist to improve debugging capabilities without direct system access, e.g. +in case you ever run into some severe malfunction. Use them only if you know +what you are doing or if a trusted Borg developer tells you what to do. + +Borg has a ``--debug-topic TOPIC`` option to enable specific debugging messages. Topics +are generally not documented. + +A ``--debug-profile FILE`` option exists which writes a profile of the main program's +execution to a file. The format of these files is not directly compatible with the +Python profiling tools, since these use the "marshal" format, which is not intended +to be secure (quoting the Python docs: "Never unmarshal data received from an untrusted +or unauthenticated source."). + +The ``borg debug profile-convert`` command can be used to take a Borg profile and convert +it to a profile file that is compatible with the Python tools. + +Additionally, if the filename specified for ``--debug-profile`` ends with ".pyprof" a +Python compatible profile is generated. This is only intended for local use by developers. diff --git a/docs/usage/delete.rst b/docs/usage/delete.rst new file mode 100644 index 00000000..a5017681 --- /dev/null +++ b/docs/usage/delete.rst @@ -0,0 +1,16 @@ +.. include:: delete.rst.inc + +Examples +~~~~~~~~ +:: + + # delete a single backup archive: + $ borg delete /path/to/repo::Monday + + # delete the whole repository and the related local cache: + $ borg delete /path/to/repo + You requested to completely DELETE the repository *including* all archives it contains: + repo Mon, 2016-02-15 19:26:54 + root-2016-02-15 Mon, 2016-02-15 19:36:29 + newname Mon, 2016-02-15 19:50:19 + Type 'YES' if you understand this and want to continue: YES diff --git a/docs/usage/delete.rst.inc b/docs/usage/delete.rst.inc index 0977f022..ca8f12b3 100644 --- a/docs/usage/delete.rst.inc +++ b/docs/usage/delete.rst.inc @@ -22,7 +22,7 @@ optional arguments ``--save-space`` | work slower, but using less space -`Common options`_ +:ref:`common_options` | filters diff --git a/docs/usage/diff.rst b/docs/usage/diff.rst new file mode 100644 index 00000000..e2972443 --- /dev/null +++ b/docs/usage/diff.rst @@ -0,0 +1,36 @@ +.. include:: diff.rst.inc + +Examples +~~~~~~~~ +:: + + $ borg init -e=none testrepo + $ mkdir testdir + $ cd testdir + $ echo asdf > file1 + $ dd if=/dev/urandom bs=1M count=4 > file2 + $ touch file3 + $ borg create ../testrepo::archive1 . + + $ chmod a+x file1 + $ echo "something" >> file2 + $ borg create ../testrepo::archive2 . + + $ rm file3 + $ touch file4 + $ borg create ../testrepo::archive3 . + + $ cd .. + $ borg diff testrepo::archive1 archive2 + [-rw-r--r-- -> -rwxr-xr-x] file1 + +135 B -252 B file2 + + $ borg diff testrepo::archive2 archive3 + added 0 B file4 + removed 0 B file3 + + $ borg diff testrepo::archive1 archive3 + [-rw-r--r-- -> -rwxr-xr-x] file1 + +135 B -252 B file2 + added 0 B file4 + removed 0 B file3 diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index 0163c5dc..fe57eb05 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -24,7 +24,7 @@ optional arguments ``--sort`` | Sort the output lines by file path. -`Common options`_ +:ref:`common_options` | Exclusion options diff --git a/docs/usage/export-tar.rst.inc b/docs/usage/export-tar.rst.inc index f2c4e03a..cb81f9b2 100644 --- a/docs/usage/export-tar.rst.inc +++ b/docs/usage/export-tar.rst.inc @@ -32,7 +32,7 @@ optional arguments ``--strip-components NUMBER`` | Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/extract.rst b/docs/usage/extract.rst new file mode 100644 index 00000000..62cbbdc6 --- /dev/null +++ b/docs/usage/extract.rst @@ -0,0 +1,29 @@ +.. include:: extract.rst.inc + +Examples +~~~~~~~~ +:: + + # Extract entire archive + $ borg extract /path/to/repo::my-files + + # Extract entire archive and list files while processing + $ borg extract --list /path/to/repo::my-files + + # Verify whether an archive could be successfully extracted, but do not write files to disk + $ borg extract --dry-run /path/to/repo::my-files + + # Extract the "src" directory + $ borg extract /path/to/repo::my-files home/USERNAME/src + + # Extract the "src" directory but exclude object files + $ borg extract /path/to/repo::my-files home/USERNAME/src --exclude '*.o' + + # Restore a raw device (must not be active/in use/mounted at that time) + $ borg extract --stdout /path/to/repo::my-sdx | dd of=/dev/sdx bs=10M + + +.. Note:: + + Currently, extract always writes into the current working directory ("."), + so make sure you ``cd`` to the right place before calling ``borg extract``. \ No newline at end of file diff --git a/docs/usage/extract.rst.inc b/docs/usage/extract.rst.inc index f5b2d494..72fc0356 100644 --- a/docs/usage/extract.rst.inc +++ b/docs/usage/extract.rst.inc @@ -36,7 +36,7 @@ optional arguments ``--sparse`` | create holes in output sparse file from all-zero chunks -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/general.rst b/docs/usage/general.rst new file mode 100644 index 00000000..6b9ecd0a --- /dev/null +++ b/docs/usage/general.rst @@ -0,0 +1,21 @@ +General +------- + +Borg consists of a number of commands. Each command accepts +a number of arguments and options and interprets various environment variables. +The following sections will describe each command in detail. + +.. include:: ../usage_general.rst.inc + +In case you are interested in more details (like formulas), please see +:ref:`internals`. For details on the available JSON output, refer to +:ref:`json_output`. + +.. _common_options: + +Common options +~~~~~~~~~~~~~~ + +All Borg commands share these options: + +.. include:: common-options.rst.inc diff --git a/docs/usage/help.rst b/docs/usage/help.rst new file mode 100644 index 00000000..a23f0420 --- /dev/null +++ b/docs/usage/help.rst @@ -0,0 +1,4 @@ +Miscellaneous Help +------------------ + +.. include:: help.rst.inc diff --git a/docs/usage/info.rst b/docs/usage/info.rst new file mode 100644 index 00000000..df770ffc --- /dev/null +++ b/docs/usage/info.rst @@ -0,0 +1,23 @@ +.. include:: info.rst.inc + +Examples +~~~~~~~~ +:: + + $ borg info /path/to/repo::root-2016-02-15 + Name: root-2016-02-15 + Fingerprint: 57c827621f21b000a8d363c1e163cc55983822b3afff3a96df595077a660be50 + Hostname: myhostname + Username: root + Time (start): Mon, 2016-02-15 19:36:29 + Time (end): Mon, 2016-02-15 19:39:26 + Command line: /usr/local/bin/borg create --list -C zlib,6 /path/to/repo::root-2016-02-15 / --one-file-system + Number of files: 38100 + + Original size Compressed size Deduplicated size + This archive: 1.33 GB 613.25 MB 571.64 MB + All archives: 1.63 GB 853.66 MB 584.12 MB + + Unique chunks Total chunks + Chunk index: 36858 48844 + diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index 0376329a..a5fc65fe 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -16,7 +16,7 @@ optional arguments ``--json`` | format output as JSON -`Common options`_ +:ref:`common_options` | filters diff --git a/docs/usage/init.rst b/docs/usage/init.rst new file mode 100644 index 00000000..46355b0d --- /dev/null +++ b/docs/usage/init.rst @@ -0,0 +1,17 @@ +.. include:: init.rst.inc + +Examples +~~~~~~~~ +:: + + # Local repository, repokey encryption, BLAKE2b (often faster, since Borg 1.1) + $ borg init --encryption=repokey-blake2 /path/to/repo + + # Local repository (no encryption) + $ borg init --encryption=none /path/to/repo + + # Remote repository (accesses a remote borg via ssh) + $ borg init --encryption=repokey-blake2 user@hostname:backup + + # Remote repository (store the key your home dir) + $ borg init --encryption=keyfile user@hostname:backup \ No newline at end of file diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index 36262431..a1fe3815 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -20,7 +20,7 @@ optional arguments ``--storage-quota`` | Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota. -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/key.rst b/docs/usage/key.rst new file mode 100644 index 00000000..d22db044 --- /dev/null +++ b/docs/usage/key.rst @@ -0,0 +1,42 @@ +.. include:: key_export.rst.inc + + +.. include:: key_import.rst.inc + +.. _borg-change-passphrase: + +.. include:: key_change-passphrase.rst.inc + +Examples +~~~~~~~~ +:: + + # Create a key file protected repository + $ borg init --encryption=keyfile -v /path/to/repo + Initializing repository at "/path/to/repo" + Enter new passphrase: + Enter same passphrase again: + Remember your passphrase. Your data will be inaccessible without it. + Key in "/root/.config/borg/keys/mnt_backup" created. + Keep this key safe. Your data will be inaccessible without it. + Synchronizing chunks cache... + Archives: 0, w/ cached Idx: 0, w/ outdated Idx: 0, w/o cached Idx: 0. + Done. + + # Change key file passphrase + $ borg key change-passphrase -v /path/to/repo + Enter passphrase for key /root/.config/borg/keys/mnt_backup: + Enter new passphrase: + Enter same passphrase again: + Remember your passphrase. Your data will be inaccessible without it. + Key updated + +Fully automated using environment variables: + +:: + + $ BORG_NEW_PASSPHRASE=old borg init -e=repokey repo + # now "old" is the current passphrase. + $ BORG_PASSPHRASE=old BORG_NEW_PASSPHRASE=new borg key change-passphrase repo + # now "new" is the current passphrase. + diff --git a/docs/usage/key_change-passphrase.rst.inc b/docs/usage/key_change-passphrase.rst.inc index 7666afc2..aa0b92b5 100644 --- a/docs/usage/key_change-passphrase.rst.inc +++ b/docs/usage/key_change-passphrase.rst.inc @@ -12,7 +12,7 @@ positional arguments REPOSITORY -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/key_export.rst.inc b/docs/usage/key_export.rst.inc index e976ae2d..2d32b90b 100644 --- a/docs/usage/key_export.rst.inc +++ b/docs/usage/key_export.rst.inc @@ -20,7 +20,7 @@ optional arguments ``--qr-html`` | Create an html file suitable for printing and later type-in or qr scan -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/key_import.rst.inc b/docs/usage/key_import.rst.inc index ceb89e3f..e60f5620 100644 --- a/docs/usage/key_import.rst.inc +++ b/docs/usage/key_import.rst.inc @@ -18,7 +18,7 @@ optional arguments ``--paper`` | interactively import from a backup done with --paper -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/key_migrate-to-repokey.rst.inc b/docs/usage/key_migrate-to-repokey.rst.inc index df242566..9f730137 100644 --- a/docs/usage/key_migrate-to-repokey.rst.inc +++ b/docs/usage/key_migrate-to-repokey.rst.inc @@ -12,7 +12,7 @@ positional arguments REPOSITORY -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/list.rst b/docs/usage/list.rst new file mode 100644 index 00000000..4268bff1 --- /dev/null +++ b/docs/usage/list.rst @@ -0,0 +1,27 @@ +.. include:: list.rst.inc + +Examples +~~~~~~~~ +:: + + $ borg list /path/to/repo + Monday Mon, 2016-02-15 19:15:11 + repo Mon, 2016-02-15 19:26:54 + root-2016-02-15 Mon, 2016-02-15 19:36:29 + newname Mon, 2016-02-15 19:50:19 + ... + + $ borg list /path/to/repo::root-2016-02-15 + drwxr-xr-x root root 0 Mon, 2016-02-15 17:44:27 . + drwxrwxr-x root root 0 Mon, 2016-02-15 19:04:49 bin + -rwxr-xr-x root root 1029624 Thu, 2014-11-13 00:08:51 bin/bash + lrwxrwxrwx root root 0 Fri, 2015-03-27 20:24:26 bin/bzcmp -> bzdiff + -rwxr-xr-x root root 2140 Fri, 2015-03-27 20:24:22 bin/bzdiff + ... + + $ borg list /path/to/repo::archiveA --list-format="{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}" + drwxrwxr-x user user 0 Sun, 2015-02-01 11:00:00 . + drwxrwxr-x user user 0 Sun, 2015-02-01 11:00:00 code + drwxrwxr-x user user 0 Sun, 2015-02-01 11:00:00 code/myproject + -rw-rw-r-- user user 1416192 Sun, 2015-02-01 11:00:00 code/myproject/file.ext + ... diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index cd8db74c..d181721e 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -25,7 +25,7 @@ optional arguments ``--json-lines`` | Only valid for listing archive contents. Format output as JSON Lines. The form of --format is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. -`Common options`_ +:ref:`common_options` | filters diff --git a/docs/usage/lock.rst b/docs/usage/lock.rst new file mode 100644 index 00000000..b4573dcc --- /dev/null +++ b/docs/usage/lock.rst @@ -0,0 +1,3 @@ +.. include:: with-lock.rst.inc + +.. include:: break-lock.rst.inc diff --git a/docs/usage/mount.rst b/docs/usage/mount.rst new file mode 100644 index 00000000..f5f1b4f5 --- /dev/null +++ b/docs/usage/mount.rst @@ -0,0 +1,44 @@ +.. include:: mount.rst.inc + +.. include:: umount.rst.inc + +Examples +~~~~~~~~ + +borg mount +++++++++++ +:: + + $ borg mount /path/to/repo::root-2016-02-15 /tmp/mymountpoint + $ ls /tmp/mymountpoint + bin boot etc home lib lib64 lost+found media mnt opt root sbin srv tmp usr var + $ borg umount /tmp/mymountpoint + +:: + + $ borg mount -o versions /path/to/repo /tmp/mymountpoint + $ ls -l /tmp/mymountpoint/home/user/doc.txt/ + total 24 + -rw-rw-r-- 1 user group 12357 Aug 26 21:19 doc.txt.cda00bc9 + -rw-rw-r-- 1 user group 12204 Aug 26 21:04 doc.txt.fa760f28 + $ fusermount -u /tmp/mymountpoint + +borgfs +++++++ +:: + + $ echo '/mnt/backup /tmp/myrepo fuse.borgfs defaults,noauto 0 0' >> /etc/fstab + $ echo '/mnt/backup::root-2016-02-15 /tmp/myarchive fuse.borgfs defaults,noauto 0 0' >> /etc/fstab + $ mount /tmp/myrepo + $ mount /tmp/myarchive + $ ls /tmp/myrepo + root-2016-02-01 root-2016-02-2015 + $ ls /tmp/myarchive + bin boot etc home lib lib64 lost+found media mnt opt root sbin srv tmp usr var + +.. Note:: + + ``borgfs`` will be automatically provided if you used a distribution + package, ``pip`` or ``setup.py`` to install Borg. Users of the + standalone binary will have to manually create a symlink (see + :ref:`pyinstaller-binary`). diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index 026cc680..57947a0b 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -20,7 +20,7 @@ optional arguments ``-o`` | Extra mount options -`Common options`_ +:ref:`common_options` | filters diff --git a/docs/usage/notes.rst b/docs/usage/notes.rst new file mode 100644 index 00000000..21d3cf4b --- /dev/null +++ b/docs/usage/notes.rst @@ -0,0 +1,226 @@ +Additional Notes +---------------- + +Here are misc. notes about topics that are maybe not covered in enough detail in the usage section. + +.. _chunker-params: + +--chunker-params +~~~~~~~~~~~~~~~~ + +The chunker params influence how input files are cut into pieces (chunks) +which are then considered for deduplication. They also have a big impact on +resource usage (RAM and disk space) as the amount of resources needed is +(also) determined by the total amount of chunks in the repository (see +`Indexes / Caches memory usage` for details). + +``--chunker-params=10,23,16,4095`` results in a fine-grained deduplication| +and creates a big amount of chunks and thus uses a lot of resources to manage +them. This is good for relatively small data volumes and if the machine has a +good amount of free RAM and disk space. + +``--chunker-params=19,23,21,4095`` (default) results in a coarse-grained +deduplication and creates a much smaller amount of chunks and thus uses less +resources. This is good for relatively big data volumes and if the machine has +a relatively low amount of free RAM and disk space. + +If you already have made some archives in a repository and you then change +chunker params, this of course impacts deduplication as the chunks will be +cut differently. + +In the worst case (all files are big and were touched in between backups), this +will store all content into the repository again. + +Usually, it is not that bad though: + +- usually most files are not touched, so it will just re-use the old chunks + it already has in the repo +- files smaller than the (both old and new) minimum chunksize result in only + one chunk anyway, so the resulting chunks are same and deduplication will apply + +If you switch chunker params to save resources for an existing repo that +already has some backup archives, you will see an increasing effect over time, +when more and more files have been touched and stored again using the bigger +chunksize **and** all references to the smaller older chunks have been removed +(by deleting / pruning archives). + +If you want to see an immediate big effect on resource usage, you better start +a new repository when changing chunker params. + +For more details, see :ref:`chunker_details`. + + +--umask +~~~~~~~ + +If you use ``--umask``, make sure that all repository-modifying borg commands +(create, delete, prune) that access the repository in question use the same +``--umask`` value. + +If multiple machines access the same repository, this should hold true for all +of them. + +--read-special +~~~~~~~~~~~~~~ + +The --read-special option is special - you do not want to use it for normal +full-filesystem backups, but rather after carefully picking some targets for it. + +The option ``--read-special`` triggers special treatment for block and char +device files as well as FIFOs. Instead of storing them as such a device (or +FIFO), they will get opened, their content will be read and in the backup +archive they will show up like a regular file. + +Symlinks will also get special treatment if (and only if) they point to such +a special file: instead of storing them as a symlink, the target special file +will get processed as described above. + +One intended use case of this is backing up the contents of one or multiple +block devices, like e.g. LVM snapshots or inactive LVs or disk partitions. + +You need to be careful about what you include when using ``--read-special``, +e.g. if you include ``/dev/zero``, your backup will never terminate. + +Restoring such files' content is currently only supported one at a time via +``--stdout`` option (and you have to redirect stdout to where ever it shall go, +maybe directly into an existing device file of your choice or indirectly via +``dd``). + +To some extent, mounting a backup archive with the backups of special files +via ``borg mount`` and then loop-mounting the image files from inside the mount +point will work. If you plan to access a lot of data in there, it likely will +scale and perform better if you do not work via the FUSE mount. + +Example ++++++++ + +Imagine you have made some snapshots of logical volumes (LVs) you want to backup. + +.. note:: + + For some scenarios, this is a good method to get "crash-like" consistency + (I call it crash-like because it is the same as you would get if you just + hit the reset button or your machine would abrubtly and completely crash). + This is better than no consistency at all and a good method for some use + cases, but likely not good enough if you have databases running. + +Then you create a backup archive of all these snapshots. The backup process will +see a "frozen" state of the logical volumes, while the processes working in the +original volumes continue changing the data stored there. + +You also add the output of ``lvdisplay`` to your backup, so you can see the LV +sizes in case you ever need to recreate and restore them. + +After the backup has completed, you remove the snapshots again. :: + + $ # create snapshots here + $ lvdisplay > lvdisplay.txt + $ borg create --read-special /path/to/repo::arch lvdisplay.txt /dev/vg0/*-snapshot + $ # remove snapshots here + +Now, let's see how to restore some LVs from such a backup. :: + + $ borg extract /path/to/repo::arch lvdisplay.txt + $ # create empty LVs with correct sizes here (look into lvdisplay.txt). + $ # we assume that you created an empty root and home LV and overwrite it now: + $ borg extract --stdout /path/to/repo::arch dev/vg0/root-snapshot > /dev/vg0/root + $ borg extract --stdout /path/to/repo::arch dev/vg0/home-snapshot > /dev/vg0/home + + +.. _append_only_mode: + +Append-only mode +~~~~~~~~~~~~~~~~ + +A repository can be made "append-only", which means that Borg will never overwrite or +delete committed data (append-only refers to the segment files, but borg will also +reject to delete the repository completely). This is useful for scenarios where a +backup client machine backups remotely to a backup server using ``borg serve``, since +a hacked client machine cannot delete backups on the server permanently. + +To activate append-only mode, edit the repository ``config`` file and add a line +``append_only=1`` to the ``[repository]`` section (or edit the line if it exists). + +In append-only mode Borg will create a transaction log in the ``transactions`` file, +where each line is a transaction and a UTC timestamp. + +In addition, ``borg serve`` can act as if a repository is in append-only mode with +its option ``--append-only``. This can be very useful for fine-tuning access control +in ``.ssh/authorized_keys`` :: + + command="borg serve --append-only ..." ssh-rsa + command="borg serve ..." ssh-rsa + +Running ``borg init`` via a ``borg serve --append-only`` server will *not* create +an append-only repository. Running ``borg init --append-only`` creates an append-only +repository regardless of server settings. + +Example ++++++++ + +Suppose an attacker remotely deleted all backups, but your repository was in append-only +mode. A transaction log in this situation might look like this: :: + + transaction 1, UTC time 2016-03-31T15:53:27.383532 + transaction 5, UTC time 2016-03-31T15:53:52.588922 + transaction 11, UTC time 2016-03-31T15:54:23.887256 + transaction 12, UTC time 2016-03-31T15:55:54.022540 + transaction 13, UTC time 2016-03-31T15:55:55.472564 + +From your security logs you conclude the attacker gained access at 15:54:00 and all +the backups where deleted or replaced by compromised backups. From the log you know +that transactions 11 and later are compromised. Note that the transaction ID is the +name of the *last* file in the transaction. For example, transaction 11 spans files 6 +to 11. + +In a real attack you'll likely want to keep the compromised repository +intact to analyze what the attacker tried to achieve. It's also a good idea to make this +copy just in case something goes wrong during the recovery. Since recovery is done by +deleting some files, a hard link copy (``cp -al``) is sufficient. + +The first step to reset the repository to transaction 5, the last uncompromised transaction, +is to remove the ``hints.N`` and ``index.N`` files in the repository (these two files are +always expendable). In this example N is 13. + +Then remove or move all segment files from the segment directories in ``data/`` starting +with file 6:: + + rm data/**/{6..13} + +That's all to it. + +Drawbacks ++++++++++ + +As data is only appended, and nothing removed, commands like ``prune`` or ``delete`` +won't free disk space, they merely tag data as deleted in a new transaction. + +Be aware that as soon as you write to the repo in non-append-only mode (e.g. prune, +delete or create archives from an admin machine), it will remove the deleted objects +permanently (including the ones that were already marked as deleted, but not removed, +in append-only mode). + +Note that you can go back-and-forth between normal and append-only operation by editing +the configuration file, it's not a "one way trip". + +Further considerations +++++++++++++++++++++++ + +Append-only mode is not respected by tools other than Borg. ``rm`` still works on the +repository. Make sure that backup client machines only get to access the repository via +``borg serve``. + +Ensure that no remote access is possible if the repository is temporarily set to normal mode +for e.g. regular pruning. + +Further protections can be implemented, but are outside of Borg's scope. For example, +file system snapshots or wrapping ``borg serve`` to set special permissions or ACLs on +new data files. + +SSH batch mode +~~~~~~~~~~~~~~ + +When running Borg using an automated script, ``ssh`` might still ask for a password, +even if there is an SSH key for the target server. Use this to make scripts more robust:: + + export BORG_RSH='ssh -oBatchMode=yes' diff --git a/docs/usage/prune.rst b/docs/usage/prune.rst new file mode 100644 index 00000000..fb1fe660 --- /dev/null +++ b/docs/usage/prune.rst @@ -0,0 +1,35 @@ +.. include:: prune.rst.inc + +Examples +~~~~~~~~ + +Be careful, prune is a potentially dangerous command, it will remove backup +archives. + +The default of prune is to apply to **all archives in the repository** unless +you restrict its operation to a subset of the archives using ``--prefix``. +When using ``--prefix``, be careful to choose a good prefix - e.g. do not use a +prefix "foo" if you do not also want to match "foobar". + +It is strongly recommended to always run ``prune -v --list --dry-run ...`` +first so you will see what it would do without it actually doing anything. + +There is also a visualized prune example in ``docs/misc/prune-example.txt``. + +:: + + # Keep 7 end of day and 4 additional end of week archives. + # Do a dry-run without actually deleting anything. + $ borg prune -v --list --dry-run --keep-daily=7 --keep-weekly=4 /path/to/repo + + # Same as above but only apply to archive names starting with the hostname + # of the machine followed by a "-" character: + $ borg prune -v --list --keep-daily=7 --keep-weekly=4 --prefix='{hostname}-' /path/to/repo + + # Keep 7 end of day, 4 additional end of week archives, + # and an end of month archive for every month: + $ borg prune -v --list --keep-daily=7 --keep-weekly=4 --keep-monthly=-1 /path/to/repo + + # Keep all backups in the last 10 days, 4 additional end of week archives, + # and an end of month archive for every month: + $ borg prune -v --list --keep-within=10d --keep-weekly=4 --keep-monthly=-1 /path/to/repo diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index 40e0c26c..a1ef2a99 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -42,7 +42,7 @@ optional arguments ``--save-space`` | work slower, but using less space -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/recreate.rst b/docs/usage/recreate.rst new file mode 100644 index 00000000..97dff506 --- /dev/null +++ b/docs/usage/recreate.rst @@ -0,0 +1,33 @@ +.. include:: recreate.rst.inc + +Examples +~~~~~~~~ +:: + + # Make old (Attic / Borg 0.xx) archives deduplicate with Borg 1.x archives + # Archives created with Borg 1.1+ and the default chunker params are skipped (archive ID stays the same) + $ borg recreate /mnt/backup --chunker-params default --progress + + # Create a backup with little but fast compression + $ borg create /mnt/backup::archive /some/files --compression lz4 + # Then compress it - this might take longer, but the backup has already completed, so no inconsistencies + # from a long-running backup job. + $ borg recreate /mnt/backup::archive --recompress --compression zlib,9 + + # Remove unwanted files from all archives in a repository + $ borg recreate /mnt/backup -e /home/icke/Pictures/drunk_photos + + + # Change archive comment + $ borg create --comment "This is a comment" /mnt/backup::archivename ~ + $ borg info /mnt/backup::archivename + Name: archivename + Fingerprint: ... + Comment: This is a comment + ... + $ borg recreate --comment "This is a better comment" /mnt/backup::archivename + $ borg info /mnt/backup::archivename + Name: archivename + Fingerprint: ... + Comment: This is a better comment + ... diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index fd3ef446..1bb72e8c 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -24,7 +24,7 @@ optional arguments ``-s``, ``--stats`` | print statistics at end -`Common options`_ +:ref:`common_options` | Exclusion options diff --git a/docs/usage/rename.rst b/docs/usage/rename.rst new file mode 100644 index 00000000..456e8fca --- /dev/null +++ b/docs/usage/rename.rst @@ -0,0 +1,13 @@ +.. include:: rename.rst.inc + +Examples +~~~~~~~~ +:: + + $ borg create /path/to/repo::archivename ~ + $ borg list /path/to/repo + archivename Mon, 2016-02-15 19:50:19 + + $ borg rename /path/to/repo::archivename newname + $ borg list /path/to/repo + newname Mon, 2016-02-15 19:50:19 diff --git a/docs/usage/rename.rst.inc b/docs/usage/rename.rst.inc index 13baa7e4..ca2475fc 100644 --- a/docs/usage/rename.rst.inc +++ b/docs/usage/rename.rst.inc @@ -14,7 +14,7 @@ positional arguments NEWNAME the new archive name to use -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/serve.rst b/docs/usage/serve.rst new file mode 100644 index 00000000..ebc5626e --- /dev/null +++ b/docs/usage/serve.rst @@ -0,0 +1,31 @@ +.. include:: serve.rst.inc + +Examples +~~~~~~~~ + +borg serve has special support for ssh forced commands (see ``authorized_keys`` +example below): it will detect that you use such a forced command and extract +the value of the ``--restrict-to-path`` option(s). + +It will then parse the original command that came from the client, makes sure +that it is also ``borg serve`` and enforce path restriction(s) as given by the +forced command. That way, other options given by the client (like ``--info`` or +``--umask``) are preserved (and are not fixed by the forced command). + +Environment variables (such as BORG_HOSTNAME_IS_UNIQUE) contained in the original +command sent by the client are *not* interpreted, but ignored. If BORG_XXX environment +variables should be set on the ``borg serve`` side, then these must be set in system-specific +locations like ``/etc/environment`` or in the forced command itself (example below). + +:: + + # Allow an SSH keypair to only run borg, and only have access to /path/to/repo. + # Use key options to disable unneeded and potentially dangerous SSH functionality. + # This will help to secure an automated remote backup system. + $ cat ~/.ssh/authorized_keys + command="borg serve --restrict-to-path /path/to/repo",no-pty,no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-user-rc ssh-rsa AAAAB3[...] + + # Set a BORG_XXX environment variable on the "borg serve" side + $ cat ~/.ssh/authorized_keys + command="export BORG_XXX=value; borg serve [...]",restrict ssh-rsa [...] + diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index dab3c32f..0ac6a29a 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -11,12 +11,14 @@ borg serve optional arguments ``--restrict-to-path PATH`` | restrict repository access to PATH. Can be specified multiple times to allow the client access to several directories. Access to all sub-directories is granted implicitly; PATH doesn't need to directly point to a repository. + ``--restrict-to-repository PATH`` + | restrict repository access. Only the repository located at PATH (no sub-directories are considered) is accessible. Can be specified multiple times to allow the client access to several repositories. Unlike --restrict-to-path sub-directories are not accessible; PATH needs to directly point at a repository location. PATH may be an empty directory or the last element of PATH may not exist, in which case the client may initialize a repository there. ``--append-only`` | only allow appending to repository segment files ``--storage-quota`` | Override storage quota of the repository (e.g. 5G, 1.5T). When a new repository is initialized, sets the storage quota on the new repository as well. Default: no quota. -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/tar.rst b/docs/usage/tar.rst new file mode 100644 index 00000000..3ef38e16 --- /dev/null +++ b/docs/usage/tar.rst @@ -0,0 +1,18 @@ +.. include:: export-tar.rst.inc + +Examples +~~~~~~~~ +:: + + # export as uncompressed tar + $ borg export-tar /path/to/repo::Monday Monday.tar + + # exclude some types, compress using gzip + $ borg export-tar /path/to/repo::Monday Monday.tar.gz --exclude '*.so' + + # use higher compression level with gzip + $ borg export-tar testrepo::linux --tar-filter="gzip -9" Monday.tar.gz + + # export a gzipped tar, but instead of storing it on disk, + # upload it to a remote site using curl. + $ borg export-tar ... --tar-filter="gzip" - | curl --data-binary @- https://somewhere/to/POST diff --git a/docs/usage/umount.rst.inc b/docs/usage/umount.rst.inc index ab02038b..586d9f05 100644 --- a/docs/usage/umount.rst.inc +++ b/docs/usage/umount.rst.inc @@ -12,7 +12,7 @@ positional arguments MOUNTPOINT mountpoint of the filesystem to umount -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/upgrade.rst b/docs/usage/upgrade.rst new file mode 100644 index 00000000..f45da8ba --- /dev/null +++ b/docs/usage/upgrade.rst @@ -0,0 +1,30 @@ +.. include:: upgrade.rst.inc + +Examples +~~~~~~~~ +:: + + # Upgrade the borg repository to the most recent version. + $ borg upgrade -v /path/to/repo + making a hardlink copy in /path/to/repo.upgrade-2016-02-15-20:51:55 + opening attic repository with borg and converting + no key file found for repository + converting repo index /path/to/repo/index.0 + converting 1 segments... + converting borg 0.xx to borg current + no key file found for repository + +.. _borg_key_migrate-to-repokey: + +Upgrading a passphrase encrypted attic repo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +attic offered a "passphrase" encryption mode, but this was removed in borg 1.0 +and replaced by the "repokey" mode (which stores the passphrase-protected +encryption key into the repository config). + +Thus, to upgrade a "passphrase" attic repo to a "repokey" borg repo, 2 steps +are needed, in this order: + +- borg upgrade repo +- borg key migrate-to-repokey repo diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index bdf76ccd..b5ba44cc 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -25,7 +25,7 @@ optional arguments ``--disable-tam`` | Disable manifest authentication (in key and cache) -`Common options`_ +:ref:`common_options` | Description diff --git a/docs/usage/with-lock.rst.inc b/docs/usage/with-lock.rst.inc index 47b5abcc..c09ba943 100644 --- a/docs/usage/with-lock.rst.inc +++ b/docs/usage/with-lock.rst.inc @@ -16,7 +16,7 @@ positional arguments ARGS command arguments -`Common options`_ +:ref:`common_options` | Description diff --git a/setup.py b/setup.py index 726c849c..e9f0bd28 100644 --- a/setup.py +++ b/setup.py @@ -287,7 +287,7 @@ class build_usage(Command): def write_options(self, parser, fp): for group in parser._action_groups: if group.title == 'Common options': - fp.write('\n\n`Common options`_\n') + fp.write('\n\n:ref:`common_options`\n') fp.write(' |') else: self.write_options_group(group, fp) From 22311abe02854cb24c973df04bfd43797f3d7e65 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 7 Jun 2017 00:42:26 +0200 Subject: [PATCH 0990/1387] docs: usage: add benchmark page Previously, benchmark crud was not contained in the usage pages at all. --- docs/usage.rst | 1 + docs/usage/benchmark.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 docs/usage/benchmark.rst diff --git a/docs/usage.rst b/docs/usage.rst index 28da5e71..4f0419b4 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -52,6 +52,7 @@ Usage usage/tar usage/serve usage/lock + usage/benchmark usage/help usage/debug diff --git a/docs/usage/benchmark.rst b/docs/usage/benchmark.rst new file mode 100644 index 00000000..27436a9b --- /dev/null +++ b/docs/usage/benchmark.rst @@ -0,0 +1 @@ +.. include:: benchmark_crud.rst.inc From 13adc80cde9e4617fbed3b82343748b85b383f8e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 7 Jun 2017 00:44:53 +0200 Subject: [PATCH 0991/1387] docs: usage: avoid bash highlight, [options] instead of --- docs/usage/benchmark_crud.rst.inc | 4 ++-- docs/usage/break-lock.rst.inc | 4 ++-- docs/usage/change-passphrase.rst.inc | 4 ++-- docs/usage/check.rst.inc | 4 ++-- docs/usage/create.rst.inc | 4 ++-- docs/usage/delete.rst.inc | 4 ++-- docs/usage/diff.rst.inc | 4 ++-- docs/usage/export-tar.rst.inc | 4 ++-- docs/usage/extract.rst.inc | 4 ++-- docs/usage/info.rst.inc | 4 ++-- docs/usage/init.rst.inc | 4 ++-- docs/usage/key_change-passphrase.rst.inc | 4 ++-- docs/usage/key_export.rst.inc | 4 ++-- docs/usage/key_import.rst.inc | 4 ++-- docs/usage/key_migrate-to-repokey.rst.inc | 4 ++-- docs/usage/list.rst.inc | 4 ++-- docs/usage/mount.rst.inc | 4 ++-- docs/usage/prune.rst.inc | 4 ++-- docs/usage/recreate.rst.inc | 4 ++-- docs/usage/rename.rst.inc | 4 ++-- docs/usage/serve.rst.inc | 4 ++-- docs/usage/umount.rst.inc | 4 ++-- docs/usage/upgrade.rst.inc | 4 ++-- docs/usage/with-lock.rst.inc | 4 ++-- setup.py | 4 ++-- 25 files changed, 50 insertions(+), 50 deletions(-) diff --git a/docs/usage/benchmark_crud.rst.inc b/docs/usage/benchmark_crud.rst.inc index 6f2d78c8..58d40970 100644 --- a/docs/usage/benchmark_crud.rst.inc +++ b/docs/usage/benchmark_crud.rst.inc @@ -4,9 +4,9 @@ borg benchmark crud ------------------- -:: +.. code-block:: none - borg [common options] benchmark crud REPO PATH + borg [common options] benchmark crud [options] REPO PATH positional arguments REPO diff --git a/docs/usage/break-lock.rst.inc b/docs/usage/break-lock.rst.inc index f0bb1eac..fe5dcc84 100644 --- a/docs/usage/break-lock.rst.inc +++ b/docs/usage/break-lock.rst.inc @@ -4,9 +4,9 @@ borg break-lock --------------- -:: +.. code-block:: none - borg [common options] break-lock REPOSITORY + borg [common options] break-lock [options] REPOSITORY positional arguments REPOSITORY diff --git a/docs/usage/change-passphrase.rst.inc b/docs/usage/change-passphrase.rst.inc index b7addf3d..29384d47 100644 --- a/docs/usage/change-passphrase.rst.inc +++ b/docs/usage/change-passphrase.rst.inc @@ -4,9 +4,9 @@ borg change-passphrase ---------------------- -:: +.. code-block:: none - borg [common options] change-passphrase REPOSITORY + borg [common options] change-passphrase [options] REPOSITORY positional arguments REPOSITORY diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index 08aef086..7e021838 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -4,9 +4,9 @@ borg check ---------- -:: +.. code-block:: none - borg [common options] check REPOSITORY_OR_ARCHIVE + borg [common options] check [options] REPOSITORY_OR_ARCHIVE positional arguments REPOSITORY_OR_ARCHIVE diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index dc6dc7f6..105df5cc 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -4,9 +4,9 @@ borg create ----------- -:: +.. code-block:: none - borg [common options] create ARCHIVE PATH + borg [common options] create [options] ARCHIVE PATH positional arguments ARCHIVE diff --git a/docs/usage/delete.rst.inc b/docs/usage/delete.rst.inc index ca8f12b3..d8f727f0 100644 --- a/docs/usage/delete.rst.inc +++ b/docs/usage/delete.rst.inc @@ -4,9 +4,9 @@ borg delete ----------- -:: +.. code-block:: none - borg [common options] delete TARGET + borg [common options] delete [options] TARGET positional arguments TARGET diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index fe57eb05..74e70376 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -4,9 +4,9 @@ borg diff --------- -:: +.. code-block:: none - borg [common options] diff REPO_ARCHIVE1 ARCHIVE2 PATH + borg [common options] diff [options] REPO_ARCHIVE1 ARCHIVE2 PATH positional arguments REPO_ARCHIVE1 diff --git a/docs/usage/export-tar.rst.inc b/docs/usage/export-tar.rst.inc index cb81f9b2..845de54d 100644 --- a/docs/usage/export-tar.rst.inc +++ b/docs/usage/export-tar.rst.inc @@ -4,9 +4,9 @@ borg export-tar --------------- -:: +.. code-block:: none - borg [common options] export-tar ARCHIVE FILE PATH + borg [common options] export-tar [options] ARCHIVE FILE PATH positional arguments ARCHIVE diff --git a/docs/usage/extract.rst.inc b/docs/usage/extract.rst.inc index 72fc0356..4a63ffeb 100644 --- a/docs/usage/extract.rst.inc +++ b/docs/usage/extract.rst.inc @@ -4,9 +4,9 @@ borg extract ------------ -:: +.. code-block:: none - borg [common options] extract ARCHIVE PATH + borg [common options] extract [options] ARCHIVE PATH positional arguments ARCHIVE diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index a5fc65fe..4c382f10 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -4,9 +4,9 @@ borg info --------- -:: +.. code-block:: none - borg [common options] info REPOSITORY_OR_ARCHIVE + borg [common options] info [options] REPOSITORY_OR_ARCHIVE positional arguments REPOSITORY_OR_ARCHIVE diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index a1fe3815..1a5abcb6 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -4,9 +4,9 @@ borg init --------- -:: +.. code-block:: none - borg [common options] init REPOSITORY + borg [common options] init [options] REPOSITORY positional arguments REPOSITORY diff --git a/docs/usage/key_change-passphrase.rst.inc b/docs/usage/key_change-passphrase.rst.inc index aa0b92b5..91323a04 100644 --- a/docs/usage/key_change-passphrase.rst.inc +++ b/docs/usage/key_change-passphrase.rst.inc @@ -4,9 +4,9 @@ borg key change-passphrase -------------------------- -:: +.. code-block:: none - borg [common options] key change-passphrase REPOSITORY + borg [common options] key change-passphrase [options] REPOSITORY positional arguments REPOSITORY diff --git a/docs/usage/key_export.rst.inc b/docs/usage/key_export.rst.inc index 2d32b90b..a0d7cf13 100644 --- a/docs/usage/key_export.rst.inc +++ b/docs/usage/key_export.rst.inc @@ -4,9 +4,9 @@ borg key export --------------- -:: +.. code-block:: none - borg [common options] key export REPOSITORY PATH + borg [common options] key export [options] REPOSITORY PATH positional arguments REPOSITORY diff --git a/docs/usage/key_import.rst.inc b/docs/usage/key_import.rst.inc index e60f5620..86ab25b6 100644 --- a/docs/usage/key_import.rst.inc +++ b/docs/usage/key_import.rst.inc @@ -4,9 +4,9 @@ borg key import --------------- -:: +.. code-block:: none - borg [common options] key import REPOSITORY PATH + borg [common options] key import [options] REPOSITORY PATH positional arguments REPOSITORY diff --git a/docs/usage/key_migrate-to-repokey.rst.inc b/docs/usage/key_migrate-to-repokey.rst.inc index 9f730137..9acf215d 100644 --- a/docs/usage/key_migrate-to-repokey.rst.inc +++ b/docs/usage/key_migrate-to-repokey.rst.inc @@ -4,9 +4,9 @@ borg key migrate-to-repokey --------------------------- -:: +.. code-block:: none - borg [common options] key migrate-to-repokey REPOSITORY + borg [common options] key migrate-to-repokey [options] REPOSITORY positional arguments REPOSITORY diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index d181721e..8cb81152 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -4,9 +4,9 @@ borg list --------- -:: +.. code-block:: none - borg [common options] list REPOSITORY_OR_ARCHIVE PATH + borg [common options] list [options] REPOSITORY_OR_ARCHIVE PATH positional arguments REPOSITORY_OR_ARCHIVE diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index 57947a0b..bfb0a24c 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -4,9 +4,9 @@ borg mount ---------- -:: +.. code-block:: none - borg [common options] mount REPOSITORY_OR_ARCHIVE MOUNTPOINT + borg [common options] mount [options] REPOSITORY_OR_ARCHIVE MOUNTPOINT positional arguments REPOSITORY_OR_ARCHIVE diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index a1ef2a99..fb23c7fc 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -4,9 +4,9 @@ borg prune ---------- -:: +.. code-block:: none - borg [common options] prune REPOSITORY + borg [common options] prune [options] REPOSITORY positional arguments REPOSITORY diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index 1bb72e8c..7069423b 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -4,9 +4,9 @@ borg recreate ------------- -:: +.. code-block:: none - borg [common options] recreate REPOSITORY_OR_ARCHIVE PATH + borg [common options] recreate [options] REPOSITORY_OR_ARCHIVE PATH positional arguments REPOSITORY_OR_ARCHIVE diff --git a/docs/usage/rename.rst.inc b/docs/usage/rename.rst.inc index ca2475fc..10d4d32a 100644 --- a/docs/usage/rename.rst.inc +++ b/docs/usage/rename.rst.inc @@ -4,9 +4,9 @@ borg rename ----------- -:: +.. code-block:: none - borg [common options] rename ARCHIVE NEWNAME + borg [common options] rename [options] ARCHIVE NEWNAME positional arguments ARCHIVE diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index 0ac6a29a..2bd87e10 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -4,9 +4,9 @@ borg serve ---------- -:: +.. code-block:: none - borg [common options] serve + borg [common options] serve [options] optional arguments ``--restrict-to-path PATH`` diff --git a/docs/usage/umount.rst.inc b/docs/usage/umount.rst.inc index 586d9f05..45d2cb97 100644 --- a/docs/usage/umount.rst.inc +++ b/docs/usage/umount.rst.inc @@ -4,9 +4,9 @@ borg umount ----------- -:: +.. code-block:: none - borg [common options] umount MOUNTPOINT + borg [common options] umount [options] MOUNTPOINT positional arguments MOUNTPOINT diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index b5ba44cc..bbf51724 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -4,9 +4,9 @@ borg upgrade ------------ -:: +.. code-block:: none - borg [common options] upgrade REPOSITORY + borg [common options] upgrade [options] REPOSITORY positional arguments REPOSITORY diff --git a/docs/usage/with-lock.rst.inc b/docs/usage/with-lock.rst.inc index c09ba943..db5523b9 100644 --- a/docs/usage/with-lock.rst.inc +++ b/docs/usage/with-lock.rst.inc @@ -4,9 +4,9 @@ borg with-lock -------------- -:: +.. code-block:: none - borg [common options] with-lock REPOSITORY COMMAND ARGS + borg [common options] with-lock [options] REPOSITORY COMMAND ARGS positional arguments REPOSITORY diff --git a/setup.py b/setup.py index e9f0bd28..68379977 100644 --- a/setup.py +++ b/setup.py @@ -261,7 +261,7 @@ class build_usage(Command): "command_": command.replace(' ', '_'), "underline": '-' * len('borg ' + command)} doc.write(".. _borg_{command_}:\n\n".format(**params)) - doc.write("borg {command}\n{underline}\n::\n\n borg [common options] {command}".format(**params)) + doc.write("borg {command}\n{underline}\n.. code-block:: none\n\n borg [common options] {command}".format(**params)) self.write_usage(parser, doc) epilog = parser.epilog parser.epilog = None @@ -278,7 +278,7 @@ class build_usage(Command): def write_usage(self, parser, fp): if any(len(o.option_strings) for o in parser._actions): - fp.write(' ') + fp.write(' [options]') for option in parser._actions: if option.option_strings: continue From a7e8e8ccd9ed2677ee80276a4edc95a25a63b40b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 6 Jun 2017 00:42:12 +0200 Subject: [PATCH 0992/1387] fix parse_version, add tests, fixes #2556 --- src/borg/testsuite/version.py | 38 +++++++++++++++++++++++++++++++++ src/borg/version.py | 40 +++++++++++++++++------------------ 2 files changed, 58 insertions(+), 20 deletions(-) create mode 100644 src/borg/testsuite/version.py diff --git a/src/borg/testsuite/version.py b/src/borg/testsuite/version.py new file mode 100644 index 00000000..b5f32e6e --- /dev/null +++ b/src/borg/testsuite/version.py @@ -0,0 +1,38 @@ +import pytest + +from ..version import parse_version + + +@pytest.mark.parametrize("version_str, version_tuple", [ + # setuptools < 8.0 uses "-" + ('1.0.0a1.dev204-g8866961.d20170606', (1, 0, 0, -4, 1)), + ('1.0.0a1.dev204-g8866961', (1, 0, 0, -4, 1)), + ('1.0.0-d20170606', (1, 0, 0, -1)), + # setuptools >= 8.0 uses "+" + ('1.0.0a1.dev204+g8866961.d20170606', (1, 0, 0, -4, 1)), + ('1.0.0a1.dev204+g8866961', (1, 0, 0, -4, 1)), + ('1.0.0+d20170606', (1, 0, 0, -1)), + # pre-release versions: + ('1.0.0a1', (1, 0, 0, -4, 1)), + ('1.0.0a2', (1, 0, 0, -4, 2)), + ('1.0.0b3', (1, 0, 0, -3, 3)), + ('1.0.0rc4', (1, 0, 0, -2, 4)), + # release versions: + ('0.0.0', (0, 0, 0, -1)), + ('0.0.11', (0, 0, 11, -1)), + ('0.11.0', (0, 11, 0, -1)), + ('11.0.0', (11, 0, 0, -1)), +]) +def test_parse_version(version_str, version_tuple): + assert parse_version(version_str) == version_tuple + + +def test_parse_version_invalid(): + with pytest.raises(ValueError): + assert parse_version('') # we require x.y.z versions + with pytest.raises(ValueError): + assert parse_version('1') # we require x.y.z versions + with pytest.raises(ValueError): + assert parse_version('1.2') # we require x.y.z versions + with pytest.raises(ValueError): + assert parse_version('crap') diff --git a/src/borg/version.py b/src/borg/version.py index 4eb0c77d..7e2e95b7 100644 --- a/src/borg/version.py +++ b/src/borg/version.py @@ -3,33 +3,33 @@ import re def parse_version(version): """ - simplistic parser for setuptools_scm versions + Simplistic parser for setuptools_scm versions. - supports final versions and alpha ('a'), beta ('b') and rc versions. It just discards commits since last tag - and git revision hash. + Supports final versions and alpha ('a'), beta ('b') and release candidate ('rc') versions. + It does not try to parse anything else than that, even if there is more in the version string. Output is a version tuple containing integers. It ends with one or two elements that ensure that relational - operators yield correct relations for alpha, beta and rc versions too. For final versions the last element - is a -1, for prerelease versions the last two elements are a smaller negative number and the number of e.g. - the beta. - - Note, this sorts version 1.0 before 1.0.0. + operators yield correct relations for alpha, beta and rc versions, too. + For final versions the last element is a -1. + For prerelease versions the last two elements are a smaller negative number and the number of e.g. the beta. This version format is part of the remote protocol, don‘t change in breaking ways. """ - - parts = version.split('+')[0].split('.') - if parts[-1].startswith('dev'): - del parts[-1] - version = [int(segment) for segment in parts[:-1]] - - prerelease = re.fullmatch('([0-9]+)(a|b|rc)([0-9]+)', parts[-1]) - if prerelease: - version_type = {'a': -4, 'b': -3, 'rc': -2}[prerelease.group(2)] - version += [int(prerelease.group(1)), version_type, int(prerelease.group(3))] + version_re = r""" + (?P\d+)\.(?P\d+)\.(?P\d+) # version, e.g. 1.2.33 + (?P(?Pa|b|rc)(?P\d+))? # optional prerelease, e.g. a1 or b2 or rc33 + """ + m = re.match(version_re, version, re.VERBOSE) + if m is None: + raise ValueError('Invalid version string %s' % version) + gd = m.groupdict() + version = [int(gd['major']), int(gd['minor']), int(gd['patch'])] + if m.lastgroup == 'prerelease': + p_type = {'a': -4, 'b': -3, 'rc': -2}[gd['ptype']] + p_num = int(gd['pnum']) + version += [p_type, p_num] else: - version += [int(parts[-1]), -1] - + version += [-1] return tuple(version) From ee821a70f500fba9d036966403f182c4a2595f0c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 7 Jun 2017 11:06:37 +0200 Subject: [PATCH 0993/1387] update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 11bcaade..dc501569 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ src/borg/crypto/low_level.c src/borg/hashindex.c src/borg/item.c src/borg/algorithms/chunker.c -src/borg/algorithms/crc32.c +src/borg/algorithms/checksums.c src/borg/platform/darwin.c src/borg/platform/freebsd.c src/borg/platform/linux.c From 07deaf14cb6ce5fd56a5f3b1f071a1ae5d03dc40 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 7 Jun 2017 11:24:21 +0200 Subject: [PATCH 0994/1387] docs: faq: Can I use Borg on SMR hard drives? --- docs/faq.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index 71cdc014..46c846c8 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -171,6 +171,23 @@ Yes, if you want to detect accidental data damage (like bit rot), use the If you want to be able to detect malicious tampering also, use an encrypted repo. It will then be able to check using CRCs and HMACs. +Can I use Borg on SMR hard drives? +---------------------------------- + +SMR (shingled magnetic recording) hard drives are very different from +regular hard drives. Applications have to behave in certain ways or +performance will be heavily degraded. + +Borg 1.1 ships with default settings suitable for SMR drives, +and has been successfully tested on *Seagate Archive v2* drives +using the ext4 file system. + +Some Linux kernel versions between 3.19 and 4.5 had various bugs +handling device-managed SMR drives, leading to IO errors, unresponsive +drives and unreliable operation in general. + +For more details, refer to :issue:`2252`. + .. _faq-integrityerror: I get an IntegrityError or similar - what now? From cd676286464a6c4f6fecb0ad14faf30e3c3c19e2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 7 Jun 2017 11:28:37 +0200 Subject: [PATCH 0995/1387] docs: changes: fix rst warning --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 51170348..e321b2c0 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -418,7 +418,7 @@ Other changes: - ArchiveFormatter: add "start" key for compatibility with "info" - RemoteRepository: account rx/tx bytes - setup.py build_usage/build_man/build_api fixes -- Manifest.in: simplify, exclude *.{so,dll,orig}, #2066 +- Manifest.in: simplify, exclude .so, .dll and .orig, #2066 - FUSE: get rid of chunk accounting, st_blocks = ceil(size / blocksize). - tests: From 0d6064e7f3bf15a549910145d68cfebd18aced21 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 7 Jun 2017 16:29:37 +0200 Subject: [PATCH 0996/1387] docs: fix build_man for relocated examples --- docs/usage/create.rst | 2 +- docs/usage/extract.rst | 2 +- docs/usage/init.rst | 2 +- setup.py | 25 +++++++++++++++++++++++-- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/usage/create.rst b/docs/usage/create.rst index 71215735..2f05be39 100644 --- a/docs/usage/create.rst +++ b/docs/usage/create.rst @@ -67,4 +67,4 @@ Examples # Backing up relative paths by moving into the correct directory first $ cd /home/user/Documents # The root directory of the archive will be "projectA" - $ borg create /path/to/repo::daily-projectA-{now:%Y-%m-%d} projectA \ No newline at end of file + $ borg create /path/to/repo::daily-projectA-{now:%Y-%m-%d} projectA diff --git a/docs/usage/extract.rst b/docs/usage/extract.rst index 62cbbdc6..be926896 100644 --- a/docs/usage/extract.rst +++ b/docs/usage/extract.rst @@ -26,4 +26,4 @@ Examples .. Note:: Currently, extract always writes into the current working directory ("."), - so make sure you ``cd`` to the right place before calling ``borg extract``. \ No newline at end of file + so make sure you ``cd`` to the right place before calling ``borg extract``. diff --git a/docs/usage/init.rst b/docs/usage/init.rst index 46355b0d..97860a15 100644 --- a/docs/usage/init.rst +++ b/docs/usage/init.rst @@ -14,4 +14,4 @@ Examples $ borg init --encryption=repokey-blake2 user@hostname:backup # Remote repository (store the key your home dir) - $ borg init --encryption=keyfile user@hostname:backup \ No newline at end of file + $ borg init --encryption=keyfile user@hostname:backup diff --git a/setup.py b/setup.py index 68379977..43414503 100644 --- a/setup.py +++ b/setup.py @@ -358,6 +358,23 @@ class build_man(Command): """) + usage_group = { + 'break-lock': 'lock', + 'with-lock': 'lock', + + 'change-passphrase': 'key', + 'key_change-passphrase': 'key', + 'key_export': 'key', + 'key_import': 'key', + 'key_migrate-to-repokey': 'key', + + 'export-tar': 'tar', + + 'benchmark_crud': 'benchmark', + + 'umount': 'mount', + } + def initialize_options(self): pass @@ -495,11 +512,15 @@ class build_man(Command): write() def write_examples(self, write, command): - with open('docs/usage.rst') as fd: + command = command.replace(' ', '_') + with open('docs/usage/%s.rst' % self.usage_group.get(command, command)) as fd: usage = fd.read() - usage_include = '.. include:: usage/%s.rst.inc' % command + usage_include = '.. include:: %s.rst.inc' % command begin = usage.find(usage_include) end = usage.find('.. include', begin + 1) + # If a command has a dedicated anchor, it will occur before the command's include. + if 0 < usage.find('.. _', begin + 1) < end: + end = usage.find('.. _', begin + 1) examples = usage[begin:end] examples = examples.replace(usage_include, '') examples = examples.replace('Examples\n~~~~~~~~', '') From 1cf031045c1935951948b055a3c8e60aa86a4aa4 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 7 Jun 2017 13:50:20 +0200 Subject: [PATCH 0997/1387] nanorst for --help --- src/borg/archiver.py | 22 ++++- src/borg/nanorst.py | 162 +++++++++++++++++++++++++++++++++ src/borg/testsuite/archiver.py | 32 +++++++ src/borg/testsuite/nanorst.py | 37 ++++++++ 4 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 src/borg/nanorst.py create mode 100644 src/borg/testsuite/nanorst.py diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 1b135ee6..629a9bdd 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -64,6 +64,7 @@ from .helpers import basic_json_data, json_print from .helpers import replace_placeholders from .helpers import ChunkIteratorFileWrapper from .helpers import popen_with_error_handling +from .nanorst import RstToTextLazy, ansi_escapes from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern from .patterns import PatternMatcher from .item import Item @@ -2256,6 +2257,18 @@ class Archiver: setattr(args, dest, option_value) def build_parser(self): + if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() and (sys.platform != 'win32' or 'ANSICON' in os.environ): + rst_state_hook = ansi_escapes + else: + rst_state_hook = None + + # You can use :ref:`xyz` in the following usage pages. However, for plain-text view, + # e.g. through "borg ... --help", define a substitution for the reference here. + # It will replace the entire :ref:`foo` verbatim. + rst_plain_text_references = { + 'a_status_oddity': '"I am seeing ‘A’ (added) status for a unchanged file!?"', + } + def process_epilog(epilog): epilog = textwrap.dedent(epilog).splitlines() try: @@ -2264,7 +2277,10 @@ class Archiver: mode = 'command-line' if mode in ('command-line', 'build_usage'): epilog = [line for line in epilog if not line.startswith('.. man')] - return '\n'.join(epilog) + epilog = '\n'.join(epilog) + if mode == 'command-line': + epilog = RstToTextLazy(epilog, rst_state_hook, rst_plain_text_references) + return epilog def define_common_options(add_common_option): add_common_option('-h', '--help', action='help', help='show this help message and exit') @@ -3451,7 +3467,7 @@ class Archiver: used to have upgraded Borg 0.xx or Attic archives deduplicate with Borg 1.x archives. - USE WITH CAUTION. + **USE WITH CAUTION.** Depending on the PATHs and patterns given, recreate can be used to permanently delete files from archives. When in doubt, use "--dry-run --verbose --list" to see how patterns/PATHS are @@ -3761,7 +3777,7 @@ class Archiver: It creates input data below the given PATH and backups this data into the given REPO. The REPO must already exist (it could be a fresh empty repo or an existing repo, the - command will create / read / update / delete some archives named borg-test-data* there. + command will create / read / update / delete some archives named borg-test-data\* there. Make sure you have free space there, you'll need about 1GB each (+ overhead). diff --git a/src/borg/nanorst.py b/src/borg/nanorst.py new file mode 100644 index 00000000..3056a0ff --- /dev/null +++ b/src/borg/nanorst.py @@ -0,0 +1,162 @@ + +import io + + +class TextPecker: + def __init__(self, s): + self.str = s + self.i = 0 + + def read(self, n): + self.i += n + return self.str[self.i - n:self.i] + + def peek(self, n): + if n >= 0: + return self.str[self.i:self.i + n] + else: + return self.str[self.i + n - 1:self.i - 1] + + def peekline(self): + out = '' + i = self.i + while i < len(self.str) and self.str[i] != '\n': + out += self.str[i] + i += 1 + return out + + def readline(self): + out = self.peekline() + self.i += len(out) + return out + + +def rst_to_text(text, state_hook=None, references=None): + """ + Convert rST to a more human text form. + + This is a very loose conversion. No advanced rST features are supported. + The generated output directly depends on the input (e.g. indentation of + admonitions). + """ + state_hook = state_hook or (lambda old_state, new_state, out: None) + references = references or {} + state = 'text' + text = TextPecker(text) + out = io.StringIO() + + inline_single = ('*', '`') + + while True: + char = text.read(1) + if not char: + break + next = text.peek(1) # type: str + + if state == 'text': + if text.peek(-1) != '\\': + if char in inline_single and next not in inline_single: + state_hook(state, char, out) + state = char + continue + if char == next == '*': + state_hook(state, '**', out) + state = '**' + text.read(1) + continue + if char == next == '`': + state_hook(state, '``', out) + state = '``' + text.read(1) + continue + if text.peek(-1).isspace() and char == ':' and text.peek(5) == 'ref:`': + # translate reference + text.read(5) + ref = '' + while True: + char = text.peek(1) + if char == '`': + text.read(1) + break + if char == '\n': + text.read(1) + continue # merge line breaks in :ref:`...\n...` + ref += text.read(1) + try: + out.write(references[ref]) + except KeyError: + raise ValueError("Undefined reference in Archiver help: %r — please add reference substitution" + "to 'rst_plain_text_references'" % ref) + continue + if text.peek(-2) in ('\n\n', '') and char == next == '.': + text.read(2) + try: + directive, arguments = text.peekline().split('::', maxsplit=1) + except ValueError: + directive = None + text.readline() + text.read(1) + if not directive: + continue + out.write(directive.title()) + out.write(':\n') + if arguments: + out.write(arguments) + out.write('\n') + continue + if state in inline_single and char == state: + state_hook(state, 'text', out) + state = 'text' + continue + if state == '``' and char == next == '`': + state_hook(state, 'text', out) + state = 'text' + text.read(1) + continue + if state == '**' and char == next == '*': + state_hook(state, 'text', out) + state = 'text' + text.read(1) + continue + out.write(char) + + assert state == 'text', 'Invalid final state %r (This usually indicates unmatched */**)' % state + return out.getvalue() + + +def ansi_escapes(old_state, new_state, out): + if old_state == 'text' and new_state in ('*', '`', '``'): + out.write('\033[4m') + if old_state == 'text' and new_state == '**': + out.write('\033[1m') + if old_state in ('*', '`', '``', '**') and new_state == 'text': + out.write('\033[0m') + + +class RstToTextLazy: + def __init__(self, str, state_hook=None, references=None): + self.str = str + self.state_hook = state_hook + self.references = references + self._rst = None + + @property + def rst(self): + if self._rst is None: + self._rst = rst_to_text(self.str, self.state_hook, self.references) + return self._rst + + def __getattr__(self, item): + return getattr(self.rst, item) + + def __str__(self): + return self.rst + + def __add__(self, other): + return self.rst + other + + def __iter__(self): + return iter(self.rst) + + def __contains__(self, item): + return item in self.rst diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 1ce661ef..8ae235f2 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -30,6 +30,7 @@ try: except ImportError: pass +import borg from .. import xattr, helpers, platform from ..archive import Archive, ChunkBuffer, flags_noatime, flags_normal from ..archiver import Archiver, parse_storage_quota @@ -44,6 +45,7 @@ from ..helpers import Manifest from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from ..helpers import bin_to_hex from ..helpers import MAX_S +from ..nanorst import RstToTextLazy from ..patterns import IECommand, PatternMatcher, parse_pattern from ..item import Item from ..logger import setup_logging @@ -3335,3 +3337,33 @@ def test_parse_storage_quota(): assert parse_storage_quota('50M') == 50 * 1000**2 with pytest.raises(argparse.ArgumentTypeError): parse_storage_quota('5M') + + +def get_all_parsers(): + """ + Return dict mapping command to parser. + """ + parser = Archiver(prog='borg').build_parser() + parsers = {} + + def discover_level(prefix, parser, Archiver): + choices = {} + for action in parser._actions: + if action.choices is not None and 'SubParsersAction' in str(action.__class__): + for cmd, parser in action.choices.items(): + choices[prefix + cmd] = parser + if prefix and not choices: + return + + for command, parser in sorted(choices.items()): + discover_level(command + " ", parser, Archiver) + parsers[command] = parser + + discover_level("", parser, Archiver) + return parsers + + +@pytest.mark.parametrize('command, parser', list(get_all_parsers().items())) +def test_help_formatting(command, parser): + if isinstance(parser.epilog, RstToTextLazy): + assert parser.epilog.rst diff --git a/src/borg/testsuite/nanorst.py b/src/borg/testsuite/nanorst.py new file mode 100644 index 00000000..9b0cb760 --- /dev/null +++ b/src/borg/testsuite/nanorst.py @@ -0,0 +1,37 @@ + +import pytest + +from ..nanorst import rst_to_text + + +def test_inline(): + assert rst_to_text('*foo* and ``bar``.') == 'foo and bar.' + + +def test_inline_spread(): + assert rst_to_text('*foo and bar, thusly\nfoobar*.') == 'foo and bar, thusly\nfoobar.' + + +def test_comment_inline(): + assert rst_to_text('Foo and Bar\n.. foo\nbar') == 'Foo and Bar\n.. foo\nbar' + + +def test_comment(): + assert rst_to_text('Foo and Bar\n\n.. foo\nbar') == 'Foo and Bar\n\nbar' + + +def test_directive_note(): + assert rst_to_text('.. note::\n Note this and that') == 'Note:\n Note this and that' + + +def test_ref(): + references = { + 'foo': 'baz' + } + assert rst_to_text('See :ref:`fo\no`.', references=references) == 'See baz.' + + +def test_undefined_ref(): + with pytest.raises(ValueError) as exc_info: + rst_to_text('See :ref:`foo`.') + assert 'Undefined reference' in str(exc_info.value) From 3f8a0221ee16aec79a5ab19e375fe0d66b545af9 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 2 May 2017 18:52:36 +0200 Subject: [PATCH 0998/1387] Revert "move chunker to borg.algorithms" This reverts commit 956b50b29cb6d3aec6b7a02d46325e8ab50bc149. # Conflicts: # setup.py # src/borg/archive.py # src/borg/helpers.py --- .gitignore | 2 +- setup.py | 7 ++++--- src/borg/{algorithms/buzhash.c => _chunker.c} | 0 src/borg/archive.py | 2 +- src/borg/{algorithms => }/chunker.pyx | 2 +- src/borg/helpers.py | 9 ++++----- src/borg/testsuite/chunker.py | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) rename src/borg/{algorithms/buzhash.c => _chunker.c} (100%) rename src/borg/{algorithms => }/chunker.pyx (98%) diff --git a/.gitignore b/.gitignore index dc501569..26af5221 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ src/borg/compress.c src/borg/crypto/low_level.c src/borg/hashindex.c src/borg/item.c -src/borg/algorithms/chunker.c +src/borg/chunker.c src/borg/algorithms/checksums.c src/borg/platform/darwin.c src/borg/platform/freebsd.c diff --git a/setup.py b/setup.py index 43414503..d7746e4d 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ from distutils.command.clean import clean compress_source = 'src/borg/compress.pyx' crypto_ll_source = 'src/borg/crypto/low_level.pyx' -chunker_source = 'src/borg/algorithms/chunker.pyx' +chunker_source = 'src/borg/chunker.pyx' hashindex_source = 'src/borg/hashindex.pyx' item_source = 'src/borg/item.pyx' checksums_source = 'src/borg/algorithms/checksums.pyx' @@ -89,7 +89,7 @@ try: self.filelist.extend([ 'src/borg/compress.c', 'src/borg/crypto/low_level.c', - 'src/borg/algorithms/chunker.c', 'src/borg/algorithms/buzhash.c', + 'src/borg/chunker.c', 'src/borg/_chunker.c', 'src/borg/hashindex.c', 'src/borg/_hashindex.c', 'src/borg/item.c', 'src/borg/algorithms/checksums.c', @@ -623,8 +623,9 @@ if not on_rtd: Extension('borg.crypto.low_level', [crypto_ll_source], libraries=crypto_libraries, include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros), Extension('borg.hashindex', [hashindex_source]), Extension('borg.item', [item_source]), - Extension('borg.algorithms.chunker', [chunker_source]), + Extension('borg.chunker', [chunker_source]), Extension('borg.algorithms.checksums', [checksums_source]), + ] if not sys.platform.startswith(('win32', )): ext_modules.append(Extension('borg.platform.posix', [platform_posix_source])) diff --git a/src/borg/algorithms/buzhash.c b/src/borg/_chunker.c similarity index 100% rename from src/borg/algorithms/buzhash.c rename to src/borg/_chunker.c diff --git a/src/borg/archive.py b/src/borg/archive.py index fb680d9b..1c0b4b3f 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -20,7 +20,7 @@ from .logger import create_logger logger = create_logger() from . import xattr -from .algorithms.chunker import Chunker +from .chunker import Chunker from .cache import ChunkListEntry from .crypto.key import key_factory from .compress import Compressor, CompressionSpec diff --git a/src/borg/algorithms/chunker.pyx b/src/borg/chunker.pyx similarity index 98% rename from src/borg/algorithms/chunker.pyx rename to src/borg/chunker.pyx index efe87a49..bbe47cec 100644 --- a/src/borg/algorithms/chunker.pyx +++ b/src/borg/chunker.pyx @@ -4,7 +4,7 @@ API_VERSION = '1.1_01' from libc.stdlib cimport free -cdef extern from "buzhash.c": +cdef extern from "_chunker.c": ctypedef int uint32_t ctypedef struct _Chunker "Chunker": pass diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 1e79f63a..62d0f5ba 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1,11 +1,11 @@ import argparse -import collections import contextlib +import collections import grp import hashlib +import logging import io import json -import logging import os import os.path import platform @@ -27,21 +27,20 @@ from datetime import datetime, timezone, timedelta from functools import partial, lru_cache from itertools import islice from operator import attrgetter -from shutil import get_terminal_size from string import Formatter +from shutil import get_terminal_size import msgpack import msgpack.fallback from .logger import create_logger - logger = create_logger() import borg.crypto.low_level from . import __version__ as borg_version from . import __version_tuple__ as borg_version_tuple +from . import chunker from . import hashindex -from .algorithms import chunker from .constants import * # NOQA diff --git a/src/borg/testsuite/chunker.py b/src/borg/testsuite/chunker.py index d18c4d0b..2a14bd60 100644 --- a/src/borg/testsuite/chunker.py +++ b/src/borg/testsuite/chunker.py @@ -1,6 +1,6 @@ from io import BytesIO -from ..algorithms.chunker import Chunker, buzhash, buzhash_update +from ..chunker import Chunker, buzhash, buzhash_update from ..constants import * # NOQA from . import BaseTestCase From 5b9c34f523bb76fff277e5f95dbebda778140119 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 7 Jun 2017 23:53:19 +0200 Subject: [PATCH 0999/1387] borg.algorithms definition --- src/borg/algorithms/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/borg/algorithms/__init__.py b/src/borg/algorithms/__init__.py index e69de29b..cfa46c51 100644 --- a/src/borg/algorithms/__init__.py +++ b/src/borg/algorithms/__init__.py @@ -0,0 +1,11 @@ +""" +borg.algorithms +=============== + +This package is intended for hash and checksum functions. + +Ideally these would be sourced from existing libraries, +but are frequently not available yet (blake2), are +available but in poor form (crc32) or don't really +make sense as a library (xxHash). +""" From 4766d66875b577ebe60aaad9fd24e697064b2b1c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 8 Jun 2017 02:23:57 +0200 Subject: [PATCH 1000/1387] enable remote tests on cygwin the cygwin issue that caused these tests to break was fixed in cygwin at least since cygwin 2.8.0 (maybe even since 2.7.0). also added a comment to our workaround (os_write wrapper, that is needed still for people running older cygwin versions) that it can be removed when cygwin 2.8.0 is considered ancient (and everybody has upgraded to some fixed version). --- src/borg/remote.py | 2 ++ src/borg/testsuite/archiver.py | 1 - src/borg/testsuite/repository.py | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 5dc2a495..6ce6c3d0 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -42,6 +42,8 @@ RATELIMIT_PERIOD = 0.1 def os_write(fd, data): """os.write wrapper so we do not lose data for partial writes.""" + # TODO: this issue is fixed in cygwin since at least 2.8.0, remove this + # wrapper / workaround when this version is considered ancient. # This is happening frequently on cygwin due to its small pipe buffer size of only 64kiB # and also due to its different blocking pipe behaviour compared to Linux/*BSD. # Neither Linux nor *BSD ever do partial writes on blocking pipes, unless interrupted by a diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 8ae235f2..c1ec2b18 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2835,7 +2835,6 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): assert not self.cmd('list', self.repository_location) -@pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteArchiverTestCase(ArchiverTestCase): prefix = '__testsuite__:' diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 25e112bd..e29c7d53 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -774,7 +774,6 @@ class RepositoryCheckTestCase(RepositoryTestCaseBase): self.assert_equal(self.repository.get(H(0)), b'data2') -@pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteRepositoryTestCase(RepositoryTestCase): repository = None # type: RemoteRepository @@ -901,7 +900,6 @@ class RemoteLegacyFree(RepositoryTestCaseBase): self.repository.commit() -@pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteRepositoryCheckTestCase(RepositoryCheckTestCase): def open(self, create=False): From 524e0b5322dfe910f2b76340ea4e0a66de95bfd0 Mon Sep 17 00:00:00 2001 From: enkore Date: Thu, 8 Jun 2017 12:27:16 +0200 Subject: [PATCH 1001/1387] docs: development: vagrant, windows10 requirements --- docs/development.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/development.rst b/docs/development.rst index 6156be36..47e57725 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -260,6 +260,8 @@ standalone binaries for various platforms. For better security, there is no automatic sync in the VM to host direction. The plugin `vagrant-scp` is useful to copy stuff from the VMs to the host. +The "windows10" box requires the `reload` plugin (``vagrant plugin install vagrant-reload``). + Usage:: # To create and provision the VM: From 63d94cd14bf18d216a46e1a8d6e605438458d977 Mon Sep 17 00:00:00 2001 From: enkore Date: Thu, 8 Jun 2017 13:11:49 +0200 Subject: [PATCH 1002/1387] gitattributes: docs/usage/*.rst.INC merge=ours --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 3724dd9d..2657d942 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,5 @@ borg/_version.py export-subst *.py diff=python -docs/usage/* merge=ours +docs/usage/*.rst.inc merge=ours docs/man/* merge=ours From b75c214af59bcf4f7e837855ba0cd31b5ffbcce8 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 8 Jun 2017 15:16:52 +0200 Subject: [PATCH 1003/1387] hashindex: Cython defines PY_SSIZE_T_CLEAN --- src/borg/_hashindex.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/_hashindex.c b/src/borg/_hashindex.c index b41d57b7..335ccac2 100644 --- a/src/borg/_hashindex.c +++ b/src/borg/_hashindex.c @@ -473,7 +473,7 @@ hashindex_write(HashIndex *index, PyObject *file_py) .value_size = index->value_size }; - length_object = PyObject_CallMethod(file_py, "write", "y#", &header, (int)sizeof(HashHeader)); + length_object = PyObject_CallMethod(file_py, "write", "y#", &header, (Py_ssize_t)sizeof(HashHeader)); if(PyErr_Occurred()) { return; } From 6e011b935430ee2d4cf8953314692bbd8b057eee Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 27 May 2017 21:50:28 +0200 Subject: [PATCH 1004/1387] cache: compact hashindex before writing to chunks.archive.d --- src/borg/_hashindex.c | 76 +++++++++++++++++++++++++++++---- src/borg/cache.py | 47 ++++++++++++++------ src/borg/hashindex.pyx | 18 +++++--- src/borg/helpers.py | 2 +- src/borg/remote.py | 4 +- src/borg/testsuite/hashindex.py | 18 +++++++- 6 files changed, 131 insertions(+), 34 deletions(-) diff --git a/src/borg/_hashindex.c b/src/borg/_hashindex.c index 824a6eec..1fb30c0f 100644 --- a/src/borg/_hashindex.c +++ b/src/borg/_hashindex.c @@ -109,10 +109,11 @@ static int hash_sizes[] = { #define EPRINTF_PATH(path, msg, ...) fprintf(stderr, "hashindex: %s: " msg " (%s)\n", path, ##__VA_ARGS__, strerror(errno)) #ifdef Py_PYTHON_H -static HashIndex *hashindex_read(PyObject *file_py); +static HashIndex *hashindex_read(PyObject *file_py, int permit_compact); static void hashindex_write(HashIndex *index, PyObject *file_py); #endif +static uint64_t hashindex_compact(HashIndex *index); static HashIndex *hashindex_init(int capacity, int key_size, int value_size); static const void *hashindex_get(HashIndex *index, const void *key); static int hashindex_set(HashIndex *index, const void *key, const void *value); @@ -273,7 +274,7 @@ count_empty(HashIndex *index) #ifdef Py_PYTHON_H static HashIndex * -hashindex_read(PyObject *file_py) +hashindex_read(PyObject *file_py, int permit_compact) { Py_ssize_t length, buckets_length, bytes_read; Py_buffer header_buffer; @@ -393,14 +394,16 @@ hashindex_read(PyObject *file_py) } index->buckets = index->buckets_buffer.buf; - index->min_empty = get_min_empty(index->num_buckets); - index->num_empty = count_empty(index); + if(!permit_compact) { + index->min_empty = get_min_empty(index->num_buckets); + index->num_empty = count_empty(index); - if(index->num_empty < index->min_empty) { - /* too many tombstones here / not enough empty buckets, do a same-size rebuild */ - if(!hashindex_resize(index, index->num_buckets)) { - PyErr_Format(PyExc_ValueError, "Failed to rebuild table"); - goto fail_free_buckets; + if(index->num_empty < index->min_empty) { + /* too many tombstones here / not enough empty buckets, do a same-size rebuild */ + if(!hashindex_resize(index, index->num_buckets)) { + PyErr_Format(PyExc_ValueError, "Failed to rebuild table"); + goto fail_free_buckets; + } } } @@ -620,6 +623,61 @@ hashindex_next_key(HashIndex *index, const void *key) return BUCKET_ADDR(index, idx); } +static uint64_t +hashindex_compact(HashIndex *index) +{ + int idx = 0; + int start_idx; + int begin_used_idx; + int empty_slot_count, count, buckets_to_copy; + int compact_tail_idx = 0; + uint64_t saved_size = (index->num_buckets - index->num_entries) * (uint64_t)index->bucket_size; + + if(index->num_buckets - index->num_entries == 0) { + /* already compact */ + return 0; + } + + while(idx < index->num_buckets) { + /* Phase 1: Find some empty slots */ + start_idx = idx; + while((BUCKET_IS_EMPTY(index, idx) || BUCKET_IS_DELETED(index, idx)) && idx < index->num_buckets) { + idx++; + } + + /* everything from start_idx to idx is empty or deleted */ + count = empty_slot_count = idx - start_idx; + begin_used_idx = idx; + + if(!empty_slot_count) { + memcpy(BUCKET_ADDR(index, compact_tail_idx), BUCKET_ADDR(index, idx), index->bucket_size); + idx++; + compact_tail_idx++; + continue; + } + + /* Phase 2: Find some non-empty/non-deleted slots we can move to the compact tail */ + + while(!(BUCKET_IS_EMPTY(index, idx) || BUCKET_IS_DELETED(index, idx)) && empty_slot_count && idx < index->num_buckets) { + idx++; + empty_slot_count--; + } + + buckets_to_copy = count - empty_slot_count; + + if(!buckets_to_copy) { + /* Nothing to move, reached end of the buckets array with no used buckets. */ + break; + } + + memcpy(BUCKET_ADDR(index, compact_tail_idx), BUCKET_ADDR(index, begin_used_idx), buckets_to_copy * index->bucket_size); + compact_tail_idx += buckets_to_copy; + } + + index->num_buckets = index->num_entries; + return saved_size; +} + static int hashindex_len(HashIndex *index) { diff --git a/src/borg/cache.py b/src/borg/cache.py index 7a9fce02..3f89d67a 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -536,6 +536,10 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" archive indexes. """ archive_path = os.path.join(self.path, 'chunks.archive.d') + # Instrumentation + processed_item_metadata_bytes = 0 + processed_item_metadata_chunks = 0 + compact_chunks_archive_saved_space = 0 def mkpath(id, suffix=''): id_hex = bin_to_hex(id) @@ -545,8 +549,10 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def cached_archives(): if self.do_cache: fns = os.listdir(archive_path) - # filenames with 64 hex digits == 256bit - return set(unhexlify(fn) for fn in fns if len(fn) == 64) + # filenames with 64 hex digits == 256bit, + # or compact indices which are 64 hex digits + ".compact" + return set(unhexlify(fn) for fn in fns if len(fn) == 64) | \ + set(unhexlify(fn[:64]) for fn in fns if len(fn) == 72 and fn.endswith('.compact')) else: return set() @@ -558,13 +564,21 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" cleanup_cached_archive(id) def cleanup_cached_archive(id): - os.unlink(mkpath(id)) try: + os.unlink(mkpath(id)) os.unlink(mkpath(id) + '.integrity') except FileNotFoundError: pass + try: + os.unlink(mkpath(id, suffix='.compact')) + os.unlink(mkpath(id, suffix='.compact') + '.integrity') + except FileNotFoundError: + pass def fetch_and_build_idx(archive_id, decrypted_repository, chunk_idx): + nonlocal processed_item_metadata_bytes + nonlocal processed_item_metadata_chunks + nonlocal compact_chunks_archive_saved_space csize, data = decrypted_repository.get(archive_id) chunk_idx.add(archive_id, 1, len(data), csize) archive = ArchiveItem(internal_dict=msgpack.unpackb(data)) @@ -573,9 +587,12 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" sync = CacheSynchronizer(chunk_idx) for item_id, (csize, data) in zip(archive.items, decrypted_repository.get_many(archive.items)): chunk_idx.add(item_id, 1, len(data), csize) + processed_item_metadata_bytes += len(data) + processed_item_metadata_chunks += 1 sync.feed(data) if self.do_cache: - fn = mkpath(archive_id) + compact_chunks_archive_saved_space += chunk_idx.compact() + fn = mkpath(archive_id, suffix='.compact') fn_tmp = mkpath(archive_id, suffix='.tmp') try: with DetachedIntegrityCheckedFile(path=fn_tmp, write=True, @@ -612,7 +629,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" # due to hash table "resonance". master_index_capacity = int(len(self.repository) / ChunkIndex.MAX_LOAD_FACTOR) if archive_ids: - chunk_idx = None + chunk_idx = None if not self.do_cache else ChunkIndex(master_index_capacity) pi = ProgressIndicatorPercent(total=len(archive_ids), step=0.1, msg='%3.0f%% Syncing chunks cache. Processing archive %s', msgid='cache.sync') @@ -624,8 +641,12 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" archive_chunk_idx_path = mkpath(archive_id) logger.info("Reading cached archive chunk index for %s ...", archive_name) try: - with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path, write=False) as fd: - archive_chunk_idx = ChunkIndex.read(fd) + try: + with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path + '.compact', write=False) as fd: + archive_chunk_idx = ChunkIndex.read(fd, permit_compact=True) + except FileNotFoundError: + with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path, write=False) as fd: + archive_chunk_idx = ChunkIndex.read(fd) except FileIntegrityError as fie: logger.error('Cached archive chunk index of %s is corrupted: %s', archive_name, fie) # Delete it and fetch a new index @@ -639,18 +660,16 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" archive_chunk_idx = ChunkIndex() fetch_and_build_idx(archive_id, decrypted_repository, archive_chunk_idx) logger.info("Merging into master chunks index ...") - if chunk_idx is None: - # we just use the first archive's idx as starting point, - # to avoid growing the hash table from 0 size and also - # to save 1 merge call. - chunk_idx = archive_chunk_idx - else: - chunk_idx.merge(archive_chunk_idx) + chunk_idx.merge(archive_chunk_idx) else: chunk_idx = chunk_idx or ChunkIndex(master_index_capacity) logger.info('Fetching archive index for %s ...', archive_name) fetch_and_build_idx(archive_id, decrypted_repository, chunk_idx) pi.finish() + logger.debug('Cache sync: processed %s bytes (%d chunks) of metadata', + format_file_size(processed_item_metadata_bytes), processed_item_metadata_chunks) + logger.debug('Cache sync: compact chunks.archive.d storage saved %s bytes', + format_file_size(compact_chunks_archive_saved_space)) logger.info('Done.') return chunk_idx diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 33868344..f8c3f84b 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -8,14 +8,14 @@ from libc.stdint cimport uint32_t, UINT32_MAX, uint64_t from libc.errno cimport errno from cpython.exc cimport PyErr_SetFromErrnoWithFilename -API_VERSION = '1.1_02' +API_VERSION = '1.1_03' cdef extern from "_hashindex.c": ctypedef struct HashIndex: pass - HashIndex *hashindex_read(object file_py) except * + HashIndex *hashindex_read(object file_py, int permit_compact) except * HashIndex *hashindex_init(int capacity, int key_size, int value_size) void hashindex_free(HashIndex *index) int hashindex_len(HashIndex *index) @@ -25,6 +25,7 @@ cdef extern from "_hashindex.c": void *hashindex_next_key(HashIndex *index, void *key) int hashindex_delete(HashIndex *index, void *key) int hashindex_set(HashIndex *index, void *key, void *value) + uint64_t hashindex_compact(HashIndex *index) uint32_t _htole32(uint32_t v) uint32_t _le32toh(uint32_t v) @@ -73,14 +74,14 @@ cdef class IndexBase: MAX_LOAD_FACTOR = HASH_MAX_LOAD MAX_VALUE = _MAX_VALUE - def __cinit__(self, capacity=0, path=None, key_size=32): + def __cinit__(self, capacity=0, path=None, key_size=32, permit_compact=False): self.key_size = key_size if path: if isinstance(path, (str, bytes)): with open(path, 'rb') as fd: - self.index = hashindex_read(fd) + self.index = hashindex_read(fd, permit_compact) else: - self.index = hashindex_read(path) + self.index = hashindex_read(path, permit_compact) assert self.index, 'hashindex_read() returned NULL with no exception set' else: self.index = hashindex_init(capacity, self.key_size, self.value_size) @@ -92,8 +93,8 @@ cdef class IndexBase: hashindex_free(self.index) @classmethod - def read(cls, path): - return cls(path=path) + def read(cls, path, permit_compact=False): + return cls(path=path, permit_compact=permit_compact) def write(self, path): if isinstance(path, (str, bytes)): @@ -140,6 +141,9 @@ cdef class IndexBase: """Return size (bytes) of hash table.""" return hashindex_size(self.index) + def compact(self): + return hashindex_compact(self.index) + cdef class NSIndex(IndexBase): diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 7e4d4baf..e47277e8 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -126,7 +126,7 @@ def check_python(): def check_extension_modules(): from . import platform, compress, item - if hashindex.API_VERSION != '1.1_02': + if hashindex.API_VERSION != '1.1_03': raise ExtensionModuleError if chunker.API_VERSION != '1.1_01': raise ExtensionModuleError diff --git a/src/borg/remote.py b/src/borg/remote.py index 63b5e817..c316404d 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -642,8 +642,8 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. # in any case, we want to cleanly close the repo, even if the # rollback can not succeed (e.g. because the connection was # already closed) and raised another exception: - logger.debug('RemoteRepository: %d bytes sent, %d bytes received, %d messages sent', - self.tx_bytes, self.rx_bytes, self.msgid) + logger.debug('RemoteRepository: %s bytes sent, %s bytes received, %d messages sent', + format_file_size(self.tx_bytes), format_file_size(self.rx_bytes), self.msgid) self.close() @property diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 120c01b4..5550e1ad 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -4,7 +4,7 @@ import os import tempfile import zlib -from ..hashindex import NSIndex, ChunkIndex +from ..hashindex import NSIndex, ChunkIndex, ChunkIndexEntry from .. import hashindex from ..crypto.file_integrity import IntegrityCheckedFile, FileIntegrityError from . import BaseTestCase @@ -156,6 +156,22 @@ class HashIndexExtraTestCase(BaseTestCase): # the index should now be empty assert list(index.iteritems()) == [] + def test_vacuum(self): + idx1 = ChunkIndex() + idx1[H(1)] = 1, 100, 100 + idx1[H(2)] = 2, 200, 200 + idx1[H(3)] = 3, 300, 300 + idx1.compact() + assert idx1.size() == 18 + 3 * (32 + 3 * 4) + #with self.assert_raises(KeyError): + # idx1[H(1)] + data = list(idx1.iteritems()) + print(data) + assert (H(1), ChunkIndexEntry(1, 100, 100)) in data + assert (H(2), ChunkIndexEntry(2, 200, 200)) in data + assert (H(3), ChunkIndexEntry(3, 300, 300)) in data + + class HashIndexSizeTestCase(BaseTestCase): def test_size_on_disk(self): From 1379af22de6693b21252fb7682e9fe7eb13dcaf8 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 4 Jun 2017 19:50:25 +0200 Subject: [PATCH 1005/1387] HashIndexCompactTestCase --- src/borg/testsuite/hashindex.py | 131 ++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 5550e1ad..179f13a6 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -1,5 +1,6 @@ import base64 import hashlib +import io import os import tempfile import zlib @@ -18,6 +19,11 @@ def H(x): return bytes('%-0.32d' % x, 'ascii') +def H2(x): + # like H(x), but with pseudo-random distribution of the output value + return hashlib.sha256(H(x)).digest() + + class HashIndexTestCase(BaseTestCase): def _generic_test(self, cls, make_value, sha): @@ -357,6 +363,131 @@ class HashIndexIntegrityTestCase(HashIndexDataTestCase): ChunkIndex.read(fd) +class HashIndexCompactTestCase(HashIndexDataTestCase): + def index(self, num_entries, num_buckets): + index_data = io.BytesIO() + index_data.write(b'BORG_IDX') + # num_entries + index_data.write(num_entries.to_bytes(4, 'little')) + # num_buckets + index_data.write(num_buckets.to_bytes(4, 'little')) + # key_size + index_data.write((32).to_bytes(1, 'little')) + # value_size + index_data.write((3 * 4).to_bytes(1, 'little')) + + self.index_data = index_data + + def index_from_data(self): + self.index_data.seek(0) + index = ChunkIndex.read(self.index_data) + return index + + def index_to_data(self, index): + data = io.BytesIO() + index.write(data) + return data.getvalue() + + def index_from_data_compact_to_data(self): + index = self.index_from_data() + index.compact() + compact_index = self.index_to_data(index) + return compact_index + + def write_entry(self, key, *values): + self.index_data.write(key) + for value in values: + self.index_data.write(value.to_bytes(4, 'little')) + + def write_empty(self, key): + self.write_entry(key, 0xffffffff, 0, 0) + + def write_deleted(self, key): + self.write_entry(key, 0xfffffffe, 0, 0) + + def test_simple(self): + self.index(num_entries=3, num_buckets=6) + self.write_entry(H2(0), 1, 2, 3) + self.write_deleted(H2(1)) + self.write_empty(H2(2)) + self.write_entry(H2(3), 5, 6, 7) + self.write_entry(H2(4), 8, 9, 10) + self.write_empty(H2(5)) + + compact_index = self.index_from_data_compact_to_data() + + self.index(num_entries=3, num_buckets=3) + self.write_entry(H2(0), 1, 2, 3) + self.write_entry(H2(3), 5, 6, 7) + self.write_entry(H2(4), 8, 9, 10) + assert compact_index == self.index_data.getvalue() + + def test_first_empty(self): + self.index(num_entries=3, num_buckets=6) + self.write_deleted(H2(1)) + self.write_entry(H2(0), 1, 2, 3) + self.write_empty(H2(2)) + self.write_entry(H2(3), 5, 6, 7) + self.write_entry(H2(4), 8, 9, 10) + self.write_empty(H2(5)) + + compact_index = self.index_from_data_compact_to_data() + + self.index(num_entries=3, num_buckets=3) + self.write_entry(H2(0), 1, 2, 3) + self.write_entry(H2(3), 5, 6, 7) + self.write_entry(H2(4), 8, 9, 10) + assert compact_index == self.index_data.getvalue() + + def test_last_used(self): + self.index(num_entries=3, num_buckets=6) + self.write_deleted(H2(1)) + self.write_entry(H2(0), 1, 2, 3) + self.write_empty(H2(2)) + self.write_entry(H2(3), 5, 6, 7) + self.write_empty(H2(5)) + self.write_entry(H2(4), 8, 9, 10) + + compact_index = self.index_from_data_compact_to_data() + + self.index(num_entries=3, num_buckets=3) + self.write_entry(H2(0), 1, 2, 3) + self.write_entry(H2(3), 5, 6, 7) + self.write_entry(H2(4), 8, 9, 10) + assert compact_index == self.index_data.getvalue() + + def test_too_few_empty_slots(self): + self.index(num_entries=3, num_buckets=6) + self.write_deleted(H2(1)) + self.write_entry(H2(0), 1, 2, 3) + self.write_entry(H2(3), 5, 6, 7) + self.write_empty(H2(2)) + self.write_empty(H2(5)) + self.write_entry(H2(4), 8, 9, 10) + + compact_index = self.index_from_data_compact_to_data() + + self.index(num_entries=3, num_buckets=3) + self.write_entry(H2(0), 1, 2, 3) + self.write_entry(H2(3), 5, 6, 7) + self.write_entry(H2(4), 8, 9, 10) + assert compact_index == self.index_data.getvalue() + + def test_empty(self): + self.index(num_entries=0, num_buckets=6) + self.write_deleted(H2(1)) + self.write_empty(H2(0)) + self.write_deleted(H2(3)) + self.write_empty(H2(2)) + self.write_empty(H2(5)) + self.write_deleted(H2(4)) + + compact_index = self.index_from_data_compact_to_data() + + self.index(num_entries=0, num_buckets=0) + assert compact_index == self.index_data.getvalue() + + class NSIndexTestCase(BaseTestCase): def test_nsindex_segment_limit(self): idx = NSIndex() From 295ac86d712b0ed9d7787fc4b2e16dc1e048968a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Jun 2017 10:57:58 +0200 Subject: [PATCH 1006/1387] testsuite: hashindex: test compact -> merge --- src/borg/testsuite/hashindex.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 179f13a6..81c1d22d 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -162,22 +162,6 @@ class HashIndexExtraTestCase(BaseTestCase): # the index should now be empty assert list(index.iteritems()) == [] - def test_vacuum(self): - idx1 = ChunkIndex() - idx1[H(1)] = 1, 100, 100 - idx1[H(2)] = 2, 200, 200 - idx1[H(3)] = 3, 300, 300 - idx1.compact() - assert idx1.size() == 18 + 3 * (32 + 3 * 4) - #with self.assert_raises(KeyError): - # idx1[H(1)] - data = list(idx1.iteritems()) - print(data) - assert (H(1), ChunkIndexEntry(1, 100, 100)) in data - assert (H(2), ChunkIndexEntry(2, 200, 200)) in data - assert (H(3), ChunkIndexEntry(3, 300, 300)) in data - - class HashIndexSizeTestCase(BaseTestCase): def test_size_on_disk(self): @@ -487,6 +471,20 @@ class HashIndexCompactTestCase(HashIndexDataTestCase): self.index(num_entries=0, num_buckets=0) assert compact_index == self.index_data.getvalue() + def test_merge(self): + master = ChunkIndex() + idx1 = ChunkIndex() + idx1[H(1)] = 1, 100, 100 + idx1[H(2)] = 2, 200, 200 + idx1[H(3)] = 3, 300, 300 + idx1.compact() + assert idx1.size() == 18 + 3 * (32 + 3 * 4) + + master.merge(idx1) + assert master[H(1)] == (1, 100, 100) + assert master[H(2)] == (2, 200, 200) + assert master[H(3)] == (3, 300, 300) + class NSIndexTestCase(BaseTestCase): def test_nsindex_segment_limit(self): From 09a9d892cf8127f3b561382de26ca80cff5e36dc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Jun 2017 11:07:17 +0200 Subject: [PATCH 1007/1387] cache sync: convert existing archive chunks idx to compact --- src/borg/cache.py | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 3f89d67a..2c5099a4 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -563,12 +563,14 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" for id in ids: cleanup_cached_archive(id) - def cleanup_cached_archive(id): + def cleanup_cached_archive(id, cleanup_compact=True): try: os.unlink(mkpath(id)) os.unlink(mkpath(id) + '.integrity') except FileNotFoundError: pass + if not cleanup_compact: + return try: os.unlink(mkpath(id, suffix='.compact')) os.unlink(mkpath(id, suffix='.compact') + '.integrity') @@ -578,7 +580,6 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def fetch_and_build_idx(archive_id, decrypted_repository, chunk_idx): nonlocal processed_item_metadata_bytes nonlocal processed_item_metadata_chunks - nonlocal compact_chunks_archive_saved_space csize, data = decrypted_repository.get(archive_id) chunk_idx.add(archive_id, 1, len(data), csize) archive = ArchiveItem(internal_dict=msgpack.unpackb(data)) @@ -591,17 +592,21 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" processed_item_metadata_chunks += 1 sync.feed(data) if self.do_cache: - compact_chunks_archive_saved_space += chunk_idx.compact() - fn = mkpath(archive_id, suffix='.compact') - fn_tmp = mkpath(archive_id, suffix='.tmp') - try: - with DetachedIntegrityCheckedFile(path=fn_tmp, write=True, - filename=bin_to_hex(archive_id)) as fd: - chunk_idx.write(fd) - except Exception: - os.unlink(fn_tmp) - else: - os.rename(fn_tmp, fn) + write_archive_index(archive_id, chunk_idx) + + def write_archive_index(archive_id, chunk_idx): + nonlocal compact_chunks_archive_saved_space + compact_chunks_archive_saved_space += chunk_idx.compact() + fn = mkpath(archive_id, suffix='.compact') + fn_tmp = mkpath(archive_id, suffix='.tmp') + try: + with DetachedIntegrityCheckedFile(path=fn_tmp, write=True, + filename=bin_to_hex(archive_id)) as fd: + chunk_idx.write(fd) + except Exception: + os.unlink(fn_tmp) + else: + os.rename(fn_tmp, fn) def get_archive_ids_to_names(archive_ids): # Pass once over all archives and build a mapping from ids to names. @@ -642,11 +647,19 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" logger.info("Reading cached archive chunk index for %s ...", archive_name) try: try: + # Attempt to load compact index first with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path + '.compact', write=False) as fd: archive_chunk_idx = ChunkIndex.read(fd, permit_compact=True) + # In case a non-compact index exists, delete it. + cleanup_cached_archive(archive_id, cleanup_compact=False) except FileNotFoundError: + # No compact index found, load non-compact index with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path, write=False) as fd: archive_chunk_idx = ChunkIndex.read(fd) + # Automatically convert to compact index. Delete the existing index first. + logger.debug('Found non-compact index for %s, converting to compact.', archive_name) + cleanup_cached_archive(archive_id) + write_archive_index(archive_id, archive_chunk_idx) except FileIntegrityError as fie: logger.error('Cached archive chunk index of %s is corrupted: %s', archive_name, fie) # Delete it and fetch a new index From 3789459a4168ad6ae39fe265d878630f7a753c9c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Jun 2017 11:10:49 +0200 Subject: [PATCH 1008/1387] cache sync: extract read_archive_index function --- src/borg/cache.py | 53 +++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 2c5099a4..05a543b3 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -608,6 +608,35 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" else: os.rename(fn_tmp, fn) + def read_archive_index(archive_id, archive_name): + archive_chunk_idx_path = mkpath(archive_id) + logger.info("Reading cached archive chunk index for %s ...", archive_name) + try: + try: + # Attempt to load compact index first + with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path + '.compact', write=False) as fd: + archive_chunk_idx = ChunkIndex.read(fd, permit_compact=True) + # In case a non-compact index exists, delete it. + cleanup_cached_archive(archive_id, cleanup_compact=False) + # Compact index read - return index, no conversion necessary (below). + return archive_chunk_idx + except FileNotFoundError: + # No compact index found, load non-compact index, and convert below. + with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path, write=False) as fd: + archive_chunk_idx = ChunkIndex.read(fd) + except FileIntegrityError as fie: + logger.error('Cached archive chunk index of %s is corrupted: %s', archive_name, fie) + # Delete corrupted index, set warning. A new index must be build. + cleanup_cached_archive(archive_id) + set_ec(EXIT_WARNING) + return None + + # Convert to compact index. Delete the existing index first. + logger.debug('Found non-compact index for %s, converting to compact.', archive_name) + cleanup_cached_archive(archive_id) + write_archive_index(archive_id, archive_chunk_idx) + return archive_chunk_idx + def get_archive_ids_to_names(archive_ids): # Pass once over all archives and build a mapping from ids to names. # The easier approach, doing a similar loop for each archive, has @@ -643,29 +672,9 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" pi.show(info=[remove_surrogates(archive_name)]) if self.do_cache: if archive_id in cached_ids: - archive_chunk_idx_path = mkpath(archive_id) - logger.info("Reading cached archive chunk index for %s ...", archive_name) - try: - try: - # Attempt to load compact index first - with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path + '.compact', write=False) as fd: - archive_chunk_idx = ChunkIndex.read(fd, permit_compact=True) - # In case a non-compact index exists, delete it. - cleanup_cached_archive(archive_id, cleanup_compact=False) - except FileNotFoundError: - # No compact index found, load non-compact index - with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path, write=False) as fd: - archive_chunk_idx = ChunkIndex.read(fd) - # Automatically convert to compact index. Delete the existing index first. - logger.debug('Found non-compact index for %s, converting to compact.', archive_name) - cleanup_cached_archive(archive_id) - write_archive_index(archive_id, archive_chunk_idx) - except FileIntegrityError as fie: - logger.error('Cached archive chunk index of %s is corrupted: %s', archive_name, fie) - # Delete it and fetch a new index - cleanup_cached_archive(archive_id) + archive_chunk_idx = read_archive_index(archive_id, archive_name) + if archive_chunk_idx is None: cached_ids.remove(archive_id) - set_ec(EXIT_WARNING) if archive_id not in cached_ids: # Do not make this an else branch; the FileIntegrityError exception handler # above can remove *archive_id* from *cached_ids*. From 92a01f9d6cd21db307cc8c383105977f74af6ddc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Jun 2017 11:13:13 +0200 Subject: [PATCH 1009/1387] cache sync: fix incorrect .integrity location for .compact --- src/borg/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 05a543b3..7dddc93a 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -601,7 +601,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" fn_tmp = mkpath(archive_id, suffix='.tmp') try: with DetachedIntegrityCheckedFile(path=fn_tmp, write=True, - filename=bin_to_hex(archive_id)) as fd: + filename=bin_to_hex(archive_id) + '.compact') as fd: chunk_idx.write(fd) except Exception: os.unlink(fn_tmp) From a75bfae2cf0849147f5cdceb5618d1103338452c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Jun 2017 11:14:31 +0200 Subject: [PATCH 1010/1387] testsuite: Corruption test_chunks_archive, adapt for .compact --- src/borg/testsuite/archiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 9e207f96..27601626 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2934,7 +2934,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): chunks_archive = os.path.join(self.cache_path, 'chunks.archive.d') assert len(os.listdir(chunks_archive)) == 4 # two archives, one chunks cache and one .integrity file each - self.corrupt(os.path.join(chunks_archive, target_id)) + self.corrupt(os.path.join(chunks_archive, target_id + '.compact')) # Trigger cache sync by changing the manifest ID in the cache config config_path = os.path.join(self.cache_path, 'config') From 1d5d50463c91beab8304e58bbd191144bb1b2ab8 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Jun 2017 11:21:50 +0200 Subject: [PATCH 1011/1387] hashindex_compact: use memmove for possibly overlapping copy --- src/borg/_hashindex.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/borg/_hashindex.c b/src/borg/_hashindex.c index 1fb30c0f..8ed95b74 100644 --- a/src/borg/_hashindex.c +++ b/src/borg/_hashindex.c @@ -650,7 +650,8 @@ hashindex_compact(HashIndex *index) begin_used_idx = idx; if(!empty_slot_count) { - memcpy(BUCKET_ADDR(index, compact_tail_idx), BUCKET_ADDR(index, idx), index->bucket_size); + /* In case idx==compact_tail_idx, the areas overlap */ + memmove(BUCKET_ADDR(index, compact_tail_idx), BUCKET_ADDR(index, idx), index->bucket_size); idx++; compact_tail_idx++; continue; From 3664adb95d4a0d17631d4cf3b2482281c18f8f0f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Jun 2017 13:57:27 +0200 Subject: [PATCH 1012/1387] docs: highlight experimental features in online docs --- docs/borg_theme/css/borg.css | 14 ++++++++++++++ docs/conf.py | 4 ++++ docs/usage/general.rst | 7 +++++++ 3 files changed, 25 insertions(+) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index ae807b46..f97b5907 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -38,3 +38,17 @@ dt code { #internals .toctree-wrapper > ul > li > ul { font-weight: normal; } + +.experimental, +#debugging-facilities, +#borg-recreate { + /* don't change text dimensions */ + margin: 0 -40px; /* padding below + border width */ + padding: 0 20px; /* 20 px visual margin between edge of text and the border */ + /* fallback for browsers that don't have repeating-linear-gradient: thick, red lines */ + border-left: 20px solid red; + border-right: 20px solid red; + /* fancy red-orange stripes */ + border-image: repeating-linear-gradient( + -45deg,red 0,red 10px,#ffa800 10px,#ffa800 20px,red 20px) 0 20 repeat; +} diff --git a/docs/conf.py b/docs/conf.py index 30171b3b..81c397bc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -74,6 +74,10 @@ exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None +# The Borg docs contain no or very little Python docs. +# Thus, the primary domain is rst. +primary_domain = 'rst' + # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True diff --git a/docs/usage/general.rst b/docs/usage/general.rst index 6b9ecd0a..062d89c8 100644 --- a/docs/usage/general.rst +++ b/docs/usage/general.rst @@ -5,6 +5,13 @@ Borg consists of a number of commands. Each command accepts a number of arguments and options and interprets various environment variables. The following sections will describe each command in detail. +.. container:: experimental + + Experimental features are marked with red-orange stripes on the sides, like this paragraph. + + Experimental features are not stable, which means that they may be changed in incompatible + ways or even removed entirely without prior notice in following releases. + .. include:: ../usage_general.rst.inc In case you are interested in more details (like formulas), please see From c5d2c7c7994faee508a45c59f41fbb46dd15438a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Jun 2017 16:00:46 +0200 Subject: [PATCH 1013/1387] docs: split deployment --- docs/deployment.rst | 223 +-------------------- docs/deployment/central-backup-server.rst | 226 ++++++++++++++++++++++ 2 files changed, 230 insertions(+), 219 deletions(-) create mode 100644 docs/deployment/central-backup-server.rst diff --git a/docs/deployment.rst b/docs/deployment.rst index d57132fd..c4e1ef3b 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -1,227 +1,12 @@ .. include:: global.rst.inc .. highlight:: none -.. _deployment: Deployment ========== -This chapter will give an example how to setup a borg repository server for multiple -clients. +This chapter details deployment strategies for the following scenarios. -Machines --------- +.. toctree:: + :titlesonly: -There are multiple machines used in this chapter and will further be named by their -respective fully qualified domain name (fqdn). - -* The backup server: `backup01.srv.local` -* The clients: - - - John Doe's desktop: `johndoe.clnt.local` - - Webserver 01: `web01.srv.local` - - Application server 01: `app01.srv.local` - -User and group --------------- - -The repository server needs to have only one UNIX user for all the clients. -Recommended user and group with additional settings: - -* User: `backup` -* Group: `backup` -* Shell: `/bin/bash` (or other capable to run the `borg serve` command) -* Home: `/home/backup` - -Most clients shall initiate a backup from the root user to catch all -users, groups and permissions (e.g. when backing up `/home`). - -Folders -------- - -The following folder tree layout is suggested on the repository server: - -* User home directory, /home/backup -* Repositories path (storage pool): /home/backup/repos -* Clients restricted paths (`/home/backup/repos/`): - - - johndoe.clnt.local: `/home/backup/repos/johndoe.clnt.local` - - web01.srv.local: `/home/backup/repos/web01.srv.local` - - app01.srv.local: `/home/backup/repos/app01.srv.local` - -Restrictions ------------- - -Borg is instructed to restrict clients into their own paths: -``borg serve --restrict-to-path /home/backup/repos/`` - -The client will be able to access any file or subdirectory inside of ``/home/backup/repos/`` -but no other directories. You can allow a client to access several separate directories by passing multiple -`--restrict-to-path` flags, for instance: ``borg serve --restrict-to-path /home/backup/repos/ --restrict-to-path /home/backup/repos/``, -which could make sense if multiple machines belong to one person which should then have access to all the -backups of their machines. - -There is only one ssh key per client allowed. Keys are added for ``johndoe.clnt.local``, ``web01.srv.local`` and -``app01.srv.local``. But they will access the backup under only one UNIX user account as: -``backup@backup01.srv.local``. Every key in ``$HOME/.ssh/authorized_keys`` has a -forced command and restrictions applied as shown below: - -:: - - command="cd /home/backup/repos/; - borg serve --restrict-to-path /home/backup/repos/", - no-port-forwarding,no-X11-forwarding,no-pty, - no-agent-forwarding,no-user-rc - -.. note:: The text shown above needs to be written on a single line! - -The options which are added to the key will perform the following: - -1. Change working directory -2. Run ``borg serve`` restricted to the client base path -3. Restrict ssh and do not allow stuff which imposes a security risk - -Due to the ``cd`` command we use, the server automatically changes the current -working directory. Then client doesn't need to have knowledge of the absolute -or relative remote repository path and can directly access the repositories at -``@:``. - -.. note:: The setup above ignores all client given commandline parameters - which are normally appended to the `borg serve` command. - -Client ------- - -The client needs to initialize the `pictures` repository like this: - - borg init backup@backup01.srv.local:pictures - -Or with the full path (should actually never be used, as only for demonstrational purposes). -The server should automatically change the current working directory to the `` folder. - - borg init backup@backup01.srv.local:/home/backup/repos/johndoe.clnt.local/pictures - -When `johndoe.clnt.local` tries to access a not restricted path the following error is raised. -John Doe tries to backup into the Web 01 path: - - borg init backup@backup01.srv.local:/home/backup/repos/web01.srv.local/pictures - -:: - - ~~~ SNIP ~~~ - Remote: borg.remote.PathNotAllowed: /home/backup/repos/web01.srv.local/pictures - ~~~ SNIP ~~~ - Repository path not allowed - -Ansible -------- - -Ansible takes care of all the system-specific commands to add the user, create the -folder. Even when the configuration is changed the repository server configuration is -satisfied and reproducible. - -Automate setting up an repository server with the user, group, folders and -permissions a Ansible playbook could be used. Keep in mind the playbook -uses the Arch Linux `pacman `_ -package manager to install and keep borg up-to-date. - -:: - - - hosts: backup01.srv.local - vars: - user: backup - group: backup - home: /home/backup - pool: "{{ home }}/repos" - auth_users: - - host: johndoe.clnt.local - key: "{{ lookup('file', '/path/to/keys/johndoe.clnt.local.pub') }}" - - host: web01.clnt.local - key: "{{ lookup('file', '/path/to/keys/web01.clnt.local.pub') }}" - - host: app01.clnt.local - key: "{{ lookup('file', '/path/to/keys/app01.clnt.local.pub') }}" - tasks: - - pacman: name=borg state=latest update_cache=yes - - group: name="{{ group }}" state=present - - user: name="{{ user }}" shell=/bin/bash home="{{ home }}" createhome=yes group="{{ group }}" groups= state=present - - file: path="{{ home }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory - - file: path="{{ home }}/.ssh" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory - - file: path="{{ pool }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory - - authorized_key: user="{{ user }}" - key="{{ item.key }}" - key_options='command="cd {{ pool }}/{{ item.host }};borg serve --restrict-to-path {{ pool }}/{{ item.host }}",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc' - with_items: "{{ auth_users }}" - - file: path="{{ home }}/.ssh/authorized_keys" owner="{{ user }}" group="{{ group }}" mode=0600 state=file - - file: path="{{ pool }}/{{ item.host }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory - with_items: "{{ auth_users }}" - -Salt ----- - -This is a configuration similar to the one above, configured to be deployed with -Salt running on a Debian system. - -:: - - Install borg backup from pip: - pkg.installed: - - pkgs: - - python3 - - python3-dev - - python3-pip - - python-virtualenv - - libssl-dev - - openssl - - libacl1-dev - - libacl1 - - liblz4-dev - - liblz4-1 - - build-essential - - libfuse-dev - - fuse - - pkg-config - pip.installed: - - pkgs: ["borgbackup"] - - bin_env: /usr/bin/pip3 - - Setup backup user: - user.present: - - name: backup - - fullname: Backup User - - home: /home/backup - - shell: /bin/bash - # CAUTION! - # If you change the ssh command= option below, it won't necessarily get pushed to the backup - # server correctly unless you delete the ~/.ssh/authorized_keys file and re-create it! - {% for host in backupclients %} - Give backup access to {{host}}: - ssh_auth.present: - - user: backup - - source: salt://conf/ssh-pubkeys/{{host}}-backup.id_ecdsa.pub - - options: - - command="cd /home/backup/repos/{{host}}; borg serve --restrict-to-path /home/backup/repos/{{host}}" - - no-port-forwarding - - no-X11-forwarding - - no-pty - - no-agent-forwarding - - no-user-rc - {% endfor %} - - -Enhancements ------------- - -As this chapter only describes a simple and effective setup it could be further -enhanced when supporting (a limited set) of client supplied commands. A wrapper -for starting `borg serve` could be written. Or borg itself could be enhanced to -autodetect it runs under SSH by checking the `SSH_ORIGINAL_COMMAND` environment -variable. This is left open for future improvements. - -When extending ssh autodetection in borg no external wrapper script is necessary -and no other interpreter or application has to be deployed. - -See also --------- - -* `SSH Daemon manpage `_ -* `Ansible `_ -* `Salt `_ + deployment/central-backup-server diff --git a/docs/deployment/central-backup-server.rst b/docs/deployment/central-backup-server.rst new file mode 100644 index 00000000..68c8fdda --- /dev/null +++ b/docs/deployment/central-backup-server.rst @@ -0,0 +1,226 @@ +.. include:: ../global.rst.inc +.. highlight:: none + +Central repository server with Ansible or Salt +============================================== + +This section will give an example how to setup a borg repository server for multiple +clients. + +Machines +-------- + +There are multiple machines used in this section and will further be named by their +respective fully qualified domain name (fqdn). + +* The backup server: `backup01.srv.local` +* The clients: + + - John Doe's desktop: `johndoe.clnt.local` + - Webserver 01: `web01.srv.local` + - Application server 01: `app01.srv.local` + +User and group +-------------- + +The repository server needs to have only one UNIX user for all the clients. +Recommended user and group with additional settings: + +* User: `backup` +* Group: `backup` +* Shell: `/bin/bash` (or other capable to run the `borg serve` command) +* Home: `/home/backup` + +Most clients shall initiate a backup from the root user to catch all +users, groups and permissions (e.g. when backing up `/home`). + +Folders +------- + +The following folder tree layout is suggested on the repository server: + +* User home directory, /home/backup +* Repositories path (storage pool): /home/backup/repos +* Clients restricted paths (`/home/backup/repos/`): + + - johndoe.clnt.local: `/home/backup/repos/johndoe.clnt.local` + - web01.srv.local: `/home/backup/repos/web01.srv.local` + - app01.srv.local: `/home/backup/repos/app01.srv.local` + +Restrictions +------------ + +Borg is instructed to restrict clients into their own paths: +``borg serve --restrict-to-path /home/backup/repos/`` + +The client will be able to access any file or subdirectory inside of ``/home/backup/repos/`` +but no other directories. You can allow a client to access several separate directories by passing multiple +`--restrict-to-path` flags, for instance: ``borg serve --restrict-to-path /home/backup/repos/ --restrict-to-path /home/backup/repos/``, +which could make sense if multiple machines belong to one person which should then have access to all the +backups of their machines. + +There is only one ssh key per client allowed. Keys are added for ``johndoe.clnt.local``, ``web01.srv.local`` and +``app01.srv.local``. But they will access the backup under only one UNIX user account as: +``backup@backup01.srv.local``. Every key in ``$HOME/.ssh/authorized_keys`` has a +forced command and restrictions applied as shown below: + +:: + + command="cd /home/backup/repos/; + borg serve --restrict-to-path /home/backup/repos/", + no-port-forwarding,no-X11-forwarding,no-pty, + no-agent-forwarding,no-user-rc + +.. note:: The text shown above needs to be written on a single line! + +The options which are added to the key will perform the following: + +1. Change working directory +2. Run ``borg serve`` restricted to the client base path +3. Restrict ssh and do not allow stuff which imposes a security risk + +Due to the ``cd`` command we use, the server automatically changes the current +working directory. Then client doesn't need to have knowledge of the absolute +or relative remote repository path and can directly access the repositories at +``@:``. + +.. note:: The setup above ignores all client given commandline parameters + which are normally appended to the `borg serve` command. + +Client +------ + +The client needs to initialize the `pictures` repository like this: + + borg init backup@backup01.srv.local:pictures + +Or with the full path (should actually never be used, as only for demonstrational purposes). +The server should automatically change the current working directory to the `` folder. + + borg init backup@backup01.srv.local:/home/backup/repos/johndoe.clnt.local/pictures + +When `johndoe.clnt.local` tries to access a not restricted path the following error is raised. +John Doe tries to backup into the Web 01 path: + + borg init backup@backup01.srv.local:/home/backup/repos/web01.srv.local/pictures + +:: + + ~~~ SNIP ~~~ + Remote: borg.remote.PathNotAllowed: /home/backup/repos/web01.srv.local/pictures + ~~~ SNIP ~~~ + Repository path not allowed + +Ansible +------- + +Ansible takes care of all the system-specific commands to add the user, create the +folder. Even when the configuration is changed the repository server configuration is +satisfied and reproducible. + +Automate setting up an repository server with the user, group, folders and +permissions a Ansible playbook could be used. Keep in mind the playbook +uses the Arch Linux `pacman `_ +package manager to install and keep borg up-to-date. + +:: + + - hosts: backup01.srv.local + vars: + user: backup + group: backup + home: /home/backup + pool: "{{ home }}/repos" + auth_users: + - host: johndoe.clnt.local + key: "{{ lookup('file', '/path/to/keys/johndoe.clnt.local.pub') }}" + - host: web01.clnt.local + key: "{{ lookup('file', '/path/to/keys/web01.clnt.local.pub') }}" + - host: app01.clnt.local + key: "{{ lookup('file', '/path/to/keys/app01.clnt.local.pub') }}" + tasks: + - pacman: name=borg state=latest update_cache=yes + - group: name="{{ group }}" state=present + - user: name="{{ user }}" shell=/bin/bash home="{{ home }}" createhome=yes group="{{ group }}" groups= state=present + - file: path="{{ home }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory + - file: path="{{ home }}/.ssh" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory + - file: path="{{ pool }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory + - authorized_key: user="{{ user }}" + key="{{ item.key }}" + key_options='command="cd {{ pool }}/{{ item.host }};borg serve --restrict-to-path {{ pool }}/{{ item.host }}",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc' + with_items: "{{ auth_users }}" + - file: path="{{ home }}/.ssh/authorized_keys" owner="{{ user }}" group="{{ group }}" mode=0600 state=file + - file: path="{{ pool }}/{{ item.host }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory + with_items: "{{ auth_users }}" + +Salt +---- + +This is a configuration similar to the one above, configured to be deployed with +Salt running on a Debian system. + +:: + + Install borg backup from pip: + pkg.installed: + - pkgs: + - python3 + - python3-dev + - python3-pip + - python-virtualenv + - libssl-dev + - openssl + - libacl1-dev + - libacl1 + - liblz4-dev + - liblz4-1 + - build-essential + - libfuse-dev + - fuse + - pkg-config + pip.installed: + - pkgs: ["borgbackup"] + - bin_env: /usr/bin/pip3 + + Setup backup user: + user.present: + - name: backup + - fullname: Backup User + - home: /home/backup + - shell: /bin/bash + # CAUTION! + # If you change the ssh command= option below, it won't necessarily get pushed to the backup + # server correctly unless you delete the ~/.ssh/authorized_keys file and re-create it! + {% for host in backupclients %} + Give backup access to {{host}}: + ssh_auth.present: + - user: backup + - source: salt://conf/ssh-pubkeys/{{host}}-backup.id_ecdsa.pub + - options: + - command="cd /home/backup/repos/{{host}}; borg serve --restrict-to-path /home/backup/repos/{{host}}" + - no-port-forwarding + - no-X11-forwarding + - no-pty + - no-agent-forwarding + - no-user-rc + {% endfor %} + + +Enhancements +------------ + +As this section only describes a simple and effective setup it could be further +enhanced when supporting (a limited set) of client supplied commands. A wrapper +for starting `borg serve` could be written. Or borg itself could be enhanced to +autodetect it runs under SSH by checking the `SSH_ORIGINAL_COMMAND` environment +variable. This is left open for future improvements. + +When extending ssh autodetection in borg no external wrapper script is necessary +and no other interpreter or application has to be deployed. + +See also +-------- + +* `SSH Daemon manpage `_ +* `Ansible `_ +* `Salt `_ From 13fe8027131178bbfecd16212d836e2de8bf9ee6 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Jun 2017 16:01:01 +0200 Subject: [PATCH 1014/1387] docs: deployment: hosting repositories --- docs/deployment.rst | 1 + docs/deployment/hosting-repositories.rst | 73 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 docs/deployment/hosting-repositories.rst diff --git a/docs/deployment.rst b/docs/deployment.rst index c4e1ef3b..e4fc728a 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -10,3 +10,4 @@ This chapter details deployment strategies for the following scenarios. :titlesonly: deployment/central-backup-server + deployment/hosting-repositories diff --git a/docs/deployment/hosting-repositories.rst b/docs/deployment/hosting-repositories.rst new file mode 100644 index 00000000..e502d644 --- /dev/null +++ b/docs/deployment/hosting-repositories.rst @@ -0,0 +1,73 @@ +.. include:: ../global.rst.inc +.. highlight:: none + +Hosting repositories +==================== + +This sections shows how to securely provide repository storage for users. +Optionally, each user can have a storage quota. + +Repositories are accessed through SSH. Each user of the service should +have her own login which is only able to access the user's files. +Technically it would be possible to have multiple users share one login, +however, separating them is better. Separate logins increase isolation +and are thus an additional layer of security and safety for both the +provider and the users. + +For example, if a user manages to breach ``borg serve`` then she can +only damage her own data (assuming that the system does not have further +vulnerabilities). + +Use the standard directory structure of the operating system. Each user +is assigned a home directory and repositories of the user reside in her +home directory. + +The following ``~user/.ssh/authorized_keys`` file is the most important +piece for a correct deployment. It allows the user to login via +their public key (which must be provided by the user), and restricts +SSH access to safe operations only. + +:: + + restrict,command="borg serve --restrict-to-repository /home//repository" + + +.. note:: The text shown above needs to be written on a **single** line! + +.. warning:: + + If this file should be automatically updated (e.g. by a web console), + pay **utmost attention** to sanitizing user input. Strip all whitespace + around the user-supplied key, ensure that it **only** contains ASCII + with no control characters and that it consists of three parts separated + by a single space. Ensure that no newlines are contained within the key. + +The `restrict` keyword enables all restrictions, i.e. disables port, agent +and X11 forwarding, as well as disabling PTY allocation and execution of ~/.ssh/rc. +If any future restriction capabilities are added to authorized_keys +files they will be included in this set. + +The `command` keyword forces execution of the specified command line +upon login. This must be ``borg serve``. The `--restrict-to-repository` +option permits access to exactly **one** repository. It can be given +multiple times to permit access to more than one repository. + +The repository may not exist yet; it can be initialized by the user, +which allows for encryption. + +Storage quotas can be enabled by adding the ``--storage-quota`` option +to the ``borg serve`` command line:: + + restrict,command="borg serve --storage-quota 20G ..." ... + +The storage quotas of repositories are completely independent. If a +client is able to access multiple repositories, each repository +can be filled to the specified quota. + +If storage quotas are used, ensure that all deployed Borg releases +support storage quotas. + +Refer to :ref:`internals_storage_quota` for more details on storage quotas. + +Refer to the `sshd(8) `_ +for more details on securing SSH. From e4247cc0d25f221efdf7447d316dcb022b38b8b7 Mon Sep 17 00:00:00 2001 From: Andrea Gelmini Date: Fri, 9 Jun 2017 16:49:30 +0200 Subject: [PATCH 1015/1387] Fix typos --- docs/changes.rst | 14 +++++++------- docs/installation.rst | 2 +- docs/man/borg-patterns.1 | 2 +- docs/man/borg.1 | 6 +++--- docs/usage/help.rst.inc | 2 +- docs/usage_general.rst.inc | 6 +++--- src/borg/algorithms/crc32_slice_by_8.c | 2 +- src/borg/archiver.py | 4 ++-- src/borg/constants.py | 2 +- src/borg/crypto/nonces.py | 2 +- src/borg/helpers.py | 8 ++++---- src/borg/remote.py | 2 +- src/borg/repository.py | 2 +- 13 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index e321b2c0..f167a116 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -936,7 +936,7 @@ Other changes: - avoid previous_location mismatch, #1741 - due to the changed canonicalization for relative pathes in PR #1711 / #1655 + due to the changed canonicalization for relative paths in PR #1711 / #1655 (implement /./ relpath hack), there would be a changed repo location warning and the user would be asked if this is ok. this would break automation and require manual intervention, which is unwanted. @@ -983,8 +983,8 @@ Bug fixes: (this seems not to get triggered in 1.0.x, but was discovered in master) - hashindex: fix iterators (always raise StopIteration when exhausted) (this seems not to get triggered in 1.0.x, but was discovered in master) -- enable relative pathes in ssh:// repo URLs, via /./relpath hack, #1655 -- allow repo pathes with colons, #1705 +- enable relative paths in ssh:// repo URLs, via /./relpath hack, #1655 +- allow repo paths with colons, #1705 - update changed repo location immediately after acceptance, #1524 - fix debug get-obj / delete-obj crash if object not found and remote repo, #1684 @@ -1063,7 +1063,7 @@ Security fixes: working in e.g. /path/client13 or /path/client1000. As this could accidentally lead to major security/privacy issues depending on - the pathes you use, the behaviour was changed to be a strict directory match. + the paths you use, the behaviour was changed to be a strict directory match. That means --restrict-to-path /path/client1 (with or without trailing slash does not make a difference) now uses /path/client1/ internally (note the trailing slash here!) for matching and allows precisely that path AND any @@ -1071,7 +1071,7 @@ Security fixes: but not /path/client13 or /path/client1000. If you willingly used the undocumented (dangerous) previous behaviour, you - may need to rearrange your --restrict-to-path pathes now. We are sorry if + may need to rearrange your --restrict-to-path paths now. We are sorry if that causes work for you, but we did not want a potentially dangerous behaviour in the software (not even using a for-backwards-compat option). @@ -1151,7 +1151,7 @@ Bug fixes: - fix repo lock deadlocks (related to lock upgrade), #1220 - catch unpacker exceptions, resync, #1351 - fix borg break-lock ignoring BORG_REPO env var, #1324 -- files cache performance fixes (fixes unneccessary re-reading/chunking/ +- files cache performance fixes (fixes unnecessary re-reading/chunking/ hashing of unmodified files for some use cases): - fix unintended file cache eviction, #1430 @@ -1434,7 +1434,7 @@ Bug fixes: - add overflow and range checks for 1st (special) uint32 of the hashindex values, switch from int32 to uint32. - fix so that refcount will never overflow, but just stick to max. value after - a overflow would have occured. + a overflow would have occurred. - borg delete: fix --cache-only for broken caches, #874 Makes --cache-only idempotent: it won't fail if the cache is already deleted. diff --git a/docs/installation.rst b/docs/installation.rst index 8897fc11..c9947f81 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -47,7 +47,7 @@ Repository File System - link - when upgrading an Attic repo in-place - listdir, stat - fsync on files and directories to ensure data is written onto storage media - (some file systems do not support fsync on directories, which Borg accomodates for) + (some file systems do not support fsync on directories, which Borg accommodates for) :ref:`data-structures` contains additional information about how |project_name| manages data. diff --git a/docs/man/borg-patterns.1 b/docs/man/borg-patterns.1 index 0c329d0d..9446b21f 100644 --- a/docs/man/borg-patterns.1 +++ b/docs/man/borg-patterns.1 @@ -166,7 +166,7 @@ the backup roots (starting points) and patterns for inclusion/exclusion. A root path starts with the prefix \fIR\fP, followed by a path (a plain path, not a file pattern). An include rule starts with the prefix +, an exclude rule starts with the prefix \-, both followed by a pattern. -Inclusion patterns are useful to include pathes that are contained in an excluded +Inclusion patterns are useful to include paths that are contained in an excluded path. The first matching pattern is used so if an include pattern matches before an exclude pattern, the file is backed up. .sp diff --git a/docs/man/borg.1 b/docs/man/borg.1 index 7e663cb3..e6d15b0f 100644 --- a/docs/man/borg.1 +++ b/docs/man/borg.1 @@ -205,7 +205,7 @@ Note: you may also prepend a \fBfile://\fP to a filesystem path to get URL style .sp \fBssh://user@host:port/path/to/repo\fP \- same, alternative syntax, port can be given .sp -\fBRemote repositories with relative pathes\fP can be given using this syntax: +\fBRemote repositories with relative paths\fP can be given using this syntax: .sp \fBuser@host:path/to/repo\fP \- path relative to current directory .sp @@ -216,7 +216,7 @@ Note: you may also prepend a \fBfile://\fP to a filesystem path to get URL style Note: giving \fBuser@host:/./path/to/repo\fP or \fBuser@host:/~/path/to/repo\fP or \fBuser@host:/~other/path/to/repo\fP is also supported, but not required here. .sp -\fBRemote repositories with relative pathes, alternative syntax with port\fP: +\fBRemote repositories with relative paths, alternative syntax with port\fP: .sp \fBssh://user@host:port/./path/to/repo\fP \- path relative to current directory .sp @@ -439,7 +439,7 @@ this with some hardware \-\- independent of the software used. We don\(aqt know a list of affected hardware. .sp If you are suspicious whether your Borg repository is still consistent -and readable after one of the failures mentioned above occured, run +and readable after one of the failures mentioned above occurred, run \fBborg check \-\-verify\-data\fP to make sure it is consistent. .SS Units .sp diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index 9082e507..29949d6c 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -118,7 +118,7 @@ the backup roots (starting points) and patterns for inclusion/exclusion. A root path starts with the prefix `R`, followed by a path (a plain path, not a file pattern). An include rule starts with the prefix +, an exclude rule starts with the prefix -, both followed by a pattern. -Inclusion patterns are useful to include pathes that are contained in an excluded +Inclusion patterns are useful to include paths that are contained in an excluded path. The first matching pattern is used so if an include pattern matches before an exclude pattern, the file is backed up. diff --git a/docs/usage_general.rst.inc b/docs/usage_general.rst.inc index 726ac624..03c41d3a 100644 --- a/docs/usage_general.rst.inc +++ b/docs/usage_general.rst.inc @@ -19,7 +19,7 @@ Note: you may also prepend a ``file://`` to a filesystem path to get URL style. ``ssh://user@host:port/path/to/repo`` - same, alternative syntax, port can be given -**Remote repositories with relative pathes** can be given using this syntax: +**Remote repositories with relative paths** can be given using this syntax: ``user@host:path/to/repo`` - path relative to current directory @@ -31,7 +31,7 @@ Note: giving ``user@host:/./path/to/repo`` or ``user@host:/~/path/to/repo`` or ``user@host:/~other/path/to/repo`` is also supported, but not required here. -**Remote repositories with relative pathes, alternative syntax with port**: +**Remote repositories with relative paths, alternative syntax with port**: ``ssh://user@host:port/./path/to/repo`` - path relative to current directory @@ -231,7 +231,7 @@ this with some hardware -- independent of the software used. We don't know a list of affected hardware. If you are suspicious whether your Borg repository is still consistent -and readable after one of the failures mentioned above occured, run +and readable after one of the failures mentioned above occurred, run ``borg check --verify-data`` to make sure it is consistent. Units diff --git a/src/borg/algorithms/crc32_slice_by_8.c b/src/borg/algorithms/crc32_slice_by_8.c index f1efbb37..2924285f 100644 --- a/src/borg/algorithms/crc32_slice_by_8.c +++ b/src/borg/algorithms/crc32_slice_by_8.c @@ -23,7 +23,7 @@ uint32_t crc32_4x8bytes(const void* data, size_t length, uint32_t previousCrc32) /// zlib's CRC32 polynomial const uint32_t Polynomial = 0xEDB88320; -/// swap endianess +/// swap endianness #if defined (__SVR4) && defined (__sun) #include #endif diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 629a9bdd..874dc736 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -245,7 +245,7 @@ class Archiver: 'By default repositories initialized with this version will produce security\n' 'errors if written to with an older version (up to and including Borg 1.0.8).\n' '\n' - 'If you want to use these older versions, you can disable the check by runnning:\n' + 'If you want to use these older versions, you can disable the check by running:\n' 'borg upgrade --disable-tam \'%s\'\n' '\n' 'See https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability ' @@ -1947,7 +1947,7 @@ class Archiver: root path starts with the prefix `R`, followed by a path (a plain path, not a file pattern). An include rule starts with the prefix +, an exclude rule starts with the prefix -, both followed by a pattern. - Inclusion patterns are useful to include pathes that are contained in an excluded + Inclusion patterns are useful to include paths that are contained in an excluded path. The first matching pattern is used so if an include pattern matches before an exclude pattern, the file is backed up. diff --git a/src/borg/constants.py b/src/borg/constants.py index c4cf0a9a..9813a8af 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -15,7 +15,7 @@ ARCHIVE_KEYS = frozenset(['version', 'name', 'items', 'cmdline', 'hostname', 'us # this is the set of keys that are always present in archives: REQUIRED_ARCHIVE_KEYS = frozenset(['version', 'name', 'items', 'cmdline', 'time', ]) -# default umask, overriden by --umask, defaults to read/write only for owner +# default umask, overridden by --umask, defaults to read/write only for owner UMASK_DEFAULT = 0o077 CACHE_TAG_NAME = 'CACHEDIR.TAG' diff --git a/src/borg/crypto/nonces.py b/src/borg/crypto/nonces.py index 7145d5e0..ec4700ac 100644 --- a/src/borg/crypto/nonces.py +++ b/src/borg/crypto/nonces.py @@ -50,7 +50,7 @@ class NonceManager: def ensure_reservation(self, nonce_space_needed): # Nonces may never repeat, even if a transaction aborts or the system crashes. # Therefore a part of the nonce space is reserved before any nonce is used for encryption. - # As these reservations are commited to permanent storage before any nonce is used, this protects + # As these reservations are committed to permanent storage before any nonce is used, this protects # against nonce reuse in crashes and transaction aborts. In that case the reservation still # persists and the whole reserved space is never reused. # diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 62d0f5ba..c93e5c0a 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -46,7 +46,7 @@ from .constants import * # NOQA ''' The global exit_code variable is used so that modules other than archiver can increase the program exit code if a -warning or error occured during their operation. This is different from archiver.exit_code, which is only accessible +warning or error occurred during their operation. This is different from archiver.exit_code, which is only accessible from the archiver object. ''' exit_code = EXIT_SUCCESS @@ -145,7 +145,7 @@ ArchiveInfo = namedtuple('ArchiveInfo', 'name id ts') class Archives(abc.MutableMapping): """ Nice wrapper around the archives dict, making sure only valid types/values get in - and we can deal with str keys (and it internally encodes to byte keys) and eiter + and we can deal with str keys (and it internally encodes to byte keys) and either str timestamps or datetime timestamps. """ def __init__(self): @@ -830,7 +830,7 @@ class Location: (?Pfile):// # file:// """ + file_path_re + optional_archive_re, re.VERBOSE) # servername/path, path or path::archive - # note: scp_re is also use for local pathes + # note: scp_re is also use for local paths scp_re = re.compile(r""" ( """ + optional_user_re + r""" # user@ (optional) @@ -979,7 +979,7 @@ def decode_dict(d, keys, encoding='utf-8', errors='surrogateescape'): def prepare_dump_dict(d): def decode_bytes(value): - # this should somehow be reversable later, but usual strings should + # this should somehow be reversible later, but usual strings should # look nice and chunk ids should mostly show in hex. Use a special # inband signaling character (ASCII DEL) to distinguish between # decoded and hex mode. diff --git a/src/borg/remote.py b/src/borg/remote.py index 6ce6c3d0..8c8d2126 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -354,7 +354,7 @@ class RepositoryServer: # pragma: no cover path_with_sep = os.path.join(path, '') # make sure there is a trailing slash (os.sep) if self.restrict_to_paths: # if --restrict-to-path P is given, we make sure that we only operate in/below path P. - # for the prefix check, it is important that the compared pathes both have trailing slashes, + # for the prefix check, it is important that the compared paths both have trailing slashes, # so that a path /foobar will NOT be accepted with --restrict-to-path /foo option. for restrict_to_path in self.restrict_to_paths: restrict_to_path_with_sep = os.path.join(os.path.realpath(restrict_to_path), '') # trailing slash diff --git a/src/borg/repository.py b/src/borg/repository.py index a5806373..d15d51b1 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -1222,7 +1222,7 @@ class LoggedIO: return fd def close_segment(self): - # set self._write_fd to None early to guard against reentry from error handling code pathes: + # set self._write_fd to None early to guard against reentry from error handling code paths: fd, self._write_fd = self._write_fd, None if fd is not None: self.segment += 1 From fd0250d34a63b4a1c04a909acd75f546e661c5c6 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Mon, 6 Feb 2017 23:19:02 +0100 Subject: [PATCH 1016/1387] Add minimal version of in repository mandatory feature flags. This should allow us to make sure older borg versions can be cleanly prevented from doing operations that are no longer safe because of repository format evolution. This allows more fine grained control than just incrementing the manifest version. So for example a change that still allows new archives to be created but would corrupt the repository when an old version tries to delete an archive or check the repository would add the new feature to the check and delete set but leave it out of the write set. This is somewhat inspired by ext{2,3,4} which uses sets for compat (everything except fsck), ro-compat (may only be accessed read-only by older versions) and features (refuse all access). --- src/borg/archive.py | 2 +- src/borg/archiver.py | 60 +++++++++++++++++++++------------- src/borg/helpers.py | 50 +++++++++++++++++++++++++++- src/borg/testsuite/archiver.py | 22 ++++++------- 4 files changed, 98 insertions(+), 36 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 1c0b4b3f..f375015d 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1152,7 +1152,7 @@ class ArchiveChecker: self.manifest = self.rebuild_manifest() else: try: - self.manifest, _ = Manifest.load(repository, key=self.key) + self.manifest, _ = Manifest.load(repository, (Manifest.Operation.CHECK,), key=self.key) except IntegrityError as exc: logger.error('Repository manifest is corrupted: %s', exc) self.error_found = True diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 874dc736..e3cadbad 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -87,8 +87,9 @@ def argument(args, str_or_bool): return str_or_bool -def with_repository(fake=False, invert_fake=False, create=False, lock=True, exclusive=False, manifest=True, cache=False, - secure=True): +def with_repository(fake=False, invert_fake=False, create=False, lock=True, + exclusive=False, manifest=True, cache=False, secure=True, + compatibility=None): """ Method decorator for subcommand-handling methods: do_XYZ(self, args, repository, …) @@ -100,7 +101,20 @@ def with_repository(fake=False, invert_fake=False, create=False, lock=True, excl :param manifest: load manifest and key, pass them as keyword arguments :param cache: open cache, pass it as keyword argument (implies manifest) :param secure: do assert_secure after loading manifest + :param compatibility: mandatory if not create and (manifest or cache), specifies mandatory feature categories to check """ + + if not create and (manifest or cache): + if compatibility is None: + raise AssertionError("with_repository decorator used without compatibility argument") + if type(compatibility) is not tuple: + raise AssertionError("with_repository decorator compatibility argument must be of type tuple") + else: + if compatibility is not None: + raise AssertionError("with_repository called with compatibility argument but would not check" + repr(compatibility)) + if create: + compatibility = Manifest.NO_OPERATION_CHECK + def decorator(method): @functools.wraps(method) def wrapper(self, args, **kwargs): @@ -117,7 +131,7 @@ def with_repository(fake=False, invert_fake=False, create=False, lock=True, excl append_only=append_only) with repository: if manifest or cache: - kwargs['manifest'], kwargs['key'] = Manifest.load(repository) + kwargs['manifest'], kwargs['key'] = Manifest.load(repository, compatibility) if 'compression' in args: kwargs['key'].compressor = args.compression.compressor if secure: @@ -276,7 +290,7 @@ class Archiver: return EXIT_WARNING return EXIT_SUCCESS - @with_repository() + @with_repository(compatibility=(Manifest.Operation.CHECK,)) def do_change_passphrase(self, args, repository, manifest, key): """Change repository key file passphrase""" if not hasattr(key, 'change_passphrase'): @@ -413,7 +427,7 @@ class Archiver: print(fmt % ('U', msg, total_size_MB / dt_update, count, file_size_formatted, content, dt_update)) print(fmt % ('D', msg, total_size_MB / dt_delete, count, file_size_formatted, content, dt_delete)) - @with_repository(fake='dry_run', exclusive=True) + @with_repository(fake='dry_run', exclusive=True, compatibility=(Manifest.Operation.WRITE,)) def do_create(self, args, repository, manifest=None, key=None): """Create new archive""" matcher = PatternMatcher(fallback=True) @@ -632,7 +646,7 @@ class Archiver: return matched return item_filter - @with_repository() + @with_repository(compatibility=(Manifest.Operation.READ,)) @with_archive def do_extract(self, args, repository, manifest, key, archive): """Extract archive contents""" @@ -714,7 +728,7 @@ class Archiver: pi.finish() return self.exit_code - @with_repository() + @with_repository(compatibility=(Manifest.Operation.READ,)) @with_archive def do_export_tar(self, args, repository, manifest, key, archive): """Export archive contents as a tarball""" @@ -927,7 +941,7 @@ class Archiver: self.print_warning("Include pattern '%s' never matched.", pattern) return self.exit_code - @with_repository() + @with_repository(compatibility=(Manifest.Operation.READ,)) @with_archive def do_diff(self, args, repository, manifest, key, archive): """Diff contents of two archives""" @@ -1140,7 +1154,7 @@ class Archiver: return self.exit_code - @with_repository(exclusive=True, cache=True) + @with_repository(exclusive=True, cache=True, compatibility=(Manifest.Operation.CHECK,)) @with_archive def do_rename(self, args, repository, manifest, key, cache, archive): """Rename an existing archive""" @@ -1161,7 +1175,7 @@ class Archiver: def _delete_archives(self, args, repository): """Delete archives""" - manifest, key = Manifest.load(repository) + manifest, key = Manifest.load(repository, (Manifest.Operation.DELETE,)) if args.location.archive: archive_names = (args.location.archive,) @@ -1219,7 +1233,7 @@ class Archiver: if not args.cache_only: msg = [] try: - manifest, key = Manifest.load(repository) + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) except NoManifestError: msg.append("You requested to completely DELETE the repository *including* all archives it may " "contain.") @@ -1258,7 +1272,7 @@ class Archiver: return self._do_mount(args) - @with_repository() + @with_repository(compatibility=(Manifest.Operation.READ,)) def _do_mount(self, args, repository, manifest, key): from .fuse import FuseOperations @@ -1276,7 +1290,7 @@ class Archiver: """un-mount the FUSE filesystem""" return umount(args.mountpoint) - @with_repository() + @with_repository(compatibility=(Manifest.Operation.READ,)) def do_list(self, args, repository, manifest, key): """List archive or repository contents""" if not hasattr(sys.stdout, 'buffer'): @@ -1348,7 +1362,7 @@ class Archiver: return self.exit_code - @with_repository(cache=True) + @with_repository(cache=True, compatibility=(Manifest.Operation.READ,)) def do_info(self, args, repository, manifest, key, cache): """Show archive details such as disk space used""" if any((args.location.archive, args.first, args.last, args.prefix)): @@ -1438,7 +1452,7 @@ class Archiver: print(str(cache)) return self.exit_code - @with_repository(exclusive=True) + @with_repository(exclusive=True, compatibility=(Manifest.Operation.DELETE,)) def do_prune(self, args, repository, manifest, key): """Prune repository archives according to specified rules""" if not any((args.secondly, args.minutely, args.hourly, args.daily, @@ -1517,7 +1531,7 @@ class Archiver: def do_upgrade(self, args, repository, manifest=None, key=None): """upgrade a repository from a previous version""" if args.tam: - manifest, key = Manifest.load(repository, force_tam_not_required=args.force) + manifest, key = Manifest.load(repository, (Manifest.Operation.CHECK,), force_tam_not_required=args.force) if not hasattr(key, 'change_passphrase'): print('This repository is not encrypted, cannot enable TAM.') @@ -1542,7 +1556,7 @@ class Archiver: open(tam_file, 'w').close() print('Updated security database') elif args.disable_tam: - manifest, key = Manifest.load(repository, force_tam_not_required=True) + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK, force_tam_not_required=True) if tam_required(repository): os.unlink(tam_required_file(repository)) if key.tam_required: @@ -1570,7 +1584,7 @@ class Archiver: print("warning: %s" % e) return self.exit_code - @with_repository(cache=True, exclusive=True) + @with_repository(cache=True, exclusive=True, compatibility=(Manifest.Operation.CHECK,)) def do_recreate(self, args, repository, manifest, key, cache): """Re-create archives""" msg = ("recreate is an experimental feature.\n" @@ -1647,7 +1661,7 @@ class Archiver: print('Process ID:', get_process_id()) return EXIT_SUCCESS - @with_repository() + @with_repository(compatibility=Manifest.NO_OPERATION_CHECK) def do_debug_dump_archive_items(self, args, repository, manifest, key): """dump (decrypted, decompressed) archive items metadata (not: data)""" archive = Archive(repository, key, manifest, args.location.archive, @@ -1661,7 +1675,7 @@ class Archiver: print('Done.') return EXIT_SUCCESS - @with_repository() + @with_repository(compatibility=Manifest.NO_OPERATION_CHECK) def do_debug_dump_archive(self, args, repository, manifest, key): """dump decoded archive metadata (not: data)""" @@ -1714,7 +1728,7 @@ class Archiver: output(fd) return EXIT_SUCCESS - @with_repository() + @with_repository(compatibility=Manifest.NO_OPERATION_CHECK) def do_debug_dump_manifest(self, args, repository, manifest, key): """dump decoded repository manifest""" @@ -1729,7 +1743,7 @@ class Archiver: json.dump(meta, fd, indent=4) return EXIT_SUCCESS - @with_repository() + @with_repository(compatibility=Manifest.NO_OPERATION_CHECK) def do_debug_dump_repo_objs(self, args, repository, manifest, key): """dump (decrypted, decompressed) repo objects""" marker = None @@ -1803,7 +1817,7 @@ class Archiver: print('Done.') return EXIT_SUCCESS - @with_repository(manifest=False, exclusive=True, cache=True) + @with_repository(manifest=False, exclusive=True, cache=True, compatibility=Manifest.NO_OPERATION_CHECK) def do_debug_refcount_obj(self, args, repository, manifest, key, cache): """display refcounts for the objects with the given IDs""" for hex_id in args.ids: diff --git a/src/borg/helpers.py b/src/borg/helpers.py index c93e5c0a..bfef9859 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1,6 +1,7 @@ import argparse import contextlib import collections +import enum import grp import hashlib import logging @@ -123,6 +124,10 @@ def check_python(): raise PythonLibcTooOld +class MandatoryFeatureUnsupported(Error): + """Unsupported repository feature(s) {}. A newer version of borg is required to access this repository.""" + + def check_extension_modules(): from . import platform, compress, item if hashindex.API_VERSION != '1.1_01': @@ -222,6 +227,34 @@ class Archives(abc.MutableMapping): class Manifest: + @enum.unique + class Operation(enum.Enum): + # The comments here only roughly describe the scope of each feature. In the end, additions need to be + # based on potential problems older clients could produce when accessing newer repositories and the + # tradeofs of locking version out or still allowing access. As all older versions and their exact + # behaviours are known when introducing new features sometimes this might not match the general descriptions + # below. + + # The READ operation describes which features are needed to safely list and extract the archives in the + # repository. + READ = 'read' + # The CHECK operation is for all operations that need either to understand every detail + # of the repository (for consistency checks and repairs) or are seldom used functions that just + # should use the most restrictive feature set because more fine grained compatibility tracking is + # not needed. + CHECK = 'check' + # The WRITE operation is for adding archives. Features here ensure that older clients don't add archives + # in an old format, or is used to lock out clients that for other reasons can no longer safely add new + # archives. + WRITE = 'write' + # The DELETE operation is for all operations (like archive deletion) that need a 100% correct reference + # count and the need to be able to find all (directly and indirectly) referenced chunks of a given archive. + DELETE = 'delete' + + NO_OPERATION_CHECK = tuple() + + SUPPORTED_REPO_FEATURES = frozenset([]) + MANIFEST_ID = b'\0' * 32 def __init__(self, key, repository, item_keys=None): @@ -242,7 +275,7 @@ class Manifest: return datetime.strptime(self.timestamp, "%Y-%m-%dT%H:%M:%S.%f") @classmethod - def load(cls, repository, key=None, force_tam_not_required=False): + def load(cls, repository, operations, key=None, force_tam_not_required=False): from .item import ManifestItem from .crypto.key import key_factory, tam_required_file, tam_required from .repository import Repository @@ -275,8 +308,23 @@ class Manifest: if not manifest_required and security_required: logger.debug('Manifest is TAM verified and says TAM is *not* required, updating security database...') os.unlink(tam_required_file(repository)) + manifest.check_repository_compatibility(operations) return manifest, key + def check_repository_compatibility(self, operations): + for operation in operations: + assert isinstance(operation, self.Operation) + feature_flags = self.config.get(b'feature_flags', None) + if feature_flags is None: + return + if operation.value.encode() not in feature_flags: + continue + requirements = feature_flags[operation.value.encode()] + if b'mandatory' in requirements: + unsupported = set(requirements[b'mandatory']) - self.SUPPORTED_REPO_FEATURES + if unsupported: + raise MandatoryFeatureUnsupported([f.decode() for f in unsupported]) + def write(self): from .item import ManifestItem if self.key.tam_required: diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index c1ec2b18..76b020b5 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -289,7 +289,7 @@ class ArchiverTestCaseBase(BaseTestCase): def open_archive(self, name): repository = Repository(self.repository_path, exclusive=True) with repository: - manifest, key = Manifest.load(repository) + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) archive = Archive(repository, key, manifest, name) return archive, repository @@ -1248,7 +1248,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('extract', '--dry-run', self.repository_location + '::test.4') # Make sure both archives have been renamed with Repository(self.repository_path) as repository: - manifest, key = Manifest.load(repository) + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) self.assert_equal(len(manifest.archives), 2) self.assert_in('test.3', manifest.archives) self.assert_in('test.4', manifest.archives) @@ -1349,7 +1349,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', '--encryption=none', self.repository_location) self.create_src_archive('test') with Repository(self.repository_path, exclusive=True) as repository: - manifest, key = Manifest.load(repository) + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) archive = Archive(repository, key, manifest, 'test') for item in archive.iter_items(): if 'chunks' in item: @@ -1367,7 +1367,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', '--encryption=none', self.repository_location) self.create_src_archive('test') with Repository(self.repository_path, exclusive=True) as repository: - manifest, key = Manifest.load(repository) + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) archive = Archive(repository, key, manifest, 'test') id = archive.metadata.items[0] repository.put(id, b'corrupted items metadata stream chunk') @@ -1417,7 +1417,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('create', '--dry-run', self.repository_location + '::test', 'input') # Make sure no archive has been created with Repository(self.repository_path) as repository: - manifest, key = Manifest.load(repository) + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) self.assert_equal(len(manifest.archives), 0) def test_progress_on(self): @@ -2091,7 +2091,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('check', self.repository_location) # Then check that the cache on disk matches exactly what's in the repo. with self.open_repository() as repository: - manifest, key = Manifest.load(repository) + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) with Cache(repository, key, manifest, sync=False) as cache: original_chunks = cache.chunks cache.destroy(repository) @@ -2112,7 +2112,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') with self.open_repository() as repository: - manifest, key = Manifest.load(repository) + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) with Cache(repository, key, manifest, sync=False) as cache: cache.begin_txn() cache.chunks.incref(list(cache.chunks.iteritems())[0][0]) @@ -2750,7 +2750,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): archive, repository = self.open_archive('archive1') with repository: - manifest, key = Manifest.load(repository) + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) with Cache(repository, key, manifest) as cache: archive = Archive(repository, key, manifest, '0.13', cache=cache, create=True) archive.items_buffer.add(Attic013Item) @@ -2762,7 +2762,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): class ManifestAuthenticationTest(ArchiverTestCaseBase): def spoof_manifest(self, repository): with repository: - _, key = Manifest.load(repository) + _, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({ 'version': 1, 'archives': {}, @@ -2775,7 +2775,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): self.cmd('init', '--encryption=repokey', self.repository_location) repository = Repository(self.repository_path, exclusive=True) with repository: - manifest, key = Manifest.load(repository) + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({ 'version': 1, 'archives': {}, @@ -2792,7 +2792,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): repository = Repository(self.repository_path, exclusive=True) with repository: shutil.rmtree(get_security_dir(bin_to_hex(repository.id))) - _, key = Manifest.load(repository) + _, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) key.tam_required = False key.change_passphrase(key._passphrase) From 193e8bcaefbe45cff2bd5f3448f926b6930e5659 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 12 Mar 2017 14:12:04 +0100 Subject: [PATCH 1017/1387] Add tests for mandatory repository feature flags. --- src/borg/testsuite/archiver.py | 63 +++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 76b020b5..85f79d71 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -41,7 +41,7 @@ from ..crypto.key import KeyfileKeyBase, RepoKey, KeyfileKey, Passphrase, TAMReq from ..crypto.keymanager import RepoIdMismatch, NotABorgKeyFile from ..crypto.file_integrity import FileIntegrityError from ..helpers import Location, get_security_dir -from ..helpers import Manifest +from ..helpers import Manifest, MandatoryFeatureUnsupported from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from ..helpers import bin_to_hex from ..helpers import MAX_S @@ -1420,6 +1420,67 @@ class ArchiverTestCase(ArchiverTestCaseBase): manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) self.assert_equal(len(manifest.archives), 0) + def add_unknown_feature(self, operation): + with Repository(self.repository_path, exclusive=True) as repository: + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) + manifest.config[b'feature_flags'] = {operation.value.encode(): {b'mandatory': [b'unknown-feature']}} + manifest.write() + repository.commit() + + def cmd_raises_unknown_feature(self, args): + if self.FORK_DEFAULT: + self.cmd(*args, exit_code=EXIT_ERROR) + else: + with pytest.raises(MandatoryFeatureUnsupported) as excinfo: + self.cmd(*args) + assert excinfo.value.args == (['unknown-feature'],) + + def test_unknown_feature_on_create(self): + print(self.cmd('init', '--encryption=repokey', self.repository_location)) + self.add_unknown_feature(Manifest.Operation.WRITE) + self.cmd_raises_unknown_feature(['create', self.repository_location + '::test', 'input']) + + def test_unknown_feature_on_change_passphrase(self): + print(self.cmd('init', '--encryption=repokey', self.repository_location)) + self.add_unknown_feature(Manifest.Operation.CHECK) + self.cmd_raises_unknown_feature(['change-passphrase', self.repository_location]) + + def test_unknown_feature_on_read(self): + print(self.cmd('init', '--encryption=repokey', self.repository_location)) + self.cmd('create', self.repository_location + '::test', 'input') + self.add_unknown_feature(Manifest.Operation.READ) + with changedir('output'): + self.cmd_raises_unknown_feature(['extract', self.repository_location + '::test']) + + self.cmd_raises_unknown_feature(['list', self.repository_location]) + self.cmd_raises_unknown_feature(['info', self.repository_location + '::test']) + + def test_unknown_feature_on_rename(self): + print(self.cmd('init', '--encryption=repokey', self.repository_location)) + self.cmd('create', self.repository_location + '::test', 'input') + self.add_unknown_feature(Manifest.Operation.CHECK) + self.cmd_raises_unknown_feature(['rename', self.repository_location + '::test', 'other']) + + def test_unknown_feature_on_delete(self): + print(self.cmd('init', '--encryption=repokey', self.repository_location)) + self.cmd('create', self.repository_location + '::test', 'input') + self.add_unknown_feature(Manifest.Operation.DELETE) + # delete of an archive raises + self.cmd_raises_unknown_feature(['delete', self.repository_location + '::test']) + self.cmd_raises_unknown_feature(['prune', '--keep-daily=3', self.repository_location]) + # delete of the whole repository ignores features + self.cmd('delete', self.repository_location) + + @unittest.skipUnless(has_llfuse, 'llfuse not installed') + def test_unknown_feature_on_mount(self): + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + self.add_unknown_feature(Manifest.Operation.READ) + mountpoint = os.path.join(self.tmpdir, 'mountpoint') + os.mkdir(mountpoint) + # XXX this might hang if it doesn't raise an error + self.cmd_raises_unknown_feature(['mount', self.repository_location + '::test', mountpoint]) + def test_progress_on(self): self.create_regular_file('file1', size=1024 * 80) self.cmd('init', '--encryption=repokey', self.repository_location) From 1176a1c4e47a562797d5a04f1b16feba85537804 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 12 Mar 2017 11:04:26 +0100 Subject: [PATCH 1018/1387] permit manifest version 2 as well 1 one. --- src/borg/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index bfef9859..d0246bdd 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -290,7 +290,7 @@ class Manifest: manifest_dict, manifest.tam_verified = key.unpack_and_verify_manifest(data, force_tam_not_required=force_tam_not_required) m = ManifestItem(internal_dict=manifest_dict) manifest.id = key.id_hash(data) - if m.get('version') != 1: + if m.get('version') not in (1, 2): raise ValueError('Invalid manifest version') manifest.archives.set_raw_dict(m.archives) manifest.timestamp = m.get('timestamp') From 310a71e4f018116d160c3a226648d33c15d35268 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 10 Jun 2017 09:56:41 +0200 Subject: [PATCH 1019/1387] cache sync: use ro_buffer to accept bytes, memoryview, ... --- src/borg/hashindex.pyx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 33868344..90db35d8 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -7,6 +7,7 @@ cimport cython from libc.stdint cimport uint32_t, UINT32_MAX, uint64_t from libc.errno cimport errno from cpython.exc cimport PyErr_SetFromErrnoWithFilename +from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release API_VERSION = '1.1_02' @@ -386,6 +387,12 @@ cdef class ChunkKeyIterator: return (self.key)[:self.key_size], ChunkIndexEntry(refcount, _le32toh(value[1]), _le32toh(value[2])) +cdef Py_buffer ro_buffer(object data) except *: + cdef Py_buffer view + PyObject_GetBuffer(data, &view, PyBUF_SIMPLE) + return view + + cdef class CacheSynchronizer: cdef ChunkIndex chunks cdef CacheSyncCtx *sync @@ -401,7 +408,11 @@ cdef class CacheSynchronizer: cache_sync_free(self.sync) def feed(self, chunk): - if not cache_sync_feed(self.sync, chunk, len(chunk)): + cdef Py_buffer chunk_buf = ro_buffer(chunk) + cdef int rc + rc = cache_sync_feed(self.sync, chunk_buf.buf, chunk_buf.len) + PyBuffer_Release(&chunk_buf) + if not rc: error = cache_sync_error(self.sync) if error != NULL: raise ValueError('cache_sync_feed failed: ' + error.decode('ascii')) From 5eb43b84640c8fcb9dd0347bf0177f185e2c4511 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 10 Jun 2017 10:05:43 +0200 Subject: [PATCH 1020/1387] cache sync: give overview of the source's structure --- src/borg/cache_sync/cache_sync.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/borg/cache_sync/cache_sync.c b/src/borg/cache_sync/cache_sync.c index e0c2e0fb..e4bc653b 100644 --- a/src/borg/cache_sync/cache_sync.c +++ b/src/borg/cache_sync/cache_sync.c @@ -1,3 +1,20 @@ +/* + * Borg cache synchronizer, + * high level interface. + * + * These routines parse msgpacked item metadata and update a HashIndex + * with all chunks that are referenced from the items. + * + * This file only contains some initialization and buffer management. + * + * The parser is split in two parts, somewhat similar to lexer/parser combinations: + * + * unpack_template.h munches msgpack and calls a specific callback for each object + * encountered (e.g. beginning of a map, an integer, a string, a map item etc.). + * + * unpack.h implements these callbacks and uses another state machine to + * extract chunk references from it. + */ #include "unpack.h" From 0e31f78dd6e29549899290eb2b05bf7bd158b8d7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 10 Jun 2017 10:12:06 +0200 Subject: [PATCH 1021/1387] cache sync: avoid "l" and such as a variable name, note vanilla changes --- src/borg/cache_sync/unpack.h | 13 ++++++------- src/borg/cache_sync/unpack_template.h | 5 +++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/borg/cache_sync/unpack.h b/src/borg/cache_sync/unpack.h index d878dda7..94172d42 100644 --- a/src/borg/cache_sync/unpack.h +++ b/src/borg/cache_sync/unpack.h @@ -288,7 +288,6 @@ static inline int unpack_callback_array_end(unpack_user* u) return 0; } } - return 0; } @@ -317,6 +316,7 @@ static inline int unpack_callback_map(unpack_user* u, unsigned int n) static inline int unpack_callback_map_item(unpack_user* u, unsigned int current) { (void)u; (void)current; + if(u->level == 1) { switch(u->expect) { case expect_map_item_end: @@ -340,7 +340,7 @@ static inline int unpack_callback_map_end(unpack_user* u) return 0; } -static inline int unpack_callback_raw(unpack_user* u, const char* b, const char* p, unsigned int l) +static inline int unpack_callback_raw(unpack_user* u, const char* b, const char* p, unsigned int length) { /* raw = what Borg uses for binary stuff and strings as well */ /* Note: p points to an internal buffer which contains l bytes. */ @@ -348,7 +348,7 @@ static inline int unpack_callback_raw(unpack_user* u, const char* b, const char* switch(u->expect) { case expect_key: - if(l != 32) { + if(length != 32) { SET_LAST_ERROR("Incorrect key length"); return -1; } @@ -356,7 +356,7 @@ static inline int unpack_callback_raw(unpack_user* u, const char* b, const char* u->expect = expect_size; break; case expect_chunks_map_key: - if(l == 6 && !memcmp("chunks", p, 6)) { + if(length == 6 && !memcmp("chunks", p, 6)) { u->expect = expect_chunks_begin; u->inside_chunks = 1; } else { @@ -369,13 +369,12 @@ static inline int unpack_callback_raw(unpack_user* u, const char* b, const char* return -1; } } - return 0; } -static inline int unpack_callback_bin(unpack_user* u, const char* b, const char* p, unsigned int l) +static inline int unpack_callback_bin(unpack_user* u, const char* b, const char* p, unsigned int length) { - (void)u; (void)b; (void)p; (void)l; + (void)u; (void)b; (void)p; (void)length; UNEXPECTED("bin"); return 0; } diff --git a/src/borg/cache_sync/unpack_template.h b/src/borg/cache_sync/unpack_template.h index 1ac26274..a6492da3 100644 --- a/src/borg/cache_sync/unpack_template.h +++ b/src/borg/cache_sync/unpack_template.h @@ -15,6 +15,11 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * + * + * This has been slightly adapted from the vanilla msgpack-{c, python} version. + * Since cache_sync does not intend to build an output data structure, + * msgpack_unpack_object and all of its uses was removed. */ #ifndef USE_CASE_RANGE From 827c478500390dc0a91f1f2675f416e5d3fb5efc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Jun 2017 19:22:18 +0200 Subject: [PATCH 1022/1387] nanorst for "borg help TOPIC" --- docs/usage/benchmark_crud.rst.inc | 2 +- docs/usage/help.rst.inc | 8 +-- docs/usage/recreate.rst.inc | 2 +- src/borg/archiver.py | 21 ++++---- src/borg/nanorst.py | 84 +++++++++++++++++++++++-------- src/borg/testsuite/archiver.py | 7 ++- src/borg/testsuite/nanorst.py | 4 ++ 7 files changed, 88 insertions(+), 40 deletions(-) diff --git a/docs/usage/benchmark_crud.rst.inc b/docs/usage/benchmark_crud.rst.inc index 58d40970..b692cae1 100644 --- a/docs/usage/benchmark_crud.rst.inc +++ b/docs/usage/benchmark_crud.rst.inc @@ -24,7 +24,7 @@ This command benchmarks borg CRUD (create, read, update, delete) operations. It creates input data below the given PATH and backups this data into the given REPO. The REPO must already exist (it could be a fresh empty repo or an existing repo, the -command will create / read / update / delete some archives named borg-test-data* there. +command will create / read / update / delete some archives named borg-test-data\* there. Make sure you have free space there, you'll need about 1GB each (+ overhead). diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index 29949d6c..22e8b0b9 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -8,8 +8,10 @@ borg help patterns File patterns support these styles: fnmatch, shell, regular expressions, path prefixes and path full-matches. By default, fnmatch is used for -`--exclude` patterns and shell-style is used for `--pattern`. If followed -by a colon (':') the first two characters of a pattern are used as a +`--exclude` patterns and shell-style is used for the experimental `--pattern` +option. + +If followed by a colon (':') the first two characters of a pattern are used as a style selector. Explicit style selection is necessary when a non-default style is desired or when the desired pattern starts with two alphanumeric characters followed by a colon (i.e. `aa:something/*`). @@ -17,7 +19,7 @@ two alphanumeric characters followed by a colon (i.e. `aa:something/*`). `Fnmatch `_, selector `fm:` This is the default style for --exclude and --exclude-from. - These patterns use a variant of shell pattern syntax, with '*' matching + These patterns use a variant of shell pattern syntax, with '\*' matching any number of characters, '?' matching any single character, '[...]' matching any single character specified, including ranges, and '[!...]' matching any character not specified. For the purpose of these patterns, diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index 7069423b..507be1d9 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -82,7 +82,7 @@ There is no risk of data loss by this. used to have upgraded Borg 0.xx or Attic archives deduplicate with Borg 1.x archives. -USE WITH CAUTION. +**USE WITH CAUTION.** Depending on the PATHs and patterns given, recreate can be used to permanently delete files from archives. When in doubt, use "--dry-run --verbose --list" to see how patterns/PATHS are diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 874dc736..56f762f2 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -64,7 +64,7 @@ from .helpers import basic_json_data, json_print from .helpers import replace_placeholders from .helpers import ChunkIteratorFileWrapper from .helpers import popen_with_error_handling -from .nanorst import RstToTextLazy, ansi_escapes +from .nanorst import rst_to_terminal from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern from .patterns import PatternMatcher from .item import Item @@ -1837,8 +1837,10 @@ class Archiver: helptext['patterns'] = textwrap.dedent(''' File patterns support these styles: fnmatch, shell, regular expressions, path prefixes and path full-matches. By default, fnmatch is used for - `--exclude` patterns and shell-style is used for `--pattern`. If followed - by a colon (':') the first two characters of a pattern are used as a + `--exclude` patterns and shell-style is used for the experimental `--pattern` + option. + + If followed by a colon (':') the first two characters of a pattern are used as a style selector. Explicit style selection is necessary when a non-default style is desired or when the desired pattern starts with two alphanumeric characters followed by a colon (i.e. `aa:something/*`). @@ -1846,7 +1848,7 @@ class Archiver: `Fnmatch `_, selector `fm:` This is the default style for --exclude and --exclude-from. - These patterns use a variant of shell pattern syntax, with '*' matching + These patterns use a variant of shell pattern syntax, with '\*' matching any number of characters, '?' matching any single character, '[...]' matching any single character specified, including ranges, and '[!...]' matching any character not specified. For the purpose of these patterns, @@ -1857,7 +1859,7 @@ class Archiver: must match from the start to just before a path separator. Except for the root path, paths will never end in the path separator when matching is attempted. Thus, if a given pattern ends in a path - separator, a '*' is appended before matching is attempted. + separator, a '\*' is appended before matching is attempted. Shell-style patterns, selector `sh:` @@ -2099,7 +2101,7 @@ class Archiver: if not args.topic: parser.print_help() elif args.topic in self.helptext: - print(self.helptext[args.topic]) + print(rst_to_terminal(self.helptext[args.topic])) elif args.topic in commands: if args.epilog_only: print(commands[args.topic].epilog) @@ -2257,11 +2259,6 @@ class Archiver: setattr(args, dest, option_value) def build_parser(self): - if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() and (sys.platform != 'win32' or 'ANSICON' in os.environ): - rst_state_hook = ansi_escapes - else: - rst_state_hook = None - # You can use :ref:`xyz` in the following usage pages. However, for plain-text view, # e.g. through "borg ... --help", define a substitution for the reference here. # It will replace the entire :ref:`foo` verbatim. @@ -2279,7 +2276,7 @@ class Archiver: epilog = [line for line in epilog if not line.startswith('.. man')] epilog = '\n'.join(epilog) if mode == 'command-line': - epilog = RstToTextLazy(epilog, rst_state_hook, rst_plain_text_references) + epilog = rst_to_terminal(epilog, rst_plain_text_references) return epilog def define_common_options(add_common_option): diff --git a/src/borg/nanorst.py b/src/borg/nanorst.py index 3056a0ff..113a86a1 100644 --- a/src/borg/nanorst.py +++ b/src/borg/nanorst.py @@ -1,5 +1,6 @@ - import io +import os +import sys class TextPecker: @@ -31,6 +32,21 @@ class TextPecker: return out +def process_directive(directive, arguments, out, state_hook): + if directive == 'container' and arguments == 'experimental': + state_hook('text', '**', out) + out.write('++ Experimental ++') + state_hook('**', 'text', out) + else: + state_hook('text', '**', out) + out.write(directive.title()) + out.write(':\n') + state_hook('**', 'text', out) + if arguments: + out.write(arguments) + out.write('\n') + + def rst_to_text(text, state_hook=None, references=None): """ Convert rST to a more human text form. @@ -54,8 +70,10 @@ def rst_to_text(text, state_hook=None, references=None): next = text.peek(1) # type: str if state == 'text': + if char == '\\' and text.peek(1) in inline_single: + continue if text.peek(-1) != '\\': - if char in inline_single and next not in inline_single: + if char in inline_single and next != char: state_hook(state, char, out) state = char continue @@ -88,21 +106,19 @@ def rst_to_text(text, state_hook=None, references=None): raise ValueError("Undefined reference in Archiver help: %r — please add reference substitution" "to 'rst_plain_text_references'" % ref) continue + if char == ':' and text.peek(2) == ':\n': # End of line code block + text.read(2) + state_hook(state, 'code-block', out) + state = 'code-block' + out.write(':\n') + continue if text.peek(-2) in ('\n\n', '') and char == next == '.': text.read(2) - try: - directive, arguments = text.peekline().split('::', maxsplit=1) - except ValueError: - directive = None - text.readline() + directive, is_directive, arguments = text.readline().partition('::') text.read(1) - if not directive: + if not is_directive: continue - out.write(directive.title()) - out.write(':\n') - if arguments: - out.write(arguments) - out.write('\n') + process_directive(directive, arguments.strip(), out, state_hook) continue if state in inline_single and char == state: state_hook(state, 'text', out) @@ -118,21 +134,22 @@ def rst_to_text(text, state_hook=None, references=None): state = 'text' text.read(1) continue + if state == 'code-block' and char == next == '\n' and text.peek(5)[1:] != ' ': + # Foo:: + # + # *stuff* *code* *ignore .. all markup* + # + # More arcane stuff + # + # Regular text... + state_hook(state, 'text', out) + state = 'text' out.write(char) assert state == 'text', 'Invalid final state %r (This usually indicates unmatched */**)' % state return out.getvalue() -def ansi_escapes(old_state, new_state, out): - if old_state == 'text' and new_state in ('*', '`', '``'): - out.write('\033[4m') - if old_state == 'text' and new_state == '**': - out.write('\033[1m') - if old_state in ('*', '`', '``', '**') and new_state == 'text': - out.write('\033[0m') - - class RstToTextLazy: def __init__(self, str, state_hook=None, references=None): self.str = str @@ -160,3 +177,26 @@ class RstToTextLazy: def __contains__(self, item): return item in self.rst + + +def ansi_escapes(old_state, new_state, out): + if old_state == 'text' and new_state in ('*', '`', '``'): + out.write('\033[4m') + if old_state == 'text' and new_state == '**': + out.write('\033[1m') + if old_state in ('*', '`', '``', '**') and new_state == 'text': + out.write('\033[0m') + + +def rst_to_terminal(rst, references=None, destination=sys.stdout): + """ + Convert *rst* to a lazy string. + + If *destination* is a file-like object connected to a terminal, + enrich text with suitable ANSI escapes. Otherwise return plain text. + """ + if hasattr(destination, 'isatty') and destination.isatty() and (sys.platform != 'win32' or 'ANSICON' in os.environ): + rst_state_hook = ansi_escapes + else: + rst_state_hook = None + return RstToTextLazy(rst, rst_state_hook, references) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index c1ec2b18..10fbbf9f 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -45,7 +45,7 @@ from ..helpers import Manifest from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from ..helpers import bin_to_hex from ..helpers import MAX_S -from ..nanorst import RstToTextLazy +from ..nanorst import RstToTextLazy, rst_to_terminal from ..patterns import IECommand, PatternMatcher, parse_pattern from ..item import Item from ..logger import setup_logging @@ -3366,3 +3366,8 @@ def get_all_parsers(): def test_help_formatting(command, parser): if isinstance(parser.epilog, RstToTextLazy): assert parser.epilog.rst + + +@pytest.mark.parametrize('topic, helptext', list(Archiver.helptext.items())) +def test_help_formatting_helptexts(topic, helptext): + assert str(rst_to_terminal(helptext)) diff --git a/src/borg/testsuite/nanorst.py b/src/borg/testsuite/nanorst.py index 9b0cb760..06543609 100644 --- a/src/borg/testsuite/nanorst.py +++ b/src/borg/testsuite/nanorst.py @@ -16,6 +16,10 @@ def test_comment_inline(): assert rst_to_text('Foo and Bar\n.. foo\nbar') == 'Foo and Bar\n.. foo\nbar' +def test_inline_escape(): + assert rst_to_text('Such as "\\*" characters.') == 'Such as "*" characters.' + + def test_comment(): assert rst_to_text('Foo and Bar\n\n.. foo\nbar') == 'Foo and Bar\n\nbar' From 5cab72035a9f04fbaa342db750ab8673147f3f39 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Jun 2017 19:24:45 +0200 Subject: [PATCH 1023/1387] mark --pattern, --patterns-from as experimental --- docs/usage/create.rst.inc | 4 +- docs/usage/diff.rst.inc | 4 +- docs/usage/export-tar.rst.inc | 4 +- docs/usage/extract.rst.inc | 4 +- docs/usage/help.rst.inc | 59 ++++++++++++------------- docs/usage/list.rst.inc | 4 +- docs/usage/recreate.rst.inc | 4 +- src/borg/archiver.py | 81 ++++++++++++++++++----------------- 8 files changed, 83 insertions(+), 81 deletions(-) diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 105df5cc..e253336e 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -41,9 +41,9 @@ Exclusion options ``--keep-exclude-tags``, ``--keep-tag-files`` | if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive ``--pattern PATTERN`` - | include/exclude paths matching PATTERN + | experimental: include/exclude paths matching PATTERN ``--patterns-from PATTERNFILE`` - | read include/exclude patterns from PATTERNFILE, one per line + | experimental: read include/exclude patterns from PATTERNFILE, one per line Filesystem options ``-x``, ``--one-file-system`` diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index 74e70376..53d6467d 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -39,9 +39,9 @@ Exclusion options ``--keep-exclude-tags``, ``--keep-tag-files`` | if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive ``--pattern PATTERN`` - | include/exclude paths matching PATTERN + | experimental: include/exclude paths matching PATTERN ``--patterns-from PATTERNFILE`` - | read include/exclude patterns from PATTERNFILE, one per line + | experimental: read include/exclude patterns from PATTERNFILE, one per line Description ~~~~~~~~~~~ diff --git a/docs/usage/export-tar.rst.inc b/docs/usage/export-tar.rst.inc index 845de54d..d5f46498 100644 --- a/docs/usage/export-tar.rst.inc +++ b/docs/usage/export-tar.rst.inc @@ -26,9 +26,9 @@ optional arguments ``--exclude-from EXCLUDEFILE`` | read exclude patterns from EXCLUDEFILE, one per line ``--pattern PATTERN`` - | include/exclude paths matching PATTERN + | experimental: include/exclude paths matching PATTERN ``--patterns-from PATTERNFILE`` - | read include/exclude patterns from PATTERNFILE, one per line + | experimental: read include/exclude patterns from PATTERNFILE, one per line ``--strip-components NUMBER`` | Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. diff --git a/docs/usage/extract.rst.inc b/docs/usage/extract.rst.inc index 4a63ffeb..e9913d65 100644 --- a/docs/usage/extract.rst.inc +++ b/docs/usage/extract.rst.inc @@ -24,9 +24,9 @@ optional arguments ``--exclude-from EXCLUDEFILE`` | read exclude patterns from EXCLUDEFILE, one per line ``--pattern PATTERN`` - | include/exclude paths matching PATTERN + | experimental: include/exclude paths matching PATTERN ``--patterns-from PATTERNFILE`` - | read include/exclude patterns from PATTERNFILE, one per line + | experimental: read include/exclude patterns from PATTERNFILE, one per line ``--numeric-owner`` | only obey numeric user and group identifiers ``--strip-components NUMBER`` diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index 22e8b0b9..40789083 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -30,7 +30,7 @@ two alphanumeric characters followed by a colon (i.e. `aa:something/*`). must match from the start to just before a path separator. Except for the root path, paths will never end in the path separator when matching is attempted. Thus, if a given pattern ends in a path - separator, a '*' is appended before matching is attempted. + separator, a '\*' is appended before matching is attempted. Shell-style patterns, selector `sh:` @@ -113,39 +113,40 @@ Examples:: EOF $ borg create --exclude-from exclude.txt backup / +.. container:: experimental -A more general and easier to use way to define filename matching patterns exists -with the `--pattern` and `--patterns-from` options. Using these, you may specify -the backup roots (starting points) and patterns for inclusion/exclusion. A -root path starts with the prefix `R`, followed by a path (a plain path, not a -file pattern). An include rule starts with the prefix +, an exclude rule starts -with the prefix -, both followed by a pattern. -Inclusion patterns are useful to include paths that are contained in an excluded -path. The first matching pattern is used so if an include pattern matches before -an exclude pattern, the file is backed up. + A more general and easier to use way to define filename matching patterns exists + with the experimental `--pattern` and `--patterns-from` options. Using these, you + may specify the backup roots (starting points) and patterns for inclusion/exclusion. + A root path starts with the prefix `R`, followed by a path (a plain path, not a + file pattern). An include rule starts with the prefix +, an exclude rule starts + with the prefix -, both followed by a pattern. + Inclusion patterns are useful to include paths that are contained in an excluded + path. The first matching pattern is used so if an include pattern matches before + an exclude pattern, the file is backed up. -Note that the default pattern style for `--pattern` and `--patterns-from` is -shell style (`sh:`), so those patterns behave similar to rsync include/exclude -patterns. The pattern style can be set via the `P` prefix. + Note that the default pattern style for `--pattern` and `--patterns-from` is + shell style (`sh:`), so those patterns behave similar to rsync include/exclude + patterns. The pattern style can be set via the `P` prefix. -Patterns (`--pattern`) and excludes (`--exclude`) from the command line are -considered first (in the order of appearance). Then patterns from `--patterns-from` -are added. Exclusion patterns from `--exclude-from` files are appended last. + Patterns (`--pattern`) and excludes (`--exclude`) from the command line are + considered first (in the order of appearance). Then patterns from `--patterns-from` + are added. Exclusion patterns from `--exclude-from` files are appended last. -An example `--patterns-from` file could look like that:: + An example `--patterns-from` file could look like that:: - # "sh:" pattern style is the default, so the following line is not needed: - P sh - R / - # can be rebuild - - /home/*/.cache - # they're downloads for a reason - - /home/*/Downloads - # susan is a nice person - # include susans home - + /home/susan - # don't backup the other home directories - - /home/* + # "sh:" pattern style is the default, so the following line is not needed: + P sh + R / + # can be rebuild + - /home/*/.cache + # they're downloads for a reason + - /home/*/Downloads + # susan is a nice person + # include susans home + + /home/susan + # don't backup the other home directories + - /home/* .. _borg_placeholders: diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index 8cb81152..a56cbecd 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -50,9 +50,9 @@ Exclusion options ``--keep-exclude-tags``, ``--keep-tag-files`` | if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive ``--pattern PATTERN`` - | include/exclude paths matching PATTERN + | experimental: include/exclude paths matching PATTERN ``--patterns-from PATTERNFILE`` - | read include/exclude patterns from PATTERNFILE, one per line + | experimental: read include/exclude patterns from PATTERNFILE, one per line Description ~~~~~~~~~~~ diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index 507be1d9..fff3578a 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -39,9 +39,9 @@ Exclusion options ``--keep-exclude-tags``, ``--keep-tag-files`` | if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive ``--pattern PATTERN`` - | include/exclude paths matching PATTERN + | experimental: include/exclude paths matching PATTERN ``--patterns-from PATTERNFILE`` - | read include/exclude patterns from PATTERNFILE, one per line + | experimental: read include/exclude patterns from PATTERNFILE, one per line Archive options ``--target TARGET`` diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 56f762f2..81fe2889 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1942,39 +1942,40 @@ class Archiver: EOF $ borg create --exclude-from exclude.txt backup / + .. container:: experimental - A more general and easier to use way to define filename matching patterns exists - with the `--pattern` and `--patterns-from` options. Using these, you may specify - the backup roots (starting points) and patterns for inclusion/exclusion. A - root path starts with the prefix `R`, followed by a path (a plain path, not a - file pattern). An include rule starts with the prefix +, an exclude rule starts - with the prefix -, both followed by a pattern. - Inclusion patterns are useful to include paths that are contained in an excluded - path. The first matching pattern is used so if an include pattern matches before - an exclude pattern, the file is backed up. + A more general and easier to use way to define filename matching patterns exists + with the experimental `--pattern` and `--patterns-from` options. Using these, you + may specify the backup roots (starting points) and patterns for inclusion/exclusion. + A root path starts with the prefix `R`, followed by a path (a plain path, not a + file pattern). An include rule starts with the prefix +, an exclude rule starts + with the prefix -, both followed by a pattern. + Inclusion patterns are useful to include paths that are contained in an excluded + path. The first matching pattern is used so if an include pattern matches before + an exclude pattern, the file is backed up. - Note that the default pattern style for `--pattern` and `--patterns-from` is - shell style (`sh:`), so those patterns behave similar to rsync include/exclude - patterns. The pattern style can be set via the `P` prefix. + Note that the default pattern style for `--pattern` and `--patterns-from` is + shell style (`sh:`), so those patterns behave similar to rsync include/exclude + patterns. The pattern style can be set via the `P` prefix. - Patterns (`--pattern`) and excludes (`--exclude`) from the command line are - considered first (in the order of appearance). Then patterns from `--patterns-from` - are added. Exclusion patterns from `--exclude-from` files are appended last. + Patterns (`--pattern`) and excludes (`--exclude`) from the command line are + considered first (in the order of appearance). Then patterns from `--patterns-from` + are added. Exclusion patterns from `--exclude-from` files are appended last. - An example `--patterns-from` file could look like that:: + An example `--patterns-from` file could look like that:: - # "sh:" pattern style is the default, so the following line is not needed: - P sh - R / - # can be rebuild - - /home/*/.cache - # they're downloads for a reason - - /home/*/Downloads - # susan is a nice person - # include susans home - + /home/susan - # don't backup the other home directories - - /home/*\n\n''') + # "sh:" pattern style is the default, so the following line is not needed: + P sh + R / + # can be rebuild + - /home/*/.cache + # they're downloads for a reason + - /home/*/Downloads + # susan is a nice person + # include susans home + + /home/susan + # don't backup the other home directories + - /home/*\n\n''') helptext['placeholders'] = textwrap.dedent(''' Repository (or Archive) URLs, --prefix and --remote-path values support these placeholders: @@ -2790,9 +2791,9 @@ class Archiver: 'objects themselves from the backup archive') exclude_group.add_argument('--pattern', action=ArgparsePatternAction, - metavar="PATTERN", help='include/exclude paths matching PATTERN') + metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction, - metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line') + metavar='PATTERNFILE', help='experimental: read include/exclude patterns from PATTERNFILE, one per line') fs_group = subparser.add_argument_group('Filesystem options') fs_group.add_argument('-x', '--one-file-system', dest='one_file_system', @@ -2875,9 +2876,9 @@ class Archiver: subparser.add_argument('--exclude-from', action=ArgparseExcludeFileAction, metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') subparser.add_argument('--pattern', action=ArgparsePatternAction, - metavar="PATTERN", help='include/exclude paths matching PATTERN') + metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') subparser.add_argument('--patterns-from', action=ArgparsePatternFileAction, - metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line') + metavar='PATTERNFILE', help='experimental: read include/exclude patterns from PATTERNFILE, one per line') subparser.add_argument('--numeric-owner', dest='numeric_owner', action='store_true', default=False, help='only obey numeric user and group identifiers') @@ -2948,9 +2949,9 @@ class Archiver: subparser.add_argument('--exclude-from', action=ArgparseExcludeFileAction, metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') subparser.add_argument('--pattern', action=ArgparsePatternAction, - metavar="PATTERN", help='include/exclude paths matching PATTERN') + metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') subparser.add_argument('--patterns-from', action=ArgparsePatternFileAction, - metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line') + metavar='PATTERNFILE', help='experimental: read include/exclude patterns from PATTERNFILE, one per line') subparser.add_argument('--strip-components', dest='strip_components', type=int, default=0, metavar='NUMBER', help='Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped.') @@ -3024,9 +3025,9 @@ class Archiver: 'objects themselves from the backup archive') exclude_group.add_argument('--pattern', action=ArgparsePatternAction, - metavar="PATTERN", help='include/exclude paths matching PATTERN') + metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction, - metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line') + metavar='PATTERNFILE', help='experimental: read include/exclude patterns from PATTERNFILE, one per line') rename_epilog = process_epilog(""" This command renames an archive in the repository. @@ -3145,9 +3146,9 @@ class Archiver: 'objects themselves from the backup archive') exclude_group.add_argument('--pattern', action=ArgparsePatternAction, - metavar="PATTERN", help='include/exclude paths matching PATTERN') + metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction, - metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line') + metavar='PATTERNFILE', help='experimental: read include/exclude patterns from PATTERNFILE, one per line') mount_epilog = process_epilog(""" This command mounts an archive as a FUSE filesystem. This can be useful for @@ -3519,9 +3520,9 @@ class Archiver: 'objects themselves from the backup archive') exclude_group.add_argument('--pattern', action=ArgparsePatternAction, - metavar="PATTERN", help='include/exclude paths matching PATTERN') + metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction, - metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line') + metavar='PATTERNFILE', help='experimental: read include/exclude patterns from PATTERNFILE, one per line') archive_group = subparser.add_argument_group('Archive options') archive_group.add_argument('--target', dest='target', metavar='TARGET', default=None, From 005068dd6db3083118d70e820327085bf1d34a35 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sat, 10 Jun 2017 11:42:42 +0200 Subject: [PATCH 1024/1387] Improve robustness of monkey patching borg.constants.PBKDF2_ITERATIONS. And add lots of warnings. --- conftest.py | 14 ++++++++------ src/borg/__init__.py | 2 ++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/conftest.py b/conftest.py index b23f30d1..887aa283 100644 --- a/conftest.py +++ b/conftest.py @@ -2,6 +2,13 @@ import os import pytest +# IMPORTANT keep this above all other borg imports to avoid inconsistent values +# for `from borg.constants import PBKDF2_ITERATIONS` (or star import) usages before +# this is executed +from borg import constants +# no fixture-based monkey-patching since star-imports are used for the constants module +constants.PBKDF2_ITERATIONS = 1 + # needed to get pretty assertion failures in unit tests: if hasattr(pytest, 'register_assert_rewrite'): pytest.register_assert_rewrite('borg.testsuite') @@ -14,12 +21,7 @@ setup_logging() from borg.testsuite import has_lchflags, has_llfuse from borg.testsuite import are_symlinks_supported, are_hardlinks_supported, is_utime_fully_supported from borg.testsuite.platform import fakeroot_detected, are_acls_working -from borg import xattr, constants - - -def pytest_configure(config): - # no fixture-based monkey-patching since star-imports are used for the constants module - constants.PBKDF2_ITERATIONS = 1 +from borg import xattr @pytest.fixture(autouse=True) diff --git a/src/borg/__init__.py b/src/borg/__init__.py index 33b9616d..c2b20186 100644 --- a/src/borg/__init__.py +++ b/src/borg/__init__.py @@ -1,5 +1,7 @@ from distutils.version import LooseVersion +# IMPORTANT keep imports from borg here to a minimum because our testsuite depends on +# beeing able to import borg.constants and then monkey patching borg.constants.PBKDF2_ITERATIONS from ._version import version as __version__ From b8ad8b84da635ea533a4133591301a4f91d5d330 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 28 May 2017 18:04:33 +0200 Subject: [PATCH 1025/1387] Cache: Wipe cache if compatibility is not sure Add detection of possibly incompatible combinations of the borg versions maintaining the cache and the featues used. --- src/borg/cache.py | 50 ++++++++++++++++++++++++++++++++++++++++++++- src/borg/helpers.py | 16 +++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 13045f0e..344682a9 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -15,8 +15,9 @@ from .constants import CACHE_README from .hashindex import ChunkIndex, ChunkIndexEntry from .helpers import Location from .helpers import Error +from .helpers import Manifest from .helpers import get_cache_dir, get_security_dir -from .helpers import int_to_bigint, bigint_to_int, bin_to_hex +from .helpers import int_to_bigint, bigint_to_int, bin_to_hex, parse_stringified_list from .helpers import format_file_size from .helpers import safe_ns from .helpers import yes, hostname_is_unique @@ -257,6 +258,8 @@ class CacheConfig: self.manifest_id = unhexlify(self._config.get('cache', 'manifest')) self.timestamp = self._config.get('cache', 'timestamp', fallback=None) self.key_type = self._config.get('cache', 'key_type', fallback=None) + self.ignored_features = set(parse_stringified_list(self._config.get('cache', 'ignored_features', fallback=''))) + self.mandatory_features = set(parse_stringified_list(self._config.get('cache', 'mandatory_features', fallback=''))) try: self.integrity = dict(self._config.items('integrity')) if self._config.get('cache', 'manifest') != self.integrity.pop('manifest'): @@ -281,6 +284,8 @@ class CacheConfig: if manifest: self._config.set('cache', 'manifest', manifest.id_str) self._config.set('cache', 'timestamp', manifest.timestamp) + self._config.set('cache', 'ignored_features', ','.join(self.ignored_features)) + self._config.set('cache', 'mandatory_features', ','.join(self.mandatory_features)) if not self._config.has_section('integrity'): self._config.add_section('integrity') for file, integrity_data in self.integrity.items(): @@ -370,6 +375,12 @@ class Cache: self.open() try: self.security_manager.assert_secure(manifest, key, cache_config=self.cache_config) + + if not self.check_cache_compatibility(): + self.wipe_cache() + + self.update_compatibility() + if sync and self.manifest.id != self.cache_config.manifest_id: self.sync() self.commit() @@ -678,6 +689,43 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" self.do_cache = os.path.isdir(archive_path) self.chunks = create_master_idx(self.chunks) + def check_cache_compatibility(self): + my_features = Manifest.SUPPORTED_REPO_FEATURES + if self.cache_config.ignored_features & my_features: + # The cache might not contain references of chunks that need a feature that is mandatory for some operation + # and which this version supports. To avoid corruption while executing that operation force rebuild. + return False + if not self.cache_config.mandatory_features <= my_features: + # The cache was build with consideration to at least one feature that this version does not understand. + # This client might misinterpret the cache. Thus force a rebuild. + return False + return True + + def wipe_cache(self): + logger.warning("Discarding incompatible cache and forcing a cache rebuild") + archive_path = os.path.join(self.path, 'chunks.archive.d') + if os.path.isdir(archive_path): + shutil.rmtree(os.path.join(self.path, 'chunks.archive.d')) + os.makedirs(os.path.join(self.path, 'chunks.archive.d')) + self.chunks = ChunkIndex() + with open(os.path.join(self.path, 'files'), 'wb'): + pass # empty file + self.cache_config.manifest_id = '' + self.cache_config._config.set('cache', 'manifest', '') + + self.cache_config.ignored_features = set() + self.cache_config.mandatory_features = set() + + def update_compatibility(self): + operation_to_features_map = self.manifest.get_all_mandatory_features() + my_features = Manifest.SUPPORTED_REPO_FEATURES + repo_features = set() + for operation, features in operation_to_features_map.items(): + repo_features.update(features) + + self.cache_config.ignored_features.update(repo_features - my_features) + self.cache_config.mandatory_features.update(repo_features & my_features) + def add_chunk(self, id, chunk, stats, overwrite=False, wait=True): if not self.txn_active: self.begin_txn() diff --git a/src/borg/helpers.py b/src/borg/helpers.py index d0246bdd..f14f0644 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -325,6 +325,17 @@ class Manifest: if unsupported: raise MandatoryFeatureUnsupported([f.decode() for f in unsupported]) + def get_all_mandatory_features(self): + result = {} + feature_flags = self.config.get(b'feature_flags', None) + if feature_flags is None: + return result + + for operation, requirements in feature_flags.items(): + if b'mandatory' in requirements: + result[operation.decode()] = set([feature.decode() for feature in requirements[b'mandatory']]) + return result + def write(self): from .item import ManifestItem if self.key.tam_required: @@ -823,6 +834,11 @@ def bin_to_hex(binary): return hexlify(binary).decode('ascii') +def parse_stringified_list(s): + l = re.split(" *, *", s) + return [item for item in l if item != ''] + + class Location: """Object representing a repository / archive location """ From e63808a63fd2221a7ff4cc815f51979a83fc06ff Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Tue, 6 Jun 2017 22:37:53 +0200 Subject: [PATCH 1026/1387] Add tests for cache compatibility code. --- conftest.py | 22 ++++++++++++++++++ src/borg/testsuite/archiver.py | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/conftest.py b/conftest.py index 887aa283..e85ae6ef 100644 --- a/conftest.py +++ b/conftest.py @@ -9,10 +9,13 @@ from borg import constants # no fixture-based monkey-patching since star-imports are used for the constants module constants.PBKDF2_ITERATIONS = 1 + # needed to get pretty assertion failures in unit tests: if hasattr(pytest, 'register_assert_rewrite'): pytest.register_assert_rewrite('borg.testsuite') + +import borg.cache from borg.logger import setup_logging # Ensure that the loggers exist for all tests @@ -55,3 +58,22 @@ def pytest_report_header(config, startdir): output = "Tests enabled: " + ", ".join(enabled) + "\n" output += "Tests disabled: " + ", ".join(disabled) return output + + +class DefaultPatches: + def __init__(self, request): + self.org_cache_wipe_cache = borg.cache.Cache.wipe_cache + + def wipe_should_not_be_called(*a, **kw): + raise AssertionError("Cache wipe was triggered, if this is part of the test add @pytest.mark.allow_cache_wipe") + if 'allow_cache_wipe' not in request.keywords: + borg.cache.Cache.wipe_cache = wipe_should_not_be_called + request.addfinalizer(self.undo) + + def undo(self): + borg.cache.Cache.wipe_cache = self.org_cache_wipe_cache + + +@pytest.fixture(autouse=True) +def default_patches(request): + return DefaultPatches(request) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 85f79d71..c4192242 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1481,6 +1481,47 @@ class ArchiverTestCase(ArchiverTestCaseBase): # XXX this might hang if it doesn't raise an error self.cmd_raises_unknown_feature(['mount', self.repository_location + '::test', mountpoint]) + @pytest.mark.allow_cache_wipe + def test_unknown_mandatory_feature_in_cache(self): + if self.prefix: + path_prefix = 'ssh://__testsuite__' + else: + path_prefix = '' + + print(self.cmd('init', '--encryption=repokey', self.repository_location)) + + with Repository(self.repository_path, exclusive=True) as repository: + if path_prefix: + repository._location = Location(self.repository_location) + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) + with Cache(repository, key, manifest) as cache: + cache.begin_txn() + cache.cache_config.mandatory_features = set(['unknown-feature']) + cache.commit() + + if self.FORK_DEFAULT: + self.cmd('create', self.repository_location + '::test', 'input') + else: + called = False + wipe_cache_safe = Cache.wipe_cache + + def wipe_wrapper(*args): + nonlocal called + called = True + wipe_cache_safe(*args) + + with patch.object(Cache, 'wipe_cache', wipe_wrapper): + self.cmd('create', self.repository_location + '::test', 'input') + + assert called + + with Repository(self.repository_path, exclusive=True) as repository: + if path_prefix: + repository._location = Location(self.repository_location) + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) + with Cache(repository, key, manifest) as cache: + assert cache.cache_config.mandatory_features == set([]) + def test_progress_on(self): self.create_regular_file('file1', size=1024 * 80) self.cmd('init', '--encryption=repokey', self.repository_location) From 4987c04e5bff58c5b91af7eefb2c7425fec0bc17 Mon Sep 17 00:00:00 2001 From: rugk Date: Sat, 10 Jun 2017 11:57:06 +0200 Subject: [PATCH 1027/1387] Remove re docs Fixes https://github.com/borgbackup/borg/issues/2458 --- docs/usage/create.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/usage/create.rst b/docs/usage/create.rst index 2f05be39..afdb754b 100644 --- a/docs/usage/create.rst +++ b/docs/usage/create.rst @@ -16,11 +16,6 @@ Examples ~/src \ --exclude '*.pyc' - # Backup home directories excluding image thumbnails (i.e. only - # /home/*/.thumbnails is excluded, not /home/*/*/.thumbnails) - $ borg create /path/to/repo::my-files /home \ - --exclude 're:^/home/[^/]+/\.thumbnails/' - # Do the same using a shell-style pattern $ borg create /path/to/repo::my-files /home \ --exclude 'sh:/home/*/.thumbnails' From 4cd1cc6a28b26e9af98efe54ca8c138c93dd32e2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 10 Jun 2017 12:06:18 +0200 Subject: [PATCH 1028/1387] docs: correct create exclude comment --- docs/usage/create.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/usage/create.rst b/docs/usage/create.rst index afdb754b..8e4a2e03 100644 --- a/docs/usage/create.rst +++ b/docs/usage/create.rst @@ -16,7 +16,8 @@ Examples ~/src \ --exclude '*.pyc' - # Do the same using a shell-style pattern + # Backup home directories excluding image thumbnails (i.e. only + # /home//.thumbnails is excluded, not /home/*/*/.thumbnails etc.) $ borg create /path/to/repo::my-files /home \ --exclude 'sh:/home/*/.thumbnails' From 49ca3dca3332572ee12010862a63460646816272 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 11 Jun 2017 11:42:39 +0200 Subject: [PATCH 1029/1387] cache sync: move assert() behind declarations --- src/borg/cache_sync/unpack_template.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/cache_sync/unpack_template.h b/src/borg/cache_sync/unpack_template.h index a6492da3..9fc1a34d 100644 --- a/src/borg/cache_sync/unpack_template.h +++ b/src/borg/cache_sync/unpack_template.h @@ -54,8 +54,6 @@ static inline void unpack_init(unpack_context* ctx) static inline int unpack_execute(unpack_context* ctx, const char* data, size_t len, size_t* off) { - assert(len >= *off); - const unsigned char* p = (unsigned char*)data + *off; const unsigned char* const pe = (unsigned char*)data + len; const void* n = NULL; @@ -70,6 +68,8 @@ static inline int unpack_execute(unpack_context* ctx, const char* data, size_t l int ret; + assert(len >= *off); + #define construct_cb(name) \ construct && unpack_callback ## name From 5f5371f0b1037c788d2c3f5273326036411d7783 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 11 Jun 2017 12:13:42 +0200 Subject: [PATCH 1030/1387] implement --glob-archives/-a --- src/borg/archive.py | 18 ++++++------ src/borg/archiver.py | 50 +++++++++++++++++++++------------- src/borg/helpers.py | 14 ++++++---- src/borg/testsuite/archiver.py | 21 ++++++++++++++ 4 files changed, 70 insertions(+), 33 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index f375015d..a0bde175 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1124,19 +1124,19 @@ class ArchiveChecker: self.error_found = False self.possibly_superseded = set() - def check(self, repository, repair=False, archive=None, first=0, last=0, sort_by='', prefix='', + def check(self, repository, repair=False, archive=None, first=0, last=0, sort_by='', glob=None, verify_data=False, save_space=False): """Perform a set of checks on 'repository' :param repair: enable repair mode, write updated or corrected data into repository :param archive: only check this archive :param first/last/sort_by: only check this number of first/last archives ordered by sort_by - :param prefix: only check archives with this prefix + :param glob: only check archives matching this glob :param verify_data: integrity verification of data referenced by archives :param save_space: Repository.commit(save_space) """ logger.info('Starting archive consistency check...') - self.check_all = archive is None and not any((first, last, prefix)) + self.check_all = archive is None and not any((first, last, glob)) self.repair = repair self.repository = repository self.init_chunks() @@ -1158,7 +1158,7 @@ class ArchiveChecker: self.error_found = True del self.chunks[Manifest.MANIFEST_ID] self.manifest = self.rebuild_manifest() - self.rebuild_refcounts(archive=archive, first=first, last=last, sort_by=sort_by, prefix=prefix) + self.rebuild_refcounts(archive=archive, first=first, last=last, sort_by=sort_by, glob=glob) self.orphan_chunks_check() self.finish(save_space=save_space) if self.error_found: @@ -1331,7 +1331,7 @@ class ArchiveChecker: logger.info('Manifest rebuild complete.') return manifest - def rebuild_refcounts(self, archive=None, first=0, last=0, sort_by='', prefix=''): + def rebuild_refcounts(self, archive=None, first=0, last=0, sort_by='', glob=None): """Rebuild object reference counts by walking the metadata Missing and/or incorrect data is repaired when detected @@ -1495,10 +1495,10 @@ class ArchiveChecker: if archive is None: sort_by = sort_by.split(',') - if any((first, last, prefix)): - archive_infos = self.manifest.archives.list(sort_by=sort_by, prefix=prefix, first=first, last=last) - if prefix and not archive_infos: - logger.warning('--prefix %s does not match any archives', prefix) + if any((first, last, glob)): + archive_infos = self.manifest.archives.list(sort_by=sort_by, glob=glob, first=first, last=last) + if glob and not archive_infos: + logger.warning('--glob-archives %s does not match any archives', glob) if first and len(archive_infos) < first: logger.warning('--first %d archives: only found %d archives', first, len(archive_infos)) if last and len(archive_infos) < last: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 23a75dfa..8c1b5819 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -33,6 +33,7 @@ import msgpack import borg from . import __version__ from . import helpers +from . import shellpattern from .algorithms.checksums import crc32 from .archive import Archive, ArchiveChecker, ArchiveRecreater, Statistics, is_special from .archive import BackupOSError, backup_io @@ -283,9 +284,11 @@ class Archiver: if not args.archives_only: if not repository.check(repair=args.repair, save_space=args.save_space): return EXIT_WARNING + if args.prefix: + args.glob_archives = args.prefix + '*' if not args.repo_only and not ArchiveChecker().check( repository, repair=args.repair, archive=args.location.archive, - first=args.first, last=args.last, sort_by=args.sort_by or 'ts', prefix=args.prefix, + first=args.first, last=args.last, sort_by=args.sort_by or 'ts', glob=args.glob_archives, verify_data=args.verify_data, save_space=args.save_space): return EXIT_WARNING return EXIT_SUCCESS @@ -1168,7 +1171,7 @@ class Archiver: @with_repository(exclusive=True, manifest=False) def do_delete(self, args, repository): """Delete an existing repository or archives""" - if any((args.location.archive, args.first, args.last, args.prefix)): + if any((args.location.archive, args.first, args.last, args.prefix, args.glob_archives)): return self._delete_archives(args, repository) else: return self._delete_repository(args, repository) @@ -1365,7 +1368,7 @@ class Archiver: @with_repository(cache=True, compatibility=(Manifest.Operation.READ,)) def do_info(self, args, repository, manifest, key, cache): """Show archive details such as disk space used""" - if any((args.location.archive, args.first, args.last, args.prefix)): + if any((args.location.archive, args.first, args.last, args.prefix, args.glob_archives)): return self._info_archives(args, repository, manifest, key, cache) else: return self._info_repository(args, repository, manifest, key, cache) @@ -1463,7 +1466,10 @@ class Archiver: return self.exit_code archives_checkpoints = manifest.archives.list(sort_by=['ts'], reverse=True) # just a ArchiveInfo list if args.prefix: - archives_checkpoints = [arch for arch in archives_checkpoints if arch.name.startswith(args.prefix)] + args.glob_archives = args.prefix + '*' + if args.glob_archives: + regex = re.compile(shellpattern.translate(args.glob_archives)) + archives_checkpoints = [arch for arch in archives_checkpoints if regex.match(arch.name) is not None] is_checkpoint = re.compile(r'\.checkpoint(\.\d+)?$').search checkpoints = [arch for arch in archives_checkpoints if is_checkpoint(arch.name)] # keep the latest checkpoint, if there is no later non-checkpoint archive @@ -3344,8 +3350,7 @@ class Archiver: help='number of monthly archives to keep') subparser.add_argument('-y', '--keep-yearly', dest='yearly', type=int, default=0, help='number of yearly archives to keep') - subparser.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, - help='only consider archive names starting with this prefix') + self.add_archives_filters_args(subparser, sort_by=False, first_last=False) subparser.add_argument('--save-space', dest='save_space', action='store_true', default=False, help='work slower, but using less space') @@ -3839,21 +3844,28 @@ class Archiver: return parser @staticmethod - def add_archives_filters_args(subparser): + def add_archives_filters_args(subparser, sort_by=True, first_last=True): filters_group = subparser.add_argument_group('filters', 'Archive filters can be applied to repository targets.') - filters_group.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, default='', - help='only consider archive names starting with this prefix') - - sort_by_default = 'timestamp' - filters_group.add_argument('--sort-by', dest='sort_by', type=SortBySpec, default=sort_by_default, - help='Comma-separated list of sorting keys; valid keys are: {}; default is: {}' - .format(', '.join(HUMAN_SORT_KEYS), sort_by_default)) - group = filters_group.add_mutually_exclusive_group() - group.add_argument('--first', dest='first', metavar='N', default=0, type=int, - help='consider first N archives after other filters were applied') - group.add_argument('--last', dest='last', metavar='N', default=0, type=int, - help='consider last N archives after other filters were applied') + group.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, default='', + help='only consider archive names starting with this prefix.') + group.add_argument('-a', '--glob-archives', dest='glob_archives', default=None, + help='only consider archive names matching the glob. ' + 'sh: rules apply, see "borg help patterns". ' + '--prefix and --glob-archives are mutually exclusive.') + + if sort_by: + sort_by_default = 'timestamp' + filters_group.add_argument('--sort-by', dest='sort_by', type=SortBySpec, default=sort_by_default, + help='Comma-separated list of sorting keys; valid keys are: {}; default is: {}' + .format(', '.join(HUMAN_SORT_KEYS), sort_by_default)) + + if first_last: + group = filters_group.add_mutually_exclusive_group() + group.add_argument('--first', dest='first', metavar='N', default=0, type=int, + help='consider first N archives after other filters were applied') + group.add_argument('--last', dest='last', metavar='N', default=0, type=int, + help='consider last N archives after other filters were applied') def get_args(self, argv, cmd): """usually, just returns argv, except if we deal with a ssh forced command for borg serve.""" diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 116c9f62..f65aac9b 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -42,6 +42,7 @@ from . import __version__ as borg_version from . import __version_tuple__ as borg_version_tuple from . import chunker from . import hashindex +from . import shellpattern from .constants import * # NOQA @@ -189,7 +190,7 @@ class Archives(abc.MutableMapping): name = safe_encode(name) del self._archives[name] - def list(self, sort_by=(), reverse=False, prefix='', first=None, last=None): + def list(self, sort_by=(), reverse=False, glob=None, first=None, last=None): """ Inexpensive Archive.list_archives replacement if we just need .name, .id, .ts Returns list of borg.helpers.ArchiveInfo instances. @@ -197,7 +198,8 @@ class Archives(abc.MutableMapping): """ if isinstance(sort_by, (str, bytes)): raise TypeError('sort_by must be a sequence of str') - archives = [x for x in self.values() if x.name.startswith(prefix)] + regex = re.compile(shellpattern.translate(glob or '*')) + archives = [x for x in self.values() if regex.match(x.name) is not None] for sortkey in reversed(sort_by): archives.sort(key=attrgetter(sortkey)) if reverse or last: @@ -207,11 +209,13 @@ class Archives(abc.MutableMapping): def list_considering(self, args): """ - get a list of archives, considering --first/last/prefix/sort cmdline args + get a list of archives, considering --first/last/prefix/glob-archives/sort cmdline args """ if args.location.archive: - raise Error('The options --first, --last and --prefix can only be used on repository targets.') - return self.list(sort_by=args.sort_by.split(','), prefix=args.prefix, first=args.first, last=args.last) + raise Error('The options --first, --last, --prefix and --glob-archives can only be used on repository targets.') + if args.prefix: + args.glob_archives = args.prefix + '*' + return self.list(sort_by=args.sort_by.split(','), glob=args.glob_archives, first=args.first, last=args.last) def set_raw_dict(self, d): """set the dict we get from the msgpack unpacker""" diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index c3de3c73..6d0b28c4 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1708,6 +1708,27 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('bar-2015-08-12-10:00', output) self.assert_in('bar-2015-08-12-20:00', output) + def test_prune_repository_glob(self): + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::2015-08-12-10:00-foo', src_dir) + self.cmd('create', self.repository_location + '::2015-08-12-20:00-foo', src_dir) + self.cmd('create', self.repository_location + '::2015-08-12-10:00-bar', src_dir) + self.cmd('create', self.repository_location + '::2015-08-12-20:00-bar', src_dir) + output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=2', '--glob-archives=2015-*-foo') + self.assert_in('Keeping archive: 2015-08-12-20:00-foo', output) + self.assert_in('Would prune: 2015-08-12-10:00-foo', output) + output = self.cmd('list', self.repository_location) + self.assert_in('2015-08-12-10:00-foo', output) + self.assert_in('2015-08-12-20:00-foo', output) + self.assert_in('2015-08-12-10:00-bar', output) + self.assert_in('2015-08-12-20:00-bar', output) + self.cmd('prune', self.repository_location, '--keep-daily=2', '--glob-archives=2015-*-foo') + output = self.cmd('list', self.repository_location) + self.assert_not_in('2015-08-12-10:00-foo', output) + self.assert_in('2015-08-12-20:00-foo', output) + self.assert_in('2015-08-12-10:00-bar', output) + self.assert_in('2015-08-12-20:00-bar', output) + def test_list_prefix(self): self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test-1', src_dir) From bffcc60f90e957b3164ba166d5450987493020f9 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 11 Jun 2017 12:27:52 +0200 Subject: [PATCH 1031/1387] docs: internals: feature flags typos, clarifications --- docs/internals/data-structures.rst | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 1e50e012..6d140823 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -337,8 +337,6 @@ the manifest, since an ID check is not possible. of Borg preserve its contents (it may have been a better place for *item_keys*, which is not preserved by unaware Borg versions, releases predating 1.0.4). -.. This was implemented in PR#1149, 78121a8 and a7b5165 - Feature flags +++++++++++++ @@ -355,7 +353,7 @@ category are operations that require accurate reference counts, for example archive deletion and check. As the manifest is always updated and always read, it is the ideal place to store -feature flags, comparable to the super-block of a file system. The only issue problem +feature flags, comparable to the super-block of a file system. The only problem is to recover from a lost manifest, i.e. how is it possible to detect which feature flags are enabled, if there is no manifest to tell. This issue is left open at this time, but is not expected to be a major hurdle; it doesn't have to be handled efficiently, it just @@ -401,7 +399,7 @@ Each operation can contain several sets of feature flags. Only one set, the *mandatory* set is currently defined. Upon reading the manifest, the Borg client has already determined which operation -is to be performed. If feature flags are found in the manifest, the set +should be performed. If feature flags are found in the manifest, the set of feature flags supported by the client is compared to the mandatory set found in the manifest. If any unsupported flags are found (i.e. the mandatory set is not a subset of the features supported by the Borg client used), the operation @@ -421,7 +419,7 @@ Borg releases unaware of feature flags. .. _Cache feature flags: .. rubric:: Cache feature flags -`The cache`_ does not have its separate flag of feature flags. Instead, Borg stores +`The cache`_ does not have its separate set of feature flags. Instead, Borg stores which flags were used to create or modify a cache. All mandatory manifest features from all operations are gathered in one set. @@ -440,10 +438,16 @@ Conversely, the *ignored_features* set contains only those features which were n relevant to operating the cache. Otherwise, the client would not pass the feature set test against the manifest. -When opening a cache and the *mandatory_features* set is a not a subset of the features +When opening a cache and the *mandatory_features* set is not a subset of the features supported by the client, the cache is wiped out and rebuilt, since a client not supporting a mandatory feature that the cache was built with would be unable to update it correctly. +The assumption behind this behaviour is that any of the unsupported features could have +been reflected in the cache and there is no way for the client to discern whether +that is the case. +Meanwhile, it may not be practical for every feature to have clients using it track +whether the feature had an impact on the cache. +Therefore, the cache is wiped. When opening a cache and the intersection of *ignored_features* and the features supported by the client contains any elements, i.e. the client possesses features From 221dc1c4c745f742d58ff4e56fa2f08b8ab6a7e0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 11 Jun 2017 13:35:23 +0200 Subject: [PATCH 1032/1387] normalize authenticated key modes rename authenticated to authenticated-blake2, consistent with the other blake2 key modes add authenticated mode that fills the blank and is consistent with the other "unqualified" key modes --- docs/man/borg-init.1 | 80 +++++++++++++++++++++++++++++++-------- docs/usage/init.rst.inc | 46 +++++++++++++++------- src/borg/archiver.py | 50 ++++++++++++++++-------- src/borg/crypto/key.py | 21 +++++++--- src/borg/testsuite/key.py | 23 +++++++++-- 5 files changed, 165 insertions(+), 55 deletions(-) diff --git a/docs/man/borg-init.1 b/docs/man/borg-init.1 index d0e1f5ed..e25b9ca1 100644 --- a/docs/man/borg-init.1 +++ b/docs/man/borg-init.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-INIT 1 "2017-05-17" "" "borg backup tool" +.TH BORG-INIT 1 "2017-06-11" "" "borg backup tool" .SH NAME borg-init \- Initialize an empty repository . @@ -81,6 +81,55 @@ a different keyboard layout. You can change your passphrase for existing repos at any time, it won\(aqt affect the encryption/decryption key or other secrets. .SS Encryption modes +.TS +center; +|l|l|l|l|. +_ +T{ +Hash/MAC +T} T{ +Not encrypted +no auth +T} T{ +Not encrypted, +but authenticated +T} T{ +Encrypted (AEAD w/ AES) +and authenticated +T} +_ +T{ +SHA\-256 +T} T{ +none +T} T{ +authenticated +T} T{ +repokey, keyfile +T} +_ +T{ +BLAKE2b +T} T{ +n/a +T} T{ +authenticated\-blake2 +T} T{ +repokey\-blake2, +keyfile\-blake2 +T} +_ +.TE +.sp +On modern Intel/AMD CPUs (except very cheap ones), AES is usually +hardware\-accelerated. +BLAKE2b is faster than SHA256 on Intel/AMD 64\-bit CPUs, +which makes \fIauthenticated\-blake2\fP faster than \fInone\fP and \fIauthenticated\fP\&. +.sp +On modern ARM CPUs, NEON provides hardware acceleration for SHA256 making it faster +than BLAKE2b\-256 there. NEON accelerates AES as well. +.sp +Hardware acceleration is always used automatically when available. .sp \fIrepokey\fP and \fIkeyfile\fP use AES\-CTR\-256 for encryption and HMAC\-SHA256 for authentication in an encrypt\-then\-MAC (EtM) construction. The chunk ID hash @@ -90,26 +139,24 @@ These modes are compatible with borg 1.0.x. \fIrepokey\-blake2\fP and \fIkeyfile\-blake2\fP are also authenticated encryption modes, but use BLAKE2b\-256 instead of HMAC\-SHA256 for authentication. The chunk ID hash is a keyed BLAKE2b\-256 hash. -These modes are new and \fInot\fP compatible with borg 1.0.x. +These modes are new and \fInot\fP compatible with Borg 1.0.x. .sp \fIauthenticated\fP mode uses no encryption, but authenticates repository contents -through the same keyed BLAKE2b\-256 hash as the other blake2 modes (it uses it -as the chunk ID hash). The key is stored like repokey. +through the same HMAC\-SHA256 hash as the \fIrepokey\fP and \fIkeyfile\fP modes (it uses it +as the chunk ID hash). The key is stored like \fIrepokey\fP\&. This mode is new and \fInot\fP compatible with borg 1.0.x. .sp -\fInone\fP mode uses no encryption and no authentication. It uses sha256 as chunk +\fIauthenticated\-blake2\fP is like \fIauthenticated\fP, but uses the keyed BLAKE2b\-256 hash +from the other blake2 modes. +This mode is new and \fInot\fP compatible with Borg 1.0.x. +.sp +\fInone\fP mode uses no encryption and no authentication. It uses SHA256 as chunk ID hash. Not recommended, rather consider using an authenticated or authenticated/encrypted mode. -This mode is compatible with borg 1.0.x. -.sp -Hardware acceleration will be used automatically. -.sp -On modern Intel/AMD CPUs (except very cheap ones), AES is usually -hardware\-accelerated. BLAKE2b is faster than SHA256 on Intel/AMD 64bit CPUs, -which makes \fIauthenticated\fP faster than \fInone\fP\&. -.sp -On modern ARM CPUs, NEON provides hardware acceleration for SHA256 making it faster -than BLAKE2b\-256 there. +Use it only for new repositories where no encryption is wanted \fBand\fP when compatibility +with 1.0.x is important. If compatibility with 1.0.x is not important, use +\fIauthenticated\-blake2\fP or \fIauthenticated\fP instead. +This mode is compatible with Borg 1.0.x. .SH OPTIONS .sp See \fIborg\-common(1)\fP for common options of Borg commands. @@ -127,6 +174,9 @@ select encryption key mode \fB(required)\fP .TP .B \-a\fP,\fB \-\-append\-only create an append\-only mode repository +.TP +.B \-\-storage\-quota +Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota. .UNINDENT .SH EXAMPLES .INDENT 0.0 diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index 1a5abcb6..856af365 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -72,6 +72,26 @@ the encryption/decryption key or other secrets. Encryption modes ++++++++++++++++ ++----------+---------------+------------------------+--------------------------+ +| Hash/MAC | Not encrypted | Not encrypted, | Encrypted (AEAD w/ AES) | +| | no auth | but authenticated | and authenticated | ++----------+---------------+------------------------+--------------------------+ +| SHA-256 | none | authenticated | repokey, keyfile | ++----------+---------------+------------------------+--------------------------+ +| BLAKE2b | n/a | authenticated-blake2 | repokey-blake2, | +| | | | keyfile-blake2 | ++----------+---------------+------------------------+--------------------------+ + +On modern Intel/AMD CPUs (except very cheap ones), AES is usually +hardware-accelerated. +BLAKE2b is faster than SHA256 on Intel/AMD 64-bit CPUs, +which makes `authenticated-blake2` faster than `none` and `authenticated`. + +On modern ARM CPUs, NEON provides hardware acceleration for SHA256 making it faster +than BLAKE2b-256 there. NEON accelerates AES as well. + +Hardware acceleration is always used automatically when available. + `repokey` and `keyfile` use AES-CTR-256 for encryption and HMAC-SHA256 for authentication in an encrypt-then-MAC (EtM) construction. The chunk ID hash is HMAC-SHA256 as well (with a separate key). @@ -80,23 +100,21 @@ These modes are compatible with borg 1.0.x. `repokey-blake2` and `keyfile-blake2` are also authenticated encryption modes, but use BLAKE2b-256 instead of HMAC-SHA256 for authentication. The chunk ID hash is a keyed BLAKE2b-256 hash. -These modes are new and *not* compatible with borg 1.0.x. +These modes are new and *not* compatible with Borg 1.0.x. `authenticated` mode uses no encryption, but authenticates repository contents -through the same keyed BLAKE2b-256 hash as the other blake2 modes (it uses it -as the chunk ID hash). The key is stored like repokey. +through the same HMAC-SHA256 hash as the `repokey` and `keyfile` modes (it uses it +as the chunk ID hash). The key is stored like `repokey`. This mode is new and *not* compatible with borg 1.0.x. -`none` mode uses no encryption and no authentication. It uses sha256 as chunk +`authenticated-blake2` is like `authenticated`, but uses the keyed BLAKE2b-256 hash +from the other blake2 modes. +This mode is new and *not* compatible with Borg 1.0.x. + +`none` mode uses no encryption and no authentication. It uses SHA256 as chunk ID hash. Not recommended, rather consider using an authenticated or authenticated/encrypted mode. -This mode is compatible with borg 1.0.x. - -Hardware acceleration will be used automatically. - -On modern Intel/AMD CPUs (except very cheap ones), AES is usually -hardware-accelerated. BLAKE2b is faster than SHA256 on Intel/AMD 64bit CPUs, -which makes `authenticated` faster than `none`. - -On modern ARM CPUs, NEON provides hardware acceleration for SHA256 making it faster -than BLAKE2b-256 there. \ No newline at end of file +Use it only for new repositories where no encryption is wanted **and** when compatibility +with 1.0.x is important. If compatibility with 1.0.x is not important, use +`authenticated-blake2` or `authenticated` instead. +This mode is compatible with Borg 1.0.x. \ No newline at end of file diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 23a75dfa..20080501 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2434,34 +2434,52 @@ class Archiver: Encryption modes ++++++++++++++++ + +----------+---------------+------------------------+--------------------------+ + | Hash/MAC | Not encrypted | Not encrypted, | Encrypted (AEAD w/ AES) | + | | no auth | but authenticated | and authenticated | + +----------+---------------+------------------------+--------------------------+ + | SHA-256 | none | authenticated | repokey, keyfile | + +----------+---------------+------------------------+--------------------------+ + | BLAKE2b | n/a | authenticated-blake2 | repokey-blake2, | + | | | | keyfile-blake2 | + +----------+---------------+------------------------+--------------------------+ + + On modern Intel/AMD CPUs (except very cheap ones), AES is usually + hardware-accelerated. + BLAKE2b is faster than SHA256 on Intel/AMD 64-bit CPUs, + which makes `authenticated-blake2` faster than `none` and `authenticated`. + + On modern ARM CPUs, NEON provides hardware acceleration for SHA256 making it faster + than BLAKE2b-256 there. NEON accelerates AES as well. + + Hardware acceleration is always used automatically when available. + `repokey` and `keyfile` use AES-CTR-256 for encryption and HMAC-SHA256 for authentication in an encrypt-then-MAC (EtM) construction. The chunk ID hash is HMAC-SHA256 as well (with a separate key). - These modes are compatible with borg 1.0.x. + These modes are compatible with Borg 1.0.x. `repokey-blake2` and `keyfile-blake2` are also authenticated encryption modes, but use BLAKE2b-256 instead of HMAC-SHA256 for authentication. The chunk ID hash is a keyed BLAKE2b-256 hash. - These modes are new and *not* compatible with borg 1.0.x. + These modes are new and *not* compatible with Borg 1.0.x. `authenticated` mode uses no encryption, but authenticates repository contents - through the same keyed BLAKE2b-256 hash as the other blake2 modes (it uses it - as the chunk ID hash). The key is stored like repokey. - This mode is new and *not* compatible with borg 1.0.x. + through the same HMAC-SHA256 hash as the `repokey` and `keyfile` modes (it uses it + as the chunk ID hash). The key is stored like `repokey`. + This mode is new and *not* compatible with Borg 1.0.x. - `none` mode uses no encryption and no authentication. It uses sha256 as chunk + `authenticated-blake2` is like `authenticated`, but uses the keyed BLAKE2b-256 hash + from the other blake2 modes. + This mode is new and *not* compatible with Borg 1.0.x. + + `none` mode uses no encryption and no authentication. It uses SHA256 as chunk ID hash. Not recommended, rather consider using an authenticated or authenticated/encrypted mode. - This mode is compatible with borg 1.0.x. - - Hardware acceleration will be used automatically. - - On modern Intel/AMD CPUs (except very cheap ones), AES is usually - hardware-accelerated. BLAKE2b is faster than SHA256 on Intel/AMD 64bit CPUs, - which makes `authenticated` faster than `none`. - - On modern ARM CPUs, NEON provides hardware acceleration for SHA256 making it faster - than BLAKE2b-256 there. + Use it only for new repositories where no encryption is wanted **and** when compatibility + with 1.0.x is important. If compatibility with 1.0.x is not important, use + `authenticated-blake2` or `authenticated` instead. + This mode is compatible with Borg 1.0.x. """) subparser = subparsers.add_parser('init', parents=[common_parser], add_help=False, description=self.do_init.__doc__, epilog=init_epilog, diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 491d0ab7..aba1f35b 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -781,10 +781,7 @@ class Blake2RepoKey(ID_BLAKE2b_256, RepoKey): MAC = blake2b_256 -class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): - TYPE = 0x06 - NAME = 'authenticated BLAKE2b' - ARG_NAME = 'authenticated' +class AuthenticatedKeyBase(RepoKey): STORAGE = KeyBlobStorage.REPO # It's only authenticated, not encrypted. @@ -825,9 +822,21 @@ class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): return data +class AuthenticatedKey(AuthenticatedKeyBase): + TYPE = 0x07 + NAME = 'authenticated' + ARG_NAME = 'authenticated' + + +class Blake2AuthenticatedKey(ID_BLAKE2b_256, AuthenticatedKeyBase): + TYPE = 0x06 + NAME = 'authenticated BLAKE2b' + ARG_NAME = 'authenticated-blake2' + + AVAILABLE_KEY_TYPES = ( PlaintextKey, PassphraseKey, - KeyfileKey, RepoKey, - Blake2KeyfileKey, Blake2RepoKey, AuthenticatedKey, + KeyfileKey, RepoKey, AuthenticatedKey, + Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey, ) diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index 34399f9b..df7f81e5 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -8,8 +8,9 @@ import msgpack import pytest from ..crypto.key import Passphrase, PasswordRetriesExceeded, bin_to_hex -from ..crypto.key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, \ - AuthenticatedKey +from ..crypto.key import PlaintextKey, PassphraseKey, AuthenticatedKey, RepoKey, KeyfileKey, \ + Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey +from ..crypto.key import ID_HMAC_SHA_256, ID_BLAKE2b_256 from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError from ..crypto.key import identify_key from ..crypto.low_level import bytes_to_long, num_aes_blocks @@ -70,12 +71,13 @@ class TestKey: return tmpdir @pytest.fixture(params=( - KeyfileKey, PlaintextKey, + AuthenticatedKey, + KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, - AuthenticatedKey, + Blake2AuthenticatedKey, )) def key(self, request, monkeypatch): monkeypatch.setenv('BORG_PASSPHRASE', 'test') @@ -256,6 +258,19 @@ class TestKey: def test_authenticated_encrypt(self, monkeypatch): monkeypatch.setenv('BORG_PASSPHRASE', 'test') key = AuthenticatedKey.create(self.MockRepository(), self.MockArgs()) + assert AuthenticatedKey.id_hash is ID_HMAC_SHA_256.id_hash + assert len(key.id_key) == 32 + plaintext = b'123456789' + authenticated = key.encrypt(plaintext) + # 0x07 is the key TYPE, 0x0100 identifies LZ4 compression, 0x90 is part of LZ4 and means that an uncompressed + # block of length nine follows (the plaintext). + assert authenticated == b'\x07\x01\x00\x90' + plaintext + + def test_blake2_authenticated_encrypt(self, monkeypatch): + monkeypatch.setenv('BORG_PASSPHRASE', 'test') + key = Blake2AuthenticatedKey.create(self.MockRepository(), self.MockArgs()) + assert Blake2AuthenticatedKey.id_hash is ID_BLAKE2b_256.id_hash + assert len(key.id_key) == 128 plaintext = b'123456789' authenticated = key.encrypt(plaintext) # 0x06 is the key TYPE, 0x0100 identifies LZ4 compression, 0x90 is part of LZ4 and means that an uncompressed From f786211b12c0be760bf035e334cb3b84f827a7df Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 11 Jun 2017 19:44:33 +0200 Subject: [PATCH 1033/1387] RepositoryCache: truncate+unlink errored file --- src/borg/remote.py | 5 +++++ src/borg/testsuite/remote.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/borg/remote.py b/src/borg/remote.py index 16346f8d..a961384f 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -27,6 +27,7 @@ from .helpers import hostname_is_unique from .helpers import replace_placeholders from .helpers import sysinfo from .helpers import format_file_size +from .helpers import truncate_and_unlink from .logger import create_logger, setup_logging from .repository import Repository, MAX_OBJECT_SIZE, LIST_SCAN_LIMIT from .version import parse_version, format_version @@ -1144,6 +1145,10 @@ class RepositoryCache(RepositoryNoCache): with open(file, 'wb') as fd: fd.write(packed) except OSError as os_error: + try: + truncate_and_unlink(file) + except FileNotFoundError: + pass # open() could have failed as well if os_error.errno == errno.ENOSPC: self.enospc += 1 self.backoff() diff --git a/src/borg/testsuite/remote.py b/src/borg/testsuite/remote.py index dccfdaff..d9117717 100644 --- a/src/borg/testsuite/remote.py +++ b/src/borg/testsuite/remote.py @@ -141,6 +141,9 @@ class TestRepositoryCache: def write(self, data): raise OSError(errno.ENOSPC, 'foo') + def truncate(self, n=None): + pass + iterator = cache.get_many([H(1), H(2), H(3)]) assert next(iterator) == b'1234' From 3c6372f84161e7c4321d227240dcb98c39251a94 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 11 Jun 2017 19:46:12 +0200 Subject: [PATCH 1034/1387] cache sync: convert incoming integers to uint64_t --- src/borg/cache_sync/unpack.h | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/borg/cache_sync/unpack.h b/src/borg/cache_sync/unpack.h index 94172d42..d3380263 100644 --- a/src/borg/cache_sync/unpack.h +++ b/src/borg/cache_sync/unpack.h @@ -118,7 +118,7 @@ static inline void unpack_init_user_state(unpack_user *u) u->expect = expect_item_begin; } -static inline int unpack_callback_int64(unpack_user* u, int64_t d) +static inline int unpack_callback_uint64(unpack_user* u, int64_t d) { switch(u->expect) { case expect_size: @@ -135,40 +135,39 @@ static inline int unpack_callback_int64(unpack_user* u, int64_t d) return 0; } +static inline int unpack_callback_uint32(unpack_user* u, uint32_t d) +{ + return unpack_callback_uint64(u, d); +} + static inline int unpack_callback_uint16(unpack_user* u, uint16_t d) { - return unpack_callback_int64(u, d); + return unpack_callback_uint64(u, d); } static inline int unpack_callback_uint8(unpack_user* u, uint8_t d) { - return unpack_callback_int64(u, d); + return unpack_callback_uint64(u, d); } - -static inline int unpack_callback_uint32(unpack_user* u, uint32_t d) +static inline int unpack_callback_int64(unpack_user* u, uint64_t d) { - return unpack_callback_int64(u, d); -} - -static inline int unpack_callback_uint64(unpack_user* u, uint64_t d) -{ - return unpack_callback_int64(u, d); + return unpack_callback_uint64(u, d); } static inline int unpack_callback_int32(unpack_user* u, int32_t d) { - return unpack_callback_int64(u, d); + return unpack_callback_uint64(u, d); } static inline int unpack_callback_int16(unpack_user* u, int16_t d) { - return unpack_callback_int64(u, d); + return unpack_callback_uint64(u, d); } static inline int unpack_callback_int8(unpack_user* u, int8_t d) { - return unpack_callback_int64(u, d); + return unpack_callback_uint64(u, d); } /* Ain't got anything to do with those floats */ From 783a5926d61c823885009bbf2f16cc5564bfe5b2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 11 Jun 2017 20:23:17 +0200 Subject: [PATCH 1035/1387] cache sync: introduce BORG_NO_PYTHON textshell edition --- scripts/fuzz-cache-sync/main.c | 3 +++ src/borg/_hashindex.c | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/scripts/fuzz-cache-sync/main.c b/scripts/fuzz-cache-sync/main.c index b25d925d..c65dd272 100644 --- a/scripts/fuzz-cache-sync/main.c +++ b/scripts/fuzz-cache-sync/main.c @@ -1,3 +1,6 @@ + +#define BORG_NO_PYTHON + #include "../../src/borg/_hashindex.c" #include "../../src/borg/cache_sync/cache_sync.c" diff --git a/src/borg/_hashindex.c b/src/borg/_hashindex.c index 087dc273..14206efe 100644 --- a/src/borg/_hashindex.c +++ b/src/borg/_hashindex.c @@ -56,7 +56,7 @@ typedef struct { int lower_limit; int upper_limit; int min_empty; -#ifdef Py_PYTHON_H +#ifndef BORG_NO_PYTHON /* buckets may be backed by a Python buffer. If buckets_buffer.buf is NULL then this is not used. */ Py_buffer buckets_buffer; #endif @@ -108,7 +108,7 @@ static int hash_sizes[] = { #define EPRINTF(msg, ...) fprintf(stderr, "hashindex: " msg "(%s)\n", ##__VA_ARGS__, strerror(errno)) #define EPRINTF_PATH(path, msg, ...) fprintf(stderr, "hashindex: %s: " msg " (%s)\n", path, ##__VA_ARGS__, strerror(errno)) -#ifdef Py_PYTHON_H +#ifndef BORG_NO_PYTHON static HashIndex *hashindex_read(PyObject *file_py, int permit_compact); static void hashindex_write(HashIndex *index, PyObject *file_py); #endif @@ -126,7 +126,7 @@ static void hashindex_free(HashIndex *index); static void hashindex_free_buckets(HashIndex *index) { -#ifdef Py_PYTHON_H +#ifndef BORG_NO_PYTHON if(index->buckets_buffer.buf) { PyBuffer_Release(&index->buckets_buffer); } else @@ -272,7 +272,7 @@ count_empty(HashIndex *index) /* Public API */ -#ifdef Py_PYTHON_H +#ifndef BORG_NO_PYTHON static HashIndex * hashindex_read(PyObject *file_py, int permit_compact) { @@ -457,7 +457,7 @@ hashindex_init(int capacity, int key_size, int value_size) index->lower_limit = get_lower_limit(index->num_buckets); index->upper_limit = get_upper_limit(index->num_buckets); index->min_empty = get_min_empty(index->num_buckets); -#ifdef Py_PYTHON_H +#ifndef BORG_NO_PYTHON index->buckets_buffer.buf = NULL; #endif for(i = 0; i < capacity; i++) { @@ -473,7 +473,7 @@ hashindex_free(HashIndex *index) free(index); } -#ifdef Py_PYTHON_H +#ifndef BORG_NO_PYTHON static void hashindex_write(HashIndex *index, PyObject *file_py) { From 25bee21253b56d57d1c3f9e5c86d480835505225 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Jun 2017 16:54:55 +0200 Subject: [PATCH 1036/1387] docs: deployment: Automated backups to a local hard drive --- docs/borg_theme/css/borg.css | 7 + docs/deployment.rst | 1 + docs/deployment/automated-local.rst | 231 +++++++++++++++++++++++ docs/deployment/hosting-repositories.rst | 4 +- 4 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 docs/deployment/automated-local.rst diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index f97b5907..c5aa5720 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -51,4 +51,11 @@ dt code { /* fancy red-orange stripes */ border-image: repeating-linear-gradient( -45deg,red 0,red 10px,#ffa800 10px,#ffa800 20px,red 20px) 0 20 repeat; + +.topic { + margin: 0 1em; + padding: 0 1em; + /* #4e4a4a = background of the ToC sidebar */ + border-left: 2px solid #4e4a4a;; + border-right: 2px solid #4e4a4a;; } diff --git a/docs/deployment.rst b/docs/deployment.rst index e4fc728a..7b1caf92 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -11,3 +11,4 @@ This chapter details deployment strategies for the following scenarios. deployment/central-backup-server deployment/hosting-repositories + deployment/automated-local diff --git a/docs/deployment/automated-local.rst b/docs/deployment/automated-local.rst new file mode 100644 index 00000000..a64cec23 --- /dev/null +++ b/docs/deployment/automated-local.rst @@ -0,0 +1,231 @@ +.. include:: ../global.rst.inc +.. highlight:: none + +Automated backups to a local hard drive +======================================= + +This guide shows how to automate backups to a hard drive directly connected +to your computer. If a backup hard drive is connected, backups are automatically +started, and the drive shut-down and disconnected when they are done. + +This guide is written for a Linux-based operating system and makes use of +systemd and udev. + +Overview +-------- + +An udev rule is created to trigger on the addition of block devices. The rule contains a tag +that triggers systemd to start a oneshot service. The oneshot service executes a script in +the standard systemd service environment, which automatically captures stdout/stderr and +logs it to the journal. + +The script mounts the added block device, if it is a registered backup drive, and creates +backups on it. When done, it optionally unmounts the file system and spins the drive down, +so that it may be physically disconnected. + +Configuring the system +---------------------- + +First, create the ``/etc/backups`` directory (as root). +All configuration goes into this directory. + +Then, create ``etc/backups/40-backup.rules`` with the following content (all on one line):: + + ACTION=="add", SUBSYSTEM=="bdi", DEVPATH=="/devices/virtual/bdi/*", + TAG+="systemd", ENV{SYSTEMD_WANTS}="automatic-backup.service" + +.. topic:: Finding a more precise udev rule + + If you always connect the drive(s) to the same physical hardware path, e.g. the same + eSATA port, then you can make a more precise udev rule. + + Execute ``udevadm monitor`` and connect a drive to the port you intend to use. + You should see a flurry of events, find those regarding the `block` subsystem. + Pick the event whose device path ends in something similar to a device file name, + typically`sdX/sdXY`. Use the event's device path and replace `sdX/sdXY` after the + `/block/` part in the path with a star (\*). For example: + `DEVPATH=="/devices/pci0000:00/0000:00:11.0/ata3/host2/target2:0:0/2:0:0:0/block/*"`. + + Reboot a few times to ensure that the hardware path does not change: on some motherboards + components of it can be random. In these cases you cannot use a more accurate rule, + or need to insert additional stars for matching the path. + +The "systemd" tag in conjunction with the SYSTEMD_WANTS environment variable has systemd +launch the "automatic-backup" service, which we will create next, as the +``/etc/backups/automatic-backup.service`` file: + +.. code-block:: ini + + [Service] + Type=oneshot + ExecStart=/etc/backups/run.sh + +Now, create the main backup script, ``/etc/backups/run.sh``. Below is a template, +modify it to suit your needs (e.g. more backup sets, dumping databases etc.). + +.. code-block:: bash + + #!/bin/bash -ue + + # The udev rule is not terribly accurate and may trigger our service before + # the kernel has finished probing partitions. Sleep for a bit to ensure + # the kernel is done. + # + # This can be avoided by using a more precise udev rule, e.g. matching + # a specific hardware path and partition. + sleep 5 + + # + # Script configuration + # + + # The backup partition is mounted there + MOUNTPOINT=/mnt/backup + + # This is the location of the Borg repository + TARGET=$MOUNTPOINT/borg-backups/backup.borg + + # Archive name schema + DATE=$(date --iso-8601)-$(hostname) + + # This is the file that will later contain UUIDs of registered backup drives + DISKS=/etc/backups/backup.disks + + # Find whether the connected block device is a backup drive + for uuid in $(lsblk --noheadings --list --output uuid) + do + if grep --quiet --fixed-strings $uuid $DISKS; then + break + fi + uuid= + done + + if [ ! $uuid ]; then + echo "No backup disk found, exiting" + exit 0 + fi + + echo "Disk $uuid is a backup disk" + partition_path=/dev/disk/by-uuid/$uuid + # Mount file system if not already done. This assumes that if something is already + # mounted at $MOUNTPOINT, it is the backup drive. It won't find the drive if + # it was mounted somewhere else. + (mount | grep $MOUNTPOINT) || mount $partition_path $MOUNTPOINT + drive=$(lsblk --inverse --noheadings --list --paths --output name $partition_path | head --lines 1) + echo "Drive path: $drive" + + # + # Create backups + # + + # Options for borg create + BORG_OPTS="--stats --one-file-system --compression lz4 --checkpoint-interval 86400" + + # Set BORG_PASSPHRASE or BORG_PASSCOMMAND somewhere around here, using export, + # if encryption is used. + + # No one can answer if Borg asks these questions, it is better to just fail quickly + # instead of hanging. + export BORG_RELOCATED_REPO_ACCESS_IS_OK=no + export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=no + + # Log Borg version + borg --version + + echo "Starting backup for $DATE" + + # This is just an example, change it however you see fit + borg create $BORG_OPTS \ + --exclude /root/.cache \ + --exclude /var/cache \ + --exclude /var/lib/docker/devicemapper \ + $TARGET::$DATE-$$-system \ + / /boot + + # /home is often a separate partition / file system. + # Even if it isn't (add --exclude /home above), it probably makes sense + # to have /home in a separate archive. + borg create $BORG_OPTS \ + --exclude 'sh:/home/*/.cache' \ + $TARGET::$DATE-$$-home \ + /home/ + + echo "Completed backup for $DATE" + + # Just to be completely paranoid + sync + + if [ -f /etc/backups/autoeject ]; then + umount $MOUNTPOINT + hdparm -Y $drive + fi + + if [ -f /etc/backups/backup-suspend ]; then + systemctl suspend + fi + +Create the ``/etc/backups/autoeject`` file to have the script automatically eject the drive +after creating the backup. Rename the file to something else (e.g. ``/etc/backup/autoeject-no``) +when you want to do something with the drive after creating backups (e.g running check). + +Create the ``/etc/backups/backup-suspend`` file if the machine should suspend after completing +the backup. Don't forget to physically disconnect the device before resuming, +otherwise you'll enter a cycle. You can also add an option to power down instead. + +Create an empty ``/etc/backups/backup.disks`` file, you'll register your backup drives +there. + +The last part is to actually enable the udev rules and services: + +.. code-block:: bash + + ln -s /etc/backups/40-backup.rules /etc/udev/rules.d/40-backup.rules + ln -s /etc/backups/automatic-backup.service /etc/systemd/system/automatic-backup.service + systemctl daemon-reload + udevadm control --reload + +Adding backup hard drives +------------------------- + +Connect your backup hard drive. Format it, if not done already. +Find the UUID of the file system that backups should be stored on:: + + lsblk -o+uuid,label + +Note the UUID into the ``/etc/backup/backup.disks`` file. + +Mount the drive to /mnt/backup. + +Initialize a Borg repository at the location indicated by ``TARGET``:: + + borg init --encryption ... /mnt/backup/borg-backups/backup.borg + +Unmount and reconnect the drive, or manually start the ``automatic-backup`` service +to start the first backup:: + + systemctl start --no-block automatic-backup + +See backup logs using journalctl:: + + journalctl -fu automatic-backup [-n number-of-lines] + +Security considerations +----------------------- + +The script as shown above will mount any file system with an UUID listed in +``/etc/backup/backup.disks``. The UUID check is a safety / annoyance-reduction +mechanism to keep the script from blowing up whenever a random USB thumb drive is connected. +It is not meant as a security mechanism. Mounting file systems and reading repository +data exposes additional attack surfaces (kernel file system drivers, +possibly user space services and Borg itself). On the other hand, someone +standing right next to your computer can attempt a lot of attacks, most of which +are easier to do than e.g. exploiting file systems (installing a physical key logger, +DMA attacks, stealing the machine, ...). + +Borg ensures that backups are not created on random drives that "just happen" +to contain a Borg repository. If an unknown unencrypted repository is encountered, +then the script aborts (BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=no). + +Backups are only created on hard drives that contain a Borg repository that is +either known (by ID) to your machine or you are using encryption and the +passphrase of the repository has to match the passphrase supplied to Borg. diff --git a/docs/deployment/hosting-repositories.rst b/docs/deployment/hosting-repositories.rst index e502d644..ae9a4e1e 100644 --- a/docs/deployment/hosting-repositories.rst +++ b/docs/deployment/hosting-repositories.rst @@ -55,7 +55,7 @@ multiple times to permit access to more than one repository. The repository may not exist yet; it can be initialized by the user, which allows for encryption. -Storage quotas can be enabled by adding the ``--storage-quota`` option +**Storage quotas** can be enabled by adding the ``--storage-quota`` option to the ``borg serve`` command line:: restrict,command="borg serve --storage-quota 20G ..." ... @@ -70,4 +70,4 @@ support storage quotas. Refer to :ref:`internals_storage_quota` for more details on storage quotas. Refer to the `sshd(8) `_ -for more details on securing SSH. +man page for more details on SSH options. From 501859ca330744f1bf485adc502edb59d61b8748 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 9 Jun 2017 18:43:24 +0200 Subject: [PATCH 1037/1387] docs: less bothersome experimental stripes --- docs/borg_theme/css/borg.css | 5 +++-- docs/usage/general.rst | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index c5aa5720..3cb50522 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -48,9 +48,10 @@ dt code { /* fallback for browsers that don't have repeating-linear-gradient: thick, red lines */ border-left: 20px solid red; border-right: 20px solid red; - /* fancy red-orange stripes */ + /* fancy red stripes */ border-image: repeating-linear-gradient( - -45deg,red 0,red 10px,#ffa800 10px,#ffa800 20px,red 20px) 0 20 repeat; + -45deg,rgba(255,0,0,0.1) 0,rgba(255,0,0,0.75) 10px,rgba(0,0,0,0) 10px,rgba(0,0,0,0) 20px,rgba(255,0,0,0.75) 20px) 0 20 repeat; +} .topic { margin: 0 1em; diff --git a/docs/usage/general.rst b/docs/usage/general.rst index 062d89c8..77b1b9ac 100644 --- a/docs/usage/general.rst +++ b/docs/usage/general.rst @@ -7,7 +7,7 @@ The following sections will describe each command in detail. .. container:: experimental - Experimental features are marked with red-orange stripes on the sides, like this paragraph. + Experimental features are marked with red stripes on the sides, like this paragraph. Experimental features are not stable, which means that they may be changed in incompatible ways or even removed entirely without prior notice in following releases. From a6dc6b611be4c44fe2e51c82691938dfb50da3a6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 12 Jun 2017 03:04:31 +0200 Subject: [PATCH 1038/1387] Vagrantfile: backslash needs escaping --- Vagrantfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index 10f8cec2..71b8a114 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -270,7 +270,7 @@ def install_borg(fuse) cd borg # clean up (wrong/outdated) stuff we likely got via rsync: rm -rf __pycache__ - find src -name '__pycache__' -exec rm -rf {} \; + find src -name '__pycache__' -exec rm -rf {} \\; pip install -r requirements.d/development.txt python setup.py clean EOF From dff5f2041e67d1870f3792424febd7f6dbb983fa Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 12 Jun 2017 03:29:37 +0200 Subject: [PATCH 1039/1387] vagrant: add OpenIndiana --- Vagrantfile | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Vagrantfile b/Vagrantfile index 71b8a114..0c670430 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -161,6 +161,16 @@ def packages_netbsd EOF end +def packages_openindiana + return <<-EOF + #pkg update # XXX needs separate provisioning step + reboot + pkg install python-34 clang-3.4 lz4 git + python3 -m ensurepip + pip3 install -U setuptools pip wheel virtualenv + touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile + EOF +end + # Install required cygwin packages and configure environment # # Microsoft/EdgeOnWindows10 image has MLS-OpenSSH installed by default, @@ -528,6 +538,19 @@ Vagrant.configure(2) do |config| b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("netbsd64") end + # rsync on openindiana has troubles, does not set correct owner for /vagrant/borg and thus gives lots of + # permission errors. can be manually fixed in the VM by: sudo chown -R vagrant /vagrant/borg ; then rsync again. + config.vm.define "openindiana64" do |b| + b.vm.box = "openindiana/hipster" + b.vm.provider :virtualbox do |v| + v.memory = 1536 + $wmem + end + b.vm.provision "packages openindiana", :type => :shell, :inline => packages_openindiana + b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("openindiana64") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(false) + b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("openindiana64") + end + config.vm.define "windows10" do |b| b.vm.box = "Microsoft/EdgeOnWindows10" b.vm.guest = :windows From 78cbf695c4430286283ca5f468e4a758aa2666cd Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 12 Jun 2017 10:53:55 +0200 Subject: [PATCH 1040/1387] cache sync: suppress GCC C90/C99 int literal warning warning: this decimal constant is unsigned only in ISO C90 Raised by GCC 4.9.2 on PowerPC. The warning is bogus here due to the immediate explicit cast; newer versions don't emit it. --- src/borg/cache_sync/unpack.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/cache_sync/unpack.h b/src/borg/cache_sync/unpack.h index d3380263..4a0ba1d4 100644 --- a/src/borg/cache_sync/unpack.h +++ b/src/borg/cache_sync/unpack.h @@ -26,7 +26,7 @@ #include "unpack_define.h" // 2**32 - 1025 -#define _MAX_VALUE ( (uint32_t) 4294966271 ) +#define _MAX_VALUE ( (uint32_t) 4294966271UL ) #define MIN(x, y) ((x) < (y) ? (x): (y)) From 88e937d0f9dcbdbd3afe2dfc2a8151d13459d472 Mon Sep 17 00:00:00 2001 From: philippje Date: Mon, 12 Jun 2017 22:17:29 +0200 Subject: [PATCH 1041/1387] changed the date of day without backup Changed from 20. December to 19. December for easier comprehension (viewing the calendar.) The missing 'd' at 20. December is hardly noticeable compared to e.g. the 19. December. --- docs/misc/prune-example.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/misc/prune-example.txt b/docs/misc/prune-example.txt index ac608b6a..12ffeb6f 100644 --- a/docs/misc/prune-example.txt +++ b/docs/misc/prune-example.txt @@ -2,7 +2,7 @@ borg prune visualized ===================== Assume it is 2016-01-01, today's backup has not yet been made and you have -created at least one backup on each day in 2015 except on 2015-12-20 (no +created at least one backup on each day in 2015 except on 2015-12-19 (no backup made on that day). This is what borg prune --keep-daily 14 --keep-monthly 6 would keep. @@ -45,7 +45,7 @@ Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su 1 2 3 4 1 1 2 3 4 5 6 5 6 7 8 9 10 11 2 3 4 5 6 7 8 7 8 9 10 11 12 13 -12 13 14 15 16 17 18 9 10 11 12 13 14 15 14 15 16 17d18d19d20 +12 13 14 15 16 17 18 9 10 11 12 13 14 15 14 15 16 17d18d19 20d 19 20 21 22 23 24 25 16 17 18 19 20 21 22 21d22d23d24d25d26d27d 26 27 28 29 30 31m 23 24 25 26 27 28 29 28d29d30d31d 30m @@ -66,8 +66,8 @@ List view 9. 2015-12-23 10. 2015-12-22 11. 2015-12-21 - (no backup made on 2015-12-20) -12. 2015-12-19 +12. 2015-12-20 + (no backup made on 2015-12-19) 13. 2015-12-18 14. 2015-12-17 @@ -83,7 +83,7 @@ Jun. December is not considered for this rule, because that backup was already kept because of the daily rule. 2015-12-17 is kept to satisfy the --keep-daily 14 rule - because no backup was -made on 2015-12-20. If a backup had been made on that day, it would not keep +made on 2015-12-19. If a backup had been made on that day, it would not keep the one from 2015-12-17. We did not include yearly, weekly, hourly, minutely or secondly rules to keep From c9c227f2ca794269cdf14d21f075c637e5485051 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 11 Jun 2017 12:37:20 +0200 Subject: [PATCH 1042/1387] cache sync: check Operation.READ compatibility with manifest --- src/borg/cache.py | 5 +++++ src/borg/testsuite/archiver.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/src/borg/cache.py b/src/borg/cache.py index 0dba8ecc..70d6f029 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -721,6 +721,11 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" except: pass + # The cache can be used by a command that e.g. only checks against Manifest.Operation.WRITE, + # which does not have to include all flags from Manifest.Operation.READ. + # Since the sync will attempt to read archives, check compatibility with Manifest.Operation.READ. + self.manifest.check_repository_compatibility((Manifest.Operation.READ, )) + self.begin_txn() with cache_if_remote(self.repository, decrypted_cache=self.key) as decrypted_repository: legacy_cleanup() diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 6d0b28c4..e9bcef5e 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1440,6 +1440,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.add_unknown_feature(Manifest.Operation.WRITE) self.cmd_raises_unknown_feature(['create', self.repository_location + '::test', 'input']) + def test_unknown_feature_on_cache_sync(self): + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('delete', '--cache-only', self.repository_location) + self.add_unknown_feature(Manifest.Operation.READ) + self.cmd_raises_unknown_feature(['create', self.repository_location + '::test', 'input']) + def test_unknown_feature_on_change_passphrase(self): print(self.cmd('init', '--encryption=repokey', self.repository_location)) self.add_unknown_feature(Manifest.Operation.CHECK) From 0b00c14c277b6480feb476e90f7ece5cf194ceff Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 13 Jun 2017 01:12:14 +0200 Subject: [PATCH 1043/1387] don't write to disk with --stdout, fixes #2645 if we always give stdout to extract_item(), it gets into the stdout- processing branch which only emits data from items that have chunks and does nothing for items which don't. --- src/borg/archiver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 2138cde9..e4487350 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -704,7 +704,7 @@ class Archiver: else: if stat.S_ISDIR(item.mode): dirs.append(item) - archive.extract_item(item, restore_attrs=False) + archive.extract_item(item, stdout=stdout, restore_attrs=False) else: archive.extract_item(item, stdout=stdout, sparse=sparse, hardlink_masters=hardlink_masters, stripped_components=strip_components, original_path=orig_path, pi=pi) @@ -721,7 +721,7 @@ class Archiver: pi.show() dir_item = dirs.pop(-1) try: - archive.extract_item(dir_item) + archive.extract_item(dir_item, stdout=stdout) except BackupOSError as e: self.print_warning('%s: %s', remove_surrogates(dir_item.path), e) for pattern in matcher.get_unmatched_include_patterns(): From 4490a8bbc353ebeb882780ccc1c8755c9df4b602 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 13 Jun 2017 11:16:04 +0200 Subject: [PATCH 1044/1387] cache sync: don't do memcpy(..., 0, 0) !ctx->buf => ctx->tail - ctx->head == 0 --- src/borg/cache_sync/cache_sync.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/borg/cache_sync/cache_sync.c b/src/borg/cache_sync/cache_sync.c index e4bc653b..70f568f8 100644 --- a/src/borg/cache_sync/cache_sync.c +++ b/src/borg/cache_sync/cache_sync.c @@ -87,8 +87,10 @@ cache_sync_feed(CacheSyncCtx *ctx, void *data, uint32_t length) ctx->ctx.user.last_error = "cache_sync_feed: unable to allocate buffer"; return 0; } - memcpy(new_buf, ctx->buf + ctx->head, ctx->tail - ctx->head); - free(ctx->buf); + if(ctx->buf) { + memcpy(new_buf, ctx->buf + ctx->head, ctx->tail - ctx->head); + free(ctx->buf); + } ctx->buf = new_buf; ctx->tail -= ctx->head; ctx->head = 0; From 944a4abd58da801c68845ce00e4eab7818bef917 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 13 Jun 2017 11:42:43 +0200 Subject: [PATCH 1045/1387] chunker: don't do uint32_t >> 32 --- src/borg/_chunker.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/_chunker.c b/src/borg/_chunker.c index f9f598bb..b72f6bd8 100644 --- a/src/borg/_chunker.c +++ b/src/borg/_chunker.c @@ -63,7 +63,7 @@ static uint32_t table_base[] = 0xc5ae37bb, 0xa76ce12a, 0x8150d8f3, 0x2ec29218, 0xa35f0984, 0x48c0647e, 0x0b5ff98c, 0x71893f7b }; -#define BARREL_SHIFT(v, shift) ( ((v) << shift) | ((v) >> (32 - shift)) ) +#define BARREL_SHIFT(v, shift) ( ((v) << shift) | ((v) >> ((32 - shift) & 0x1f)) ) size_t pagemask; From e189a4d3029763384d65e4086706c519ec3cc4f0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 13 Jun 2017 14:15:37 +0200 Subject: [PATCH 1046/1387] info: use CacheSynchronizer & HashIndex.stats_against --- src/borg/archive.py | 32 ++++++++------------ src/borg/cache_sync/cache_sync.c | 9 +++++- src/borg/cache_sync/unpack.h | 3 ++ src/borg/hashindex.pyx | 51 ++++++++++++++++++++++++++++++-- src/borg/helpers.py | 2 +- 5 files changed, 73 insertions(+), 24 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index a0bde175..91239bdc 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -25,7 +25,7 @@ from .cache import ChunkListEntry from .crypto.key import key_factory from .compress import Compressor, CompressionSpec from .constants import * # NOQA -from .hashindex import ChunkIndex, ChunkIndexEntry +from .hashindex import ChunkIndex, ChunkIndexEntry, CacheSynchronizer from .helpers import Manifest from .helpers import hardlinkable from .helpers import ChunkIteratorFileWrapper, open_item @@ -478,30 +478,22 @@ Utilization of max. archive size: {csize_max:.0%} def calc_stats(self, cache): def add(id): - count, size, csize = cache.chunks[id] - stats.update(size, csize, count == 1) - cache.chunks[id] = count - 1, size, csize + entry = cache.chunks[id] + archive_index.add(id, 1, entry.size, entry.csize) - def add_file_chunks(chunks): - for id, _, _ in chunks: - add(id) - - # This function is a bit evil since it abuses the cache to calculate - # the stats. The cache transaction must be rolled back afterwards - unpacker = msgpack.Unpacker(use_list=False) - cache.begin_txn() - stats = Statistics() + archive_index = ChunkIndex() + sync = CacheSynchronizer(archive_index) add(self.id) + pi = ProgressIndicatorPercent(total=len(self.metadata.items), msg='Calculating statistics... %3d%%') for id, chunk in zip(self.metadata.items, self.repository.get_many(self.metadata.items)): + pi.show(increase=1) add(id) data = self.key.decrypt(id, chunk) - unpacker.feed(data) - for item in unpacker: - chunks = item.get(b'chunks') - if chunks is not None: - stats.nfiles += 1 - add_file_chunks(chunks) - cache.rollback() + sync.feed(data) + stats = Statistics() + stats.osize, stats.csize, unique_size, stats.usize, unique_chunks, chunks = archive_index.stats_against(cache.chunks) + stats.nfiles = sync.num_files + pi.finish() return stats @contextmanager diff --git a/src/borg/cache_sync/cache_sync.c b/src/borg/cache_sync/cache_sync.c index 70f568f8..53b61552 100644 --- a/src/borg/cache_sync/cache_sync.c +++ b/src/borg/cache_sync/cache_sync.c @@ -38,6 +38,7 @@ cache_sync_init(HashIndex *chunks) unpack_init(&ctx->ctx); /* needs to be set only once */ ctx->ctx.user.chunks = chunks; + ctx->ctx.user.num_files = 0; ctx->buf = NULL; ctx->head = 0; ctx->tail = 0; @@ -56,11 +57,17 @@ cache_sync_free(CacheSyncCtx *ctx) } static const char * -cache_sync_error(CacheSyncCtx *ctx) +cache_sync_error(const CacheSyncCtx *ctx) { return ctx->ctx.user.last_error; } +static uint64_t +cache_sync_num_files(const CacheSyncCtx *ctx) +{ + return ctx->ctx.user.num_files; +} + /** * feed data to the cache synchronizer * 0 = abort, 1 = continue diff --git a/src/borg/cache_sync/unpack.h b/src/borg/cache_sync/unpack.h index 4a0ba1d4..8332fcff 100644 --- a/src/borg/cache_sync/unpack.h +++ b/src/borg/cache_sync/unpack.h @@ -50,6 +50,8 @@ typedef struct unpack_user { HashIndex *chunks; + uint64_t num_files; + /* * We don't care about most stuff. This flag tells us whether we're at the chunks structure, * meaning: @@ -358,6 +360,7 @@ static inline int unpack_callback_raw(unpack_user* u, const char* b, const char* if(length == 6 && !memcmp("chunks", p, 6)) { u->expect = expect_chunks_begin; u->inside_chunks = 1; + u->num_files++; } else { u->expect = expect_map_item_end; } diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 084518f9..bf84d6d4 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -9,7 +9,7 @@ from libc.errno cimport errno from cpython.exc cimport PyErr_SetFromErrnoWithFilename from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release -API_VERSION = '1.1_03' +API_VERSION = '1.1_04' cdef extern from "_hashindex.c": @@ -38,7 +38,8 @@ cdef extern from "cache_sync/cache_sync.c": pass CacheSyncCtx *cache_sync_init(HashIndex *chunks) - const char *cache_sync_error(CacheSyncCtx *ctx) + const char *cache_sync_error(const CacheSyncCtx *ctx) + uint64_t cache_sync_num_files(const CacheSyncCtx *ctx) int cache_sync_feed(CacheSyncCtx *ctx, void *data, uint32_t length) void cache_sync_free(CacheSyncCtx *ctx) @@ -329,6 +330,48 @@ cdef class ChunkIndex(IndexBase): return size, csize, unique_size, unique_csize, unique_chunks, chunks + def stats_against(self, ChunkIndex master_index): + """ + Calculate chunk statistics of this index against *master_index*. + + A chunk is counted as unique if the number of references + in this index matches the number of references in *master_index*. + + This index must be a subset of *master_index*. + + Return the same statistics tuple as summarize: + size, csize, unique_size, unique_csize, unique_chunks, chunks. + """ + cdef uint64_t size = 0, csize = 0, unique_size = 0, unique_csize = 0, chunks = 0, unique_chunks = 0 + cdef uint32_t our_refcount, chunk_size, chunk_csize + cdef const uint32_t *our_values + cdef const uint32_t *master_values + cdef const void *key = NULL + cdef HashIndex *master = master_index.index + + while True: + key = hashindex_next_key(self.index, key) + if not key: + break + our_values = (key + self.key_size) + master_values = hashindex_get(master, key) + if not master_values: + raise ValueError('stats_against: key contained in self but not in master_index.') + our_refcount = _le32toh(our_values[0]) + chunk_size = _le32toh(master_values[1]) + chunk_csize = _le32toh(master_values[2]) + + chunks += our_refcount + size += chunk_size * our_refcount + csize += chunk_csize * our_refcount + if our_values[0] == master_values[0]: + # our refcount equals the master's refcount, so this chunk is unique to us + unique_chunks += 1 + unique_size += chunk_size + unique_csize += chunk_csize + + return size, csize, unique_size, unique_csize, unique_chunks, chunks + def add(self, key, refs, size, csize): assert len(key) == self.key_size cdef uint32_t[3] data @@ -420,3 +463,7 @@ cdef class CacheSynchronizer: error = cache_sync_error(self.sync) if error != NULL: raise ValueError('cache_sync_feed failed: ' + error.decode('ascii')) + + @property + def num_files(self): + return cache_sync_num_files(self.sync) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index f65aac9b..fdacd141 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -131,7 +131,7 @@ class MandatoryFeatureUnsupported(Error): def check_extension_modules(): from . import platform, compress, item - if hashindex.API_VERSION != '1.1_03': + if hashindex.API_VERSION != '1.1_04': raise ExtensionModuleError if chunker.API_VERSION != '1.1_01': raise ExtensionModuleError From ccd066f0af67b6addfd62e7b015968fec44b7403 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 13 Jun 2017 18:30:42 +0200 Subject: [PATCH 1047/1387] FUSE: fix negative uid/gid crash, fixes #2674 they could come into archives e.g. when backing up external drives under cygwin. --- src/borg/fuse.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 4441c406..ffcce63f 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -70,7 +70,9 @@ class FuseOperations(llfuse.Operations): self.items = {} self.parent = {} self.contents = defaultdict(dict) - self.default_dir = Item(mode=0o40755, mtime=int(time.time() * 1e9), uid=os.getuid(), gid=os.getgid()) + self.default_uid = os.getuid() + self.default_gid = os.getgid() + self.default_dir = Item(mode=0o40755, mtime=int(time.time() * 1e9), uid=self.default_uid, gid=self.default_gid) self.pending_archives = {} self.cache = ItemCache() data_cache_capacity = int(os.environ.get('BORG_MOUNT_DATA_CACHE_ENTRIES', os.cpu_count() or 1)) @@ -263,8 +265,8 @@ class FuseOperations(llfuse.Operations): entry.attr_timeout = 300 entry.st_mode = item.mode entry.st_nlink = item.get('nlink', 1) - entry.st_uid = item.uid - entry.st_gid = item.gid + entry.st_uid = item.uid if item.uid >= 0 else self.default_uid + entry.st_gid = item.gid if item.gid >= 0 else self.default_gid entry.st_rdev = item.get('rdev', 0) entry.st_size = item.get_size() entry.st_blksize = 512 From c791921951ace5d4f08fab510fb5d55ece69d168 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 13 Jun 2017 20:03:39 +0200 Subject: [PATCH 1048/1387] fuse: instrument caches note: signal processing can be arbitrarily delayed; Python processes signals as soon as the code returns into the interpreter loop, which doesn't happen unless libfuse returns control, i.e. some request has been sent to the file system. --- src/borg/fuse.py | 21 +++++++++++++++++++-- src/borg/remote.py | 8 +++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 4441c406..b561bbb5 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -2,6 +2,7 @@ import errno import io import os import stat +import sys import tempfile import time from collections import defaultdict @@ -16,7 +17,7 @@ from .logger import create_logger logger = create_logger() from .archive import Archive -from .helpers import daemonize, hardlinkable +from .helpers import daemonize, hardlinkable, signal_handler, format_file_size from .item import Item from .lrucache import LRUCache @@ -97,6 +98,20 @@ class FuseOperations(llfuse.Operations): self.contents[1][os.fsencode(name)] = archive_inode self.pending_archives[archive_inode] = archive + def sig_info_handler(self, sig_no, stack): + logger.debug('fuse: %d inodes, %d synth inodes, %d edges (%s)', + self._inode_count, len(self.items), len(self.parent), + # getsizeof is the size of the dict itself; key and value are two small-ish integers, + # which are shared due to code structure (this has been verified). + format_file_size(sys.getsizeof(self.parent) + len(self.parent) * sys.getsizeof(self._inode_count))) + logger.debug('fuse: %d pending archives', len(self.pending_archives)) + logger.debug('fuse: ItemCache %d entries, %s', + self._inode_count - len(self.items), + format_file_size(os.stat(self.cache.fd.fileno()).st_size)) + logger.debug('fuse: data cache: %d/%d entries, %s', len(self.data_cache.items()), self.data_cache._capacity, + format_file_size(sum(len(chunk) for key, chunk in self.data_cache.items()))) + self.repository.log_instrumentation() + def mount(self, mountpoint, mount_options, foreground=False): """Mount filesystem on *mountpoint* with *mount_options*.""" options = ['fsname=borgfs', 'ro'] @@ -124,7 +139,9 @@ class FuseOperations(llfuse.Operations): # mirror. umount = False try: - signal = fuse_main() + with signal_handler('SIGUSR1', self.sig_info_handler), \ + signal_handler('SIGINFO', self.sig_info_handler): + signal = fuse_main() # no crash and no signal (or it's ^C and we're in the foreground) -> umount request umount = (signal is None or (signal == SIGINT and foreground)) finally: diff --git a/src/borg/remote.py b/src/borg/remote.py index a961384f..670179a4 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -1088,6 +1088,9 @@ class RepositoryNoCache: for key, data in zip(keys, self.repository.get_many(keys)): yield self.transform(key, data) + def log_instrumentation(self): + pass + class RepositoryCache(RepositoryNoCache): """ @@ -1161,12 +1164,15 @@ class RepositoryCache(RepositoryNoCache): self.backoff() return transformed - def close(self): + def log_instrumentation(self): logger.debug('RepositoryCache: current items %d, size %s / %s, %d hits, %d misses, %d slow misses (+%.1fs), ' '%d evictions, %d ENOSPC hit', len(self.cache), format_file_size(self.size), format_file_size(self.size_limit), self.hits, self.misses, self.slow_misses, self.slow_lat, self.evictions, self.enospc) + + def close(self): + self.log_instrumentation() self.cache.clear() shutil.rmtree(self.basedir) From 879f72f227f519827f455d11615fb49e69c64f3a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 13 Jun 2017 23:06:00 +0200 Subject: [PATCH 1049/1387] fuse: log process_archive timing the easier alternative to "/bin/time stat mountpoint//..." --- src/borg/fuse.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index b561bbb5..731197ce 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -160,6 +160,7 @@ class FuseOperations(llfuse.Operations): """ self.file_versions = {} # for versions mode: original path -> version unpacker = msgpack.Unpacker() + t0 = time.perf_counter() for key, chunk in zip(archive.metadata.items, self.repository.get_many(archive.metadata.items)): data = self.key.decrypt(key, chunk) unpacker.feed(data) @@ -183,6 +184,8 @@ class FuseOperations(llfuse.Operations): for segment in segments[:-1]: parent = self.process_inner(segment, parent) self.process_leaf(segments[-1], item, parent, prefix, is_dir) + duration = time.perf_counter() - t0 + logger.debug('fuse: process_archive completed in %.1f s for archive %s', duration, archive.name) def process_leaf(self, name, item, parent, prefix, is_dir): def file_version(item): From ff05895b7e282c275c84ebd5224c63a8d7fd6f2e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 13 Jun 2017 23:14:52 +0200 Subject: [PATCH 1050/1387] fuse: don't keep all Archive() instances around they're only needed inside process_archive, and not needed in general for pending_archives. --- src/borg/fuse.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 731197ce..6239625c 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -81,22 +81,18 @@ class FuseOperations(llfuse.Operations): def _create_filesystem(self): self._create_dir(parent=1) # first call, create root dir (inode == 1) if self.args.location.archive: - archive = Archive(self.repository_uncached, self.key, self.manifest, self.args.location.archive, - consider_part_files=self.args.consider_part_files) - self.process_archive(archive) + self.process_archive(self.args.location.archive) else: archive_names = (x.name for x in self.manifest.archives.list_considering(self.args)) - for name in archive_names: - archive = Archive(self.repository_uncached, self.key, self.manifest, name, - consider_part_files=self.args.consider_part_files) + for archive_name in archive_names: if self.versions: # process archives immediately - self.process_archive(archive) + self.process_archive(archive_name) else: # lazy load archives, create archive placeholder inode archive_inode = self._create_dir(parent=1) - self.contents[1][os.fsencode(name)] = archive_inode - self.pending_archives[archive_inode] = archive + self.contents[1][os.fsencode(archive_name)] = archive_inode + self.pending_archives[archive_inode] = archive_name def sig_info_handler(self, sig_no, stack): logger.debug('fuse: %d inodes, %d synth inodes, %d edges (%s)', @@ -155,12 +151,14 @@ class FuseOperations(llfuse.Operations): self.parent[ino] = parent return ino - def process_archive(self, archive, prefix=[]): + def process_archive(self, archive_name, prefix=[]): """Build fuse inode hierarchy from archive metadata """ self.file_versions = {} # for versions mode: original path -> version - unpacker = msgpack.Unpacker() t0 = time.perf_counter() + unpacker = msgpack.Unpacker() + archive = Archive(self.repository_uncached, self.key, self.manifest, archive_name, + consider_part_files=self.args.consider_part_files) for key, chunk in zip(archive.metadata.items, self.repository.get_many(archive.metadata.items)): data = self.key.decrypt(key, chunk) unpacker.feed(data) @@ -314,9 +312,9 @@ class FuseOperations(llfuse.Operations): def _load_pending_archive(self, inode): # Check if this is an archive we need to load - archive = self.pending_archives.pop(inode, None) - if archive: - self.process_archive(archive, [os.fsencode(archive.name)]) + archive_name = self.pending_archives.pop(inode, None) + if archive_name: + self.process_archive(archive_name, [os.fsencode(archive_name)]) def lookup(self, parent_inode, name, ctx=None): self._load_pending_archive(parent_inode) From ec532304d20f12ec7e09f837d2c1097df71bff79 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 13 Jun 2017 23:36:12 +0200 Subject: [PATCH 1051/1387] fuse: remove unnecessary normpaths --- src/borg/fuse.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index f91a3044..a5742e03 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -166,7 +166,7 @@ class FuseOperations(llfuse.Operations): unpacker.feed(data) for item in unpacker: item = Item(internal_dict=item) - path = os.fsencode(os.path.normpath(item.path)) + path = os.fsencode(item.path) is_dir = stat.S_ISDIR(item.mode) if is_dir: try: @@ -208,14 +208,14 @@ class FuseOperations(llfuse.Operations): if version is not None: # regular file, with contents - maybe a hardlink master name = make_versioned_name(name, version) - path = os.fsencode(os.path.normpath(item.path)) + path = os.fsencode(item.path) self.file_versions[path] = version path = item.path del item.path # safe some space if 'source' in item and hardlinkable(item.mode): # a hardlink, no contents, is the hardlink master - source = os.fsencode(os.path.normpath(item.source)) + source = os.fsencode(item.source) if self.versions: # adjust source name with version version = self.file_versions[source] From f04119c246da698b5a7cae96d407af6e4d4674b2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 13 Jun 2017 23:52:17 +0200 Subject: [PATCH 1052/1387] fuse: ItemCache on top of object cache --- src/borg/fuse.py | 86 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 12 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index a5742e03..c1341f56 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -35,22 +35,76 @@ else: class ItemCache: - def __init__(self): + GROW_BY = 2 * 1024 * 1024 + + def __init__(self, repository, key): + self.repository = repository + self.key = key + self.data = bytearray() + self.writeptr = 0 self.fd = tempfile.TemporaryFile(prefix='borg-tmp') self.offset = 1000000 - def add(self, item): - pos = self.fd.seek(0, io.SEEK_END) - self.fd.write(msgpack.packb(item.as_dict())) - return pos + self.offset + def new_stream(self): + self.stream_offset = 0 + self.chunk_begin = 0 + self.chunk_length = 0 + self.current_item = b'' + + def set_current_id(self, chunk_id, chunk_length): + self.chunk_id = chunk_id + self.chunk_begin += self.chunk_length + self.chunk_length = chunk_length + + def write_bytes(self, msgpacked_bytes): + self.current_item += msgpacked_bytes + self.stream_offset += len(msgpacked_bytes) + + def unpacked(self): + msgpacked_bytes = self.current_item + self.current_item = b'' + self.last_context_sensitive = self.stream_offset - len(msgpacked_bytes) <= self.chunk_begin + self.last_length = len(msgpacked_bytes) + self.last_item = msgpacked_bytes + + def inode_for_current_item(self): + if self.writeptr + 37 >= len(self.data): + self.data = self.data + bytes(self.GROW_BY) + + if self.last_context_sensitive: + pos = self.fd.seek(0, io.SEEK_END) + self.fd.write(self.last_item) + self.data[self.writeptr:self.writeptr+9] = b'S' + pos.to_bytes(8, 'little') + self.writeptr += 9 + return self.writeptr - 9 + self.offset + else: + self.data[self.writeptr:self.writeptr+1] = b'I' + self.data[self.writeptr+1:self.writeptr+33] = self.chunk_id + last_item_offset = self.stream_offset - self.last_length + last_item_offset -= self.chunk_begin + self.data[self.writeptr+33:self.writeptr+37] = last_item_offset.to_bytes(4, 'little') + self.writeptr += 37 + return self.writeptr - 37 + self.offset def get(self, inode): offset = inode - self.offset if offset < 0: raise ValueError('ItemCache.get() called with an invalid inode number') - self.fd.seek(offset, io.SEEK_SET) - item = next(msgpack.Unpacker(self.fd, read_size=1024)) - return Item(internal_dict=item) + is_context_sensitive = self.data[offset] == ord(b'S') + + # print(is_context_sensitive) + if is_context_sensitive: + fd_offset = int.from_bytes(self.data[offset+1:offset+9], 'little') + self.fd.seek(fd_offset, io.SEEK_SET) + return Item(internal_dict=next(msgpack.Unpacker(self.fd, read_size=1024))) + else: + chunk_id = bytes(self.data[offset+1:offset+33]) + chunk_offset = int.from_bytes(self.data[offset+33:offset+37], 'little') + chunk = self.key.decrypt(chunk_id, next(self.repository.get_many([chunk_id]))) + data = memoryview(chunk)[chunk_offset:] + unpacker = msgpack.Unpacker() + unpacker.feed(data) + return Item(internal_dict=next(unpacker)) class FuseOperations(llfuse.Operations): @@ -75,7 +129,7 @@ class FuseOperations(llfuse.Operations): self.default_gid = os.getgid() self.default_dir = Item(mode=0o40755, mtime=int(time.time() * 1e9), uid=self.default_uid, gid=self.default_gid) self.pending_archives = {} - self.cache = ItemCache() + self.cache = ItemCache(cached_repo, key) data_cache_capacity = int(os.environ.get('BORG_MOUNT_DATA_CACHE_ENTRIES', os.cpu_count() or 1)) logger.debug('mount data cache capacity: %d chunks', data_cache_capacity) self.data_cache = LRUCache(capacity=data_cache_capacity, dispose=lambda _: None) @@ -103,8 +157,9 @@ class FuseOperations(llfuse.Operations): # which are shared due to code structure (this has been verified). format_file_size(sys.getsizeof(self.parent) + len(self.parent) * sys.getsizeof(self._inode_count))) logger.debug('fuse: %d pending archives', len(self.pending_archives)) - logger.debug('fuse: ItemCache %d entries, %s', + logger.debug('fuse: ItemCache %d entries, meta-array %s, dependent items %s', self._inode_count - len(self.items), + format_file_size(sys.getsizeof(self.cache.data)), format_file_size(os.stat(self.cache.fd.fileno()).st_size)) logger.debug('fuse: data cache: %d/%d entries, %s', len(self.data_cache.items()), self.data_cache._capacity, format_file_size(sum(len(chunk) for key, chunk in self.data_cache.items()))) @@ -161,10 +216,17 @@ class FuseOperations(llfuse.Operations): unpacker = msgpack.Unpacker() archive = Archive(self.repository_uncached, self.key, self.manifest, archive_name, consider_part_files=self.args.consider_part_files) + self.cache.new_stream() for key, chunk in zip(archive.metadata.items, self.repository.get_many(archive.metadata.items)): data = self.key.decrypt(key, chunk) + self.cache.set_current_id(key, len(data)) unpacker.feed(data) - for item in unpacker: + while True: + try: + item = unpacker.unpack(self.cache.write_bytes) + except msgpack.OutOfData: + break + self.cache.unpacked() item = Item(internal_dict=item) path = os.fsencode(item.path) is_dir = stat.S_ISDIR(item.mode) @@ -230,7 +292,7 @@ class FuseOperations(llfuse.Operations): item.nlink = item.get('nlink', 1) + 1 self.items[inode] = item else: - inode = self.cache.add(item) + inode = self.cache.inode_for_current_item() self.parent[inode] = parent if name: self.contents[parent][name] = inode From 9fd79a9e564ac8e8acf9c2b8ba8807e08ff43d47 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 14 Jun 2017 11:14:10 +0200 Subject: [PATCH 1053/1387] fuse: decrypted cache --- src/borg/archiver.py | 2 +- src/borg/fuse.py | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e4487350..23398bb3 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1279,7 +1279,7 @@ class Archiver: def _do_mount(self, args, repository, manifest, key): from .fuse import FuseOperations - with cache_if_remote(repository) as cached_repo: + with cache_if_remote(repository, decrypted_cache=key) as cached_repo: operations = FuseOperations(key, repository, manifest, args, cached_repo) logger.info("Mounting filesystem") try: diff --git a/src/borg/fuse.py b/src/borg/fuse.py index c1341f56..00cdd730 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -37,9 +37,8 @@ else: class ItemCache: GROW_BY = 2 * 1024 * 1024 - def __init__(self, repository, key): - self.repository = repository - self.key = key + def __init__(self, decrypted_repository): + self.decrypted_repository = decrypted_repository self.data = bytearray() self.writeptr = 0 self.fd = tempfile.TemporaryFile(prefix='borg-tmp') @@ -100,7 +99,7 @@ class ItemCache: else: chunk_id = bytes(self.data[offset+1:offset+33]) chunk_offset = int.from_bytes(self.data[offset+33:offset+37], 'little') - chunk = self.key.decrypt(chunk_id, next(self.repository.get_many([chunk_id]))) + csize, chunk = next(self.decrypted_repository.get_many([chunk_id])) data = memoryview(chunk)[chunk_offset:] unpacker = msgpack.Unpacker() unpacker.feed(data) @@ -114,10 +113,10 @@ class FuseOperations(llfuse.Operations): allow_damaged_files = False versions = False - def __init__(self, key, repository, manifest, args, cached_repo): + def __init__(self, key, repository, manifest, args, decrypted_repository): super().__init__() self.repository_uncached = repository - self.repository = cached_repo + self.decrypted_repository = decrypted_repository self.args = args self.manifest = manifest self.key = key @@ -129,7 +128,7 @@ class FuseOperations(llfuse.Operations): self.default_gid = os.getgid() self.default_dir = Item(mode=0o40755, mtime=int(time.time() * 1e9), uid=self.default_uid, gid=self.default_gid) self.pending_archives = {} - self.cache = ItemCache(cached_repo, key) + self.cache = ItemCache(decrypted_repository) data_cache_capacity = int(os.environ.get('BORG_MOUNT_DATA_CACHE_ENTRIES', os.cpu_count() or 1)) logger.debug('mount data cache capacity: %d chunks', data_cache_capacity) self.data_cache = LRUCache(capacity=data_cache_capacity, dispose=lambda _: None) @@ -163,7 +162,7 @@ class FuseOperations(llfuse.Operations): format_file_size(os.stat(self.cache.fd.fileno()).st_size)) logger.debug('fuse: data cache: %d/%d entries, %s', len(self.data_cache.items()), self.data_cache._capacity, format_file_size(sum(len(chunk) for key, chunk in self.data_cache.items()))) - self.repository.log_instrumentation() + self.decrypted_repository.log_instrumentation() def mount(self, mountpoint, mount_options, foreground=False): """Mount filesystem on *mountpoint* with *mount_options*.""" @@ -217,8 +216,7 @@ class FuseOperations(llfuse.Operations): archive = Archive(self.repository_uncached, self.key, self.manifest, archive_name, consider_part_files=self.args.consider_part_files) self.cache.new_stream() - for key, chunk in zip(archive.metadata.items, self.repository.get_many(archive.metadata.items)): - data = self.key.decrypt(key, chunk) + for key, (csize, data) in zip(archive.metadata.items, self.decrypted_repository.get_many(archive.metadata.items)): self.cache.set_current_id(key, len(data)) unpacker.feed(data) while True: From 3b928a455828ec15bf492fe599eb70c1ea326247 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 14 Jun 2017 12:15:46 +0200 Subject: [PATCH 1054/1387] fuse: refactor ItemCache --- src/borg/fuse.py | 258 ++++++++++++++++++++++----------- src/borg/lrucache.py | 8 + src/borg/testsuite/lrucache.py | 3 + 3 files changed, 182 insertions(+), 87 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 00cdd730..b2db0681 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -2,6 +2,7 @@ import errno import io import os import stat +import struct import sys import tempfile import time @@ -35,76 +36,162 @@ else: class ItemCache: - GROW_BY = 2 * 1024 * 1024 + """ + This is the "meat" of the file system's metadata storage. + + This class generates inode numbers that efficiently index items in archives, + and retrieves items from these inode numbers. + """ + + # Approximately ~230000 items (depends on the average number of items per metadata chunk) + # Since growing a bytearray has to copy it, growing it will converge to o(n), however, + # this is not yet relevant due to the swiftness of copying memory. If it becomes an issue, + # use an anonymous mmap and just resize that (or, if on 64 bit, make it so big you never need + # to resize it in the first place; that's free). + GROW_META_BY = 2 * 1024 * 1024 + + indirect_entry_struct = struct.Struct('=cII') + assert indirect_entry_struct.size == 9 def __init__(self, decrypted_repository): self.decrypted_repository = decrypted_repository - self.data = bytearray() - self.writeptr = 0 - self.fd = tempfile.TemporaryFile(prefix='borg-tmp') + # self.meta, the "meta-array" is a densely packed array of metadata about where items can be found. + # It is indexed by the inode number minus self.offset. (This is in a way eerily similar to how the first + # unices did this). + # The meta-array contains chunk IDs and item entries (described in inode_for_current_item). + # The chunk IDs are referenced by item entries through relative offsets, + # which are bounded by the metadata chunk size. + self.meta = bytearray() + # The current write offset in self.meta + self.write_offset = 0 + + # Offset added to meta-indices, resulting in an inode, + # or substracted from inodes, resulting in a meta-indices. self.offset = 1000000 - def new_stream(self): - self.stream_offset = 0 - self.chunk_begin = 0 - self.chunk_length = 0 - self.current_item = b'' + # A temporary file that contains direct items, i.e. items directly cached in this layer. + # These are items that span more than one chunk and thus cannot be efficiently cached + # by the object cache (self.decrypted_repository), which would require variable-length structures; + # possible but not worth the effort, see inode_for_current_item. + self.fd = tempfile.TemporaryFile(prefix='borg-tmp') - def set_current_id(self, chunk_id, chunk_length): - self.chunk_id = chunk_id - self.chunk_begin += self.chunk_length - self.chunk_length = chunk_length + # A small LRU cache for chunks requested by ItemCache.get() from the object cache, + # this significantly speeds up directory traversal and similar operations which + # tend to re-read the same chunks over and over. + # The capacity is kept low because increasing it does not provide any significant advantage, + # but makes LRUCache's square behaviour noticeable as well as consuming some memory. + self.chunks = LRUCache(capacity=10, dispose=lambda _: None) - def write_bytes(self, msgpacked_bytes): - self.current_item += msgpacked_bytes - self.stream_offset += len(msgpacked_bytes) - - def unpacked(self): - msgpacked_bytes = self.current_item - self.current_item = b'' - self.last_context_sensitive = self.stream_offset - len(msgpacked_bytes) <= self.chunk_begin - self.last_length = len(msgpacked_bytes) - self.last_item = msgpacked_bytes - - def inode_for_current_item(self): - if self.writeptr + 37 >= len(self.data): - self.data = self.data + bytes(self.GROW_BY) - - if self.last_context_sensitive: - pos = self.fd.seek(0, io.SEEK_END) - self.fd.write(self.last_item) - self.data[self.writeptr:self.writeptr+9] = b'S' + pos.to_bytes(8, 'little') - self.writeptr += 9 - return self.writeptr - 9 + self.offset - else: - self.data[self.writeptr:self.writeptr+1] = b'I' - self.data[self.writeptr+1:self.writeptr+33] = self.chunk_id - last_item_offset = self.stream_offset - self.last_length - last_item_offset -= self.chunk_begin - self.data[self.writeptr+33:self.writeptr+37] = last_item_offset.to_bytes(4, 'little') - self.writeptr += 37 - return self.writeptr - 37 + self.offset + # Instrumentation + # Count of indirect items, i.e. data is cached in the object cache, in this cache + self.indirect_items = 0 + # Count of direct items, i.e. data is in self.fd + self.direct_items = 0 def get(self, inode): offset = inode - self.offset if offset < 0: raise ValueError('ItemCache.get() called with an invalid inode number') - is_context_sensitive = self.data[offset] == ord(b'S') - - # print(is_context_sensitive) - if is_context_sensitive: - fd_offset = int.from_bytes(self.data[offset+1:offset+9], 'little') + if self.meta[offset] == ord(b'S'): + fd_offset = int.from_bytes(self.meta[offset + 1:offset + 9], 'little') self.fd.seek(fd_offset, io.SEEK_SET) return Item(internal_dict=next(msgpack.Unpacker(self.fd, read_size=1024))) else: - chunk_id = bytes(self.data[offset+1:offset+33]) - chunk_offset = int.from_bytes(self.data[offset+33:offset+37], 'little') - csize, chunk = next(self.decrypted_repository.get_many([chunk_id])) + _, chunk_id_relative_offset, chunk_offset = self.indirect_entry_struct.unpack_from(self.meta, offset) + chunk_id_offset = offset - chunk_id_relative_offset + # bytearray slices are bytearrays as well, explicitly convert to bytes() + chunk_id = bytes(self.meta[chunk_id_offset:chunk_id_offset + 32]) + chunk_offset = int.from_bytes(self.meta[offset + 5:offset + 9], 'little') + chunk = self.chunks.get(chunk_id) + if not chunk: + csize, chunk = next(self.decrypted_repository.get_many([chunk_id])) + self.chunks[chunk_id] = chunk data = memoryview(chunk)[chunk_offset:] unpacker = msgpack.Unpacker() unpacker.feed(data) return Item(internal_dict=next(unpacker)) + def iter_archive_items(self, archive_item_ids): + unpacker = msgpack.Unpacker() + + stream_offset = 0 + chunk_begin = 0 + last_chunk_length = 0 + msgpacked_bytes = b'' + + write_offset = self.write_offset + meta = self.meta + pack_indirect_into = self.indirect_entry_struct.pack_into + + def write_bytes(append_msgpacked_bytes): + nonlocal msgpacked_bytes + nonlocal stream_offset + msgpacked_bytes += append_msgpacked_bytes + stream_offset += len(append_msgpacked_bytes) + + for key, (csize, data) in zip(archive_item_ids, self.decrypted_repository.get_many(archive_item_ids)): + # Store the chunk ID in the meta-array + if write_offset + 32 >= len(meta): + self.meta = meta = meta + bytes(self.GROW_META_BY) + meta[write_offset:write_offset + 32] = key + current_id_offset = write_offset + write_offset += 32 + + # The chunk boundaries cannot be tracked through write_bytes, because the unpack state machine + # *can* and *will* consume partial items, so calls to write_bytes are unrelated to chunk boundaries. + chunk_begin += last_chunk_length + last_chunk_length = len(data) + + unpacker.feed(data) + while True: + try: + item = unpacker.unpack(write_bytes) + except msgpack.OutOfData: + # Need more data, feed the next chunk + break + + current_item_length = len(msgpacked_bytes) + current_spans_chunks = stream_offset - current_item_length <= chunk_begin + current_item = msgpacked_bytes + msgpacked_bytes = b'' + + if write_offset + 9 >= len(meta): + self.meta = meta = meta + bytes(self.GROW_META_BY) + + # item entries in the meta-array come in two different flavours, both nine bytes long. + # (1) for items that span chunks: + # + # 'S' + 8 byte offset into the self.fd file, where the msgpacked item starts. + # + # (2) for items that are completely contained in one chunk, which usually is the great majority + # (about 700:1 for system backups) + # + # 'I' + 4 byte offset where the chunk ID is + 4 byte offset in the chunk + # where the msgpacked items starts + # + # The chunk ID offset is the number of bytes _back_ from the start of the entry, i.e.: + # + # |Chunk ID| .... |S1234abcd| + # ^------ offset ----------^ + + if current_spans_chunks: + pos = self.fd.seek(0, io.SEEK_END) + self.fd.write(current_item) + meta[write_offset:write_offset + 9] = b'S' + pos.to_bytes(8, 'little') + write_offset += 9 + self.direct_items += 1 + inode = write_offset - 9 + self.offset + else: + item_offset = stream_offset - current_item_length - chunk_begin + pack_indirect_into(meta, write_offset, b'I', write_offset - current_id_offset, item_offset) + write_offset += 9 + self.indirect_items += 1 + inode = write_offset - 9 + self.offset + + yield inode, Item(internal_dict=item) + + self.write_offset = write_offset + class FuseOperations(llfuse.Operations): """Export archive as a fuse filesystem @@ -120,9 +207,17 @@ class FuseOperations(llfuse.Operations): self.args = args self.manifest = manifest self.key = key - self._inode_count = 0 + # Maps inode numbers to Item instances. This is used for synthetic inodes, + # i.e. file-system objects that are made up by FuseOperations and are not contained + # in the archives. For example archive directories or intermediate directories + # not contained in archives. self.items = {} + # _inode_count is the current count of synthetic inodes, i.e. those in self.items + self._inode_count = 0 + # Maps inode numbers to the inode number of the parent self.parent = {} + # Maps inode numbers to a dictionary mapping byte directory entry names to their inode numbers, + # i.e. this contains all dirents of everything that is mounted. (It becomes really big). self.contents = defaultdict(dict) self.default_uid = os.getuid() self.default_gid = os.getgid() @@ -150,15 +245,15 @@ class FuseOperations(llfuse.Operations): self.pending_archives[archive_inode] = archive_name def sig_info_handler(self, sig_no, stack): - logger.debug('fuse: %d inodes, %d synth inodes, %d edges (%s)', - self._inode_count, len(self.items), len(self.parent), + logger.debug('fuse: %d synth inodes, %d edges (%s)', + self._inode_count, len(self.parent), # getsizeof is the size of the dict itself; key and value are two small-ish integers, # which are shared due to code structure (this has been verified). format_file_size(sys.getsizeof(self.parent) + len(self.parent) * sys.getsizeof(self._inode_count))) logger.debug('fuse: %d pending archives', len(self.pending_archives)) - logger.debug('fuse: ItemCache %d entries, meta-array %s, dependent items %s', - self._inode_count - len(self.items), - format_file_size(sys.getsizeof(self.cache.data)), + logger.debug('fuse: ItemCache %d entries (%d direct, %d indirect), meta-array size %s, direct items size %s', + self.cache.direct_items + self.cache.indirect_items, self.cache.direct_items, self.cache.indirect_items, + format_file_size(sys.getsizeof(self.cache.meta)), format_file_size(os.stat(self.cache.fd.fileno()).st_size)) logger.debug('fuse: data cache: %d/%d entries, %s', len(self.data_cache.items()), self.data_cache._capacity, format_file_size(sum(len(chunk) for key, chunk in self.data_cache.items()))) @@ -212,42 +307,31 @@ class FuseOperations(llfuse.Operations): """ self.file_versions = {} # for versions mode: original path -> version t0 = time.perf_counter() - unpacker = msgpack.Unpacker() archive = Archive(self.repository_uncached, self.key, self.manifest, archive_name, consider_part_files=self.args.consider_part_files) - self.cache.new_stream() - for key, (csize, data) in zip(archive.metadata.items, self.decrypted_repository.get_many(archive.metadata.items)): - self.cache.set_current_id(key, len(data)) - unpacker.feed(data) - while True: + for item_inode, item in self.cache.iter_archive_items(archive.metadata.items): + path = os.fsencode(item.path) + is_dir = stat.S_ISDIR(item.mode) + if is_dir: try: - item = unpacker.unpack(self.cache.write_bytes) - except msgpack.OutOfData: - break - self.cache.unpacked() - item = Item(internal_dict=item) - path = os.fsencode(item.path) - is_dir = stat.S_ISDIR(item.mode) - if is_dir: - try: - # This can happen if an archive was created with a command line like - # $ borg create ... dir1/file dir1 - # In this case the code below will have created a default_dir inode for dir1 already. - inode = self._find_inode(path, prefix) - except KeyError: - pass - else: - self.items[inode] = item - continue - segments = prefix + path.split(b'/') - parent = 1 - for segment in segments[:-1]: - parent = self.process_inner(segment, parent) - self.process_leaf(segments[-1], item, parent, prefix, is_dir) + # This can happen if an archive was created with a command line like + # $ borg create ... dir1/file dir1 + # In this case the code below will have created a default_dir inode for dir1 already. + inode = self._find_inode(path, prefix) + except KeyError: + pass + else: + self.items[inode] = item + continue + segments = prefix + path.split(b'/') + parent = 1 + for segment in segments[:-1]: + parent = self.process_inner(segment, parent) + self.process_leaf(segments[-1], item, parent, prefix, is_dir, item_inode) duration = time.perf_counter() - t0 logger.debug('fuse: process_archive completed in %.1f s for archive %s', duration, archive.name) - def process_leaf(self, name, item, parent, prefix, is_dir): + def process_leaf(self, name, item, parent, prefix, is_dir, item_inode): def file_version(item): if 'chunks' in item: ident = 0 @@ -290,7 +374,7 @@ class FuseOperations(llfuse.Operations): item.nlink = item.get('nlink', 1) + 1 self.items[inode] = item else: - inode = self.cache.inode_for_current_item() + inode = item_inode self.parent[inode] = parent if name: self.contents[parent][name] = inode diff --git a/src/borg/lrucache.py b/src/borg/lrucache.py index 4d3ba73b..03db283f 100644 --- a/src/borg/lrucache.py +++ b/src/borg/lrucache.py @@ -28,6 +28,14 @@ class LRUCache: def __contains__(self, key): return key in self._cache + def get(self, key, default=None): + value = self._cache.get(key, default) + if value is default: + return value + self._lru.remove(key) + self._lru.append(key) + return value + def clear(self): for value in self._cache.values(): self._dispose(value) diff --git a/src/borg/testsuite/lrucache.py b/src/borg/testsuite/lrucache.py index 9fb4f92b..eea171d6 100644 --- a/src/borg/testsuite/lrucache.py +++ b/src/borg/testsuite/lrucache.py @@ -19,7 +19,10 @@ class TestLRUCache: assert 'b' in c with pytest.raises(KeyError): c['a'] + assert c.get('a') is None + assert c.get('a', 'foo') == 'foo' assert c['b'] == 1 + assert c.get('b') == 1 assert c['c'] == 2 c['d'] = 3 assert len(c) == 2 From faf2d0b53777501e48dbc41fe000a4a6aa290f46 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 14 Jun 2017 19:16:36 +0200 Subject: [PATCH 1055/1387] chunker: fix invalid use of types With the argument specified as unsigned char *, Cython emits code in the Python wrapper to convert string-like objects to unsigned char* (essentially PyBytes_AS_STRING). Because the len(data) call is performed on a cdef'd string-ish type, Cython emits a strlen() call, on the result of PyBytes_AS_STRING. This is not correct, since embedded null bytes are entirely possible. Incidentally, the code generated by Cython was also not correct, since the Clang Static Analyzer found a path of execution where passing arguments in a weird way from Python resulted in strlen(NULL). Formulated like this, Cython emits essentially: c_buzhash( PyBytes_AS_STRING(data), PyObject_Length(data), ... ) which is correct. --- src/borg/chunker.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/chunker.pyx b/src/borg/chunker.pyx index bbe47cec..d2b44f68 100644 --- a/src/borg/chunker.pyx +++ b/src/borg/chunker.pyx @@ -50,11 +50,11 @@ cdef class Chunker: return chunker_process(self.chunker) -def buzhash(unsigned char *data, unsigned long seed): +def buzhash(data, unsigned long seed): cdef uint32_t *table cdef uint32_t sum table = buzhash_init_table(seed & 0xffffffff) - sum = c_buzhash(data, len(data), table) + sum = c_buzhash( data, len(data), table) free(table) return sum From 2766693706729bf656d15f8cab272a5b78304381 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 15 Jun 2017 23:50:17 +0200 Subject: [PATCH 1056/1387] fuse: update comments --- src/borg/fuse.py | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index b2db0681..8492ee3f 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -43,8 +43,9 @@ class ItemCache: and retrieves items from these inode numbers. """ - # Approximately ~230000 items (depends on the average number of items per metadata chunk) - # Since growing a bytearray has to copy it, growing it will converge to o(n), however, + # 2 MiB are approximately ~230000 items (depends on the average number of items per metadata chunk). + # + # Since growing a bytearray has to copy it, growing it will converge to O(n^2), however, # this is not yet relevant due to the swiftness of copying memory. If it becomes an issue, # use an anonymous mmap and just resize that (or, if on 64 bit, make it so big you never need # to resize it in the first place; that's free). @@ -58,32 +59,35 @@ class ItemCache: # self.meta, the "meta-array" is a densely packed array of metadata about where items can be found. # It is indexed by the inode number minus self.offset. (This is in a way eerily similar to how the first # unices did this). - # The meta-array contains chunk IDs and item entries (described in inode_for_current_item). + # The meta-array contains chunk IDs and item entries (described in iter_archive_items). # The chunk IDs are referenced by item entries through relative offsets, # which are bounded by the metadata chunk size. self.meta = bytearray() # The current write offset in self.meta self.write_offset = 0 - # Offset added to meta-indices, resulting in an inode, - # or substracted from inodes, resulting in a meta-indices. + # Offset added to meta-indices, resulting in inodes, + # or subtracted from inodes, resulting in meta-indices. + # XXX: Merge FuseOperations.items and ItemCache to avoid + # this implicit limitation / hack (on the number of synthetic inodes, degenerate + # cases can inflate their number far beyond the number of archives). self.offset = 1000000 # A temporary file that contains direct items, i.e. items directly cached in this layer. # These are items that span more than one chunk and thus cannot be efficiently cached # by the object cache (self.decrypted_repository), which would require variable-length structures; - # possible but not worth the effort, see inode_for_current_item. + # possible but not worth the effort, see iter_archive_items. self.fd = tempfile.TemporaryFile(prefix='borg-tmp') # A small LRU cache for chunks requested by ItemCache.get() from the object cache, # this significantly speeds up directory traversal and similar operations which # tend to re-read the same chunks over and over. # The capacity is kept low because increasing it does not provide any significant advantage, - # but makes LRUCache's square behaviour noticeable as well as consuming some memory. + # but makes LRUCache's square behaviour noticeable and consumes more memory. self.chunks = LRUCache(capacity=10, dispose=lambda _: None) # Instrumentation - # Count of indirect items, i.e. data is cached in the object cache, in this cache + # Count of indirect items, i.e. data is cached in the object cache, not directly in this cache self.indirect_items = 0 # Count of direct items, i.e. data is in self.fd self.direct_items = 0 @@ -92,16 +96,11 @@ class ItemCache: offset = inode - self.offset if offset < 0: raise ValueError('ItemCache.get() called with an invalid inode number') - if self.meta[offset] == ord(b'S'): - fd_offset = int.from_bytes(self.meta[offset + 1:offset + 9], 'little') - self.fd.seek(fd_offset, io.SEEK_SET) - return Item(internal_dict=next(msgpack.Unpacker(self.fd, read_size=1024))) - else: + if self.meta[offset] == ord(b'I'): _, chunk_id_relative_offset, chunk_offset = self.indirect_entry_struct.unpack_from(self.meta, offset) chunk_id_offset = offset - chunk_id_relative_offset # bytearray slices are bytearrays as well, explicitly convert to bytes() chunk_id = bytes(self.meta[chunk_id_offset:chunk_id_offset + 32]) - chunk_offset = int.from_bytes(self.meta[offset + 5:offset + 9], 'little') chunk = self.chunks.get(chunk_id) if not chunk: csize, chunk = next(self.decrypted_repository.get_many([chunk_id])) @@ -110,12 +109,21 @@ class ItemCache: unpacker = msgpack.Unpacker() unpacker.feed(data) return Item(internal_dict=next(unpacker)) + elif self.meta[offset] == ord(b'S'): + fd_offset = int.from_bytes(self.meta[offset + 1:offset + 9], 'little') + self.fd.seek(fd_offset, io.SEEK_SET) + return Item(internal_dict=next(msgpack.Unpacker(self.fd, read_size=1024))) + else: + raise ValueError('Invalid entry type in self.meta') def iter_archive_items(self, archive_item_ids): unpacker = msgpack.Unpacker() + # Current offset in the metadata stream, which consists of all metadata chunks glued together stream_offset = 0 + # Offset of the current chunk in the metadata stream chunk_begin = 0 + # Length of the chunk preciding the current chunk last_chunk_length = 0 msgpacked_bytes = b'' @@ -124,6 +132,7 @@ class ItemCache: pack_indirect_into = self.indirect_entry_struct.pack_into def write_bytes(append_msgpacked_bytes): + # XXX: Future versions of msgpack include an Unpacker.tell() method that provides this for free. nonlocal msgpacked_bytes nonlocal stream_offset msgpacked_bytes += append_msgpacked_bytes @@ -150,9 +159,9 @@ class ItemCache: # Need more data, feed the next chunk break - current_item_length = len(msgpacked_bytes) - current_spans_chunks = stream_offset - current_item_length <= chunk_begin current_item = msgpacked_bytes + current_item_length = len(current_item) + current_spans_chunks = stream_offset - current_item_length < chunk_begin msgpacked_bytes = b'' if write_offset + 9 >= len(meta): @@ -178,15 +187,13 @@ class ItemCache: pos = self.fd.seek(0, io.SEEK_END) self.fd.write(current_item) meta[write_offset:write_offset + 9] = b'S' + pos.to_bytes(8, 'little') - write_offset += 9 self.direct_items += 1 - inode = write_offset - 9 + self.offset else: item_offset = stream_offset - current_item_length - chunk_begin pack_indirect_into(meta, write_offset, b'I', write_offset - current_id_offset, item_offset) - write_offset += 9 self.indirect_items += 1 - inode = write_offset - 9 + self.offset + inode = write_offset + self.offset + write_offset += 9 yield inode, Item(internal_dict=item) From b2a4ae6bc240eb5dade96bd807d762af734e671a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 16 Jun 2017 00:41:38 +0200 Subject: [PATCH 1057/1387] lrucache: use explicit sentinel instead of None just in case someone wants to cache a big pile of nothing --- src/borg/lrucache.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/borg/lrucache.py b/src/borg/lrucache.py index 03db283f..492e18b6 100644 --- a/src/borg/lrucache.py +++ b/src/borg/lrucache.py @@ -1,3 +1,6 @@ +sentinel = object() + + class LRUCache: def __init__(self, capacity, dispose): self._cache = {} @@ -29,9 +32,9 @@ class LRUCache: return key in self._cache def get(self, key, default=None): - value = self._cache.get(key, default) - if value is default: - return value + value = self._cache.get(key, sentinel) + if value is sentinel: + return default self._lru.remove(key) self._lru.append(key) return value From 2b13607f4657ff1c28dc2aeb162dbf2b43bb1421 Mon Sep 17 00:00:00 2001 From: enkore Date: Fri, 16 Jun 2017 11:44:23 +0200 Subject: [PATCH 1058/1387] init: shaext is supported in openssl and better on ryzen than b2 --- src/borg/archiver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 23398bb3..e1406560 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2452,7 +2452,8 @@ class Archiver: On modern Intel/AMD CPUs (except very cheap ones), AES is usually hardware-accelerated. - BLAKE2b is faster than SHA256 on Intel/AMD 64-bit CPUs, + BLAKE2b is faster than SHA256 on Intel/AMD 64-bit CPUs + (except AMD Ryzen and future CPUs with SHA extensions), which makes `authenticated-blake2` faster than `none` and `authenticated`. On modern ARM CPUs, NEON provides hardware acceleration for SHA256 making it faster From 1f5ddb6572a68d85cae730ff0c04fae85f7e7d0d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 11:59:56 +0200 Subject: [PATCH 1059/1387] document pattern denial of service --- src/borg/archiver.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e1406560..2ae5074b 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1921,6 +1921,15 @@ class Archiver: Other include/exclude patterns that would normally match will be ignored. Same logic applies for exclude. + .. note:: + + `re:`, `sh:` and `fm:` patterns are all implemented on top of the Python SRE + engine. It is very easy to formulate patterns for each of these types which + requires an inordinate amount of time to match paths. If untrusted users + are able to supply patterns, ensure they cannot supply `re:` patterns. + Further, ensure that `sh:` and `fm:` patterns only contain a handful of + wildcards at most. + Exclusions can be passed via the command line option `--exclude`. When used from within a shell the patterns should be quoted to protect them from expansion. From 48642d787aaf5496bba0e2df10141536bc149886 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 12:18:52 +0200 Subject: [PATCH 1060/1387] docs: double backticks for --options --- src/borg/archiver.py | 62 ++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e1406560..bcbc51c9 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2520,7 +2520,7 @@ class Archiver: stored in the segments. - If you use a remote repo server via ssh:, the repo check is executed on the repo server without causing significant network traffic. - - The repository check can be skipped using the --archives-only option. + - The repository check can be skipped using the ``--archives-only`` option. Second, the consistency and correctness of the archive metadata is verified: @@ -2542,9 +2542,9 @@ class Archiver: decryption and this is always done client-side, because key access will be required). - The archive checks can be time consuming, they can be skipped using the - --repository-only option. + ``--repository-only`` option. - The --verify-data option will perform a full integrity verification (as opposed to + The ``--verify-data`` option will perform a full integrity verification (as opposed to checking the CRC32 of the segment) of data, which means reading the data from the repository, decrypting and decompressing it. This is a cryptographic verification, which will detect (accidental) corruption. For encrypted repositories it is @@ -2570,7 +2570,7 @@ class Archiver: subparser.add_argument('--verify-data', dest='verify_data', action='store_true', default=False, help='perform cryptographic archive data integrity verification ' - '(conflicts with --repository-only)') + '(conflicts with ``--repository-only``)') subparser.add_argument('--repair', dest='repair', action='store_true', default=False, help='attempt to repair any inconsistencies found') @@ -2644,7 +2644,7 @@ class Archiver: help='path to the backup') subparser.add_argument('--paper', dest='paper', action='store_true', default=False, - help='interactively import from a backup done with --paper') + help='interactively import from a backup done with ``--paper``') change_passphrase_epilog = process_epilog(""" The key files used for repository encryption are optionally passphrase @@ -2716,7 +2716,7 @@ class Archiver: {now}, {utcnow}, {fqdn}, {hostname}, {user} and some others. To speed up pulling backups over sshfs and similar network file systems which do - not provide correct inode information the --ignore-inode flag can be used. This + not provide correct inode information the ``--ignore-inode`` flag can be used. This potentially decreases reliability of change detection, while avoiding always reading all files on these file systems. @@ -2725,7 +2725,7 @@ class Archiver: is used to determine changed files quickly uses absolute filenames. If this is not possible, consider creating a bind mount to a stable location. - The --progress option shows (from left to right) Original, Compressed and Deduplicated + The ``--progress`` option shows (from left to right) Original, Compressed and Deduplicated (O, C and D, respectively), then the Number of files (N) processed so far, followed by the currently processed path. @@ -2734,8 +2734,8 @@ class Archiver: .. man NOTES - The --exclude patterns are not like tar. In tar --exclude .bundler/gems will - exclude foo/.bundler/gems. In borg it will not, you need to use --exclude + The ``--exclude`` patterns are not like tar. In tar ``--exclude`` .bundler/gems will + exclude foo/.bundler/gems. In borg it will not, you need to use ``--exclude`` '\*/.bundler/gems' to get the same effect. See ``borg help patterns`` for more information. @@ -2941,7 +2941,7 @@ class Archiver: When giving '-' as the output FILE, Borg will write a tar stream to standard output. - By default (--tar-filter=auto) Borg will detect whether the FILE should be compressed + By default (``--tar-filter=auto``) Borg will detect whether the FILE should be compressed based on its file extension and pipe the tarball through an appropriate filter before writing it to FILE: @@ -2949,7 +2949,7 @@ class Archiver: - .tar.bz2: bzip2 - .tar.xz: xz - Alternatively a --tar-filter program may be explicitly specified. It should + Alternatively a ``--tar-filter`` program may be explicitly specified. It should read the uncompressed tar stream from stdin and write a compressed/filtered tar stream to stdout. @@ -2960,7 +2960,7 @@ class Archiver: Timestamp resolution is limited to whole seconds, not the nanosecond resolution otherwise supported by Borg. - A --sparse option (as found in borg extract) is not supported. + A ``--sparse`` option (as found in borg extract) is not supported. By default the entire archive is extracted but a subset of files and directories can be selected by passing a list of ``PATHs`` as arguments. @@ -3015,7 +3015,7 @@ class Archiver: For archives prior to Borg 1.1 chunk contents are compared by default. If you did not create the archives with different chunker params, - pass --same-chunker-params. + pass ``--same-chunker-params``. Note that the chunker params changed from Borg 0.xx to 1.0. See the output of the "borg help patterns" command for more help on exclude patterns. @@ -3122,7 +3122,7 @@ class Archiver: .. man NOTES - The following keys are available for --format: + The following keys are available for ``--format``: """) + BaseFormatter.keys_help() + textwrap.dedent(""" @@ -3148,13 +3148,13 @@ class Archiver: (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")""") subparser.add_argument('--json', action='store_true', help='Only valid for listing repository contents. Format output as JSON. ' - 'The form of --format is ignored, ' + 'The form of ``--format`` is ignored, ' 'but keys used in it are added to the JSON output. ' 'Some keys are always present. Note: JSON can only represent text. ' 'A "barchive" key is therefore not available.') subparser.add_argument('--json-lines', action='store_true', help='Only valid for listing archive contents. Format output as JSON Lines. ' - 'The form of --format is ignored, ' + 'The form of ``--format`` is ignored, ' 'but keys used in it are added to the JSON output. ' 'Some keys are always present. Note: JSON can only represent text. ' 'A "bpath" key is therefore not available.') @@ -3208,7 +3208,7 @@ class Archiver: - versions: when used with a repository mount, this gives a merged, versioned view of the files in the archives. EXPERIMENTAL, layout may change in future. - allow_damaged_files: by default damaged files (where missing chunks were - replaced with runs of zeros by borg check --repair) are not readable and + replaced with runs of zeros by borg check ``--repair``) are not readable and return EIO (I/O error). Set this option to read such files. The BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is meant for advanced users @@ -3303,7 +3303,7 @@ class Archiver: Also, prune automatically removes checkpoint archives (incomplete archives left behind by interrupted backup runs) except if the checkpoint is the latest archive (and thus still needed). Checkpoint archives are not considered when - comparing archive counts against the retention limits (--keep-X). + comparing archive counts against the retention limits (``--keep-X``). If a prefix is set with -P, then only archives that start with the prefix are considered for deletion and only those archives count towards the totals @@ -3316,14 +3316,14 @@ class Archiver: from different machines) in one shared repository, use one prune call per data set that matches only the respective archives using the -P option. - The "--keep-within" option takes an argument of the form "", - where char is "H", "d", "w", "m", "y". For example, "--keep-within 2d" means + The ``--keep-within`` option takes an argument of the form "", + where char is "H", "d", "w", "m", "y". For example, ``--keep-within 2d`` means to keep all archives that were created within the past 48 hours. "1m" is taken to mean "31d". The archives kept with this option do not count towards the totals specified by any other options. A good procedure is to thin out more and more the older your backups get. - As an example, "--keep-daily 7" means to keep the latest backup on each day, + As an example, ``--keep-daily 7`` means to keep the latest backup on each day, up to 7 most recent days with backups (days without backups do not count). The rules are applied from secondly to yearly, and backups selected by previous rules do not count towards those of later rules. The time that each backup @@ -3331,7 +3331,7 @@ class Archiver: the local timezone, and weeks go from Monday to Sunday. Specifying a negative number of archives to keep means that there is no limit. - The "--keep-last N" option is doing the same as "--keep-secondly N" (and it will + The ``--keep-last N`` option is doing the same as ``--keep-secondly N`` (and it will keep the last N archives under the assumption that you do not create more than one backup archive in the same second). """) @@ -3487,33 +3487,33 @@ class Archiver: This is an *experimental* feature. Do *not* use this on your only backup. - --exclude, --exclude-from, --exclude-if-present, --keep-exclude-tags, and PATH + ``--exclude``, ``--exclude-from``, ``--exclude-if-present``, ``--keep-exclude-tags``, and PATH have the exact same semantics as in "borg create". If PATHs are specified the resulting archive will only contain files from these PATHs. Note that all paths in an archive are relative, therefore absolute patterns/paths - will *not* match (--exclude, --exclude-from, PATHs). + will *not* match (``--exclude``, ``--exclude-from``, PATHs). - --recompress allows to change the compression of existing data in archives. + ``--recompress`` allows to change the compression of existing data in archives. Due to how Borg stores compressed size information this might display incorrect information for archives that were not recreated at the same time. There is no risk of data loss by this. - --chunker-params will re-chunk all files in the archive, this can be + ``--chunker-params`` will re-chunk all files in the archive, this can be used to have upgraded Borg 0.xx or Attic archives deduplicate with Borg 1.x archives. **USE WITH CAUTION.** Depending on the PATHs and patterns given, recreate can be used to permanently delete files from archives. - When in doubt, use "--dry-run --verbose --list" to see how patterns/PATHS are + When in doubt, use ``--dry-run --verbose --list`` to see how patterns/PATHS are interpreted. The archive being recreated is only removed after the operation completes. The archive that is built during the operation exists at the same time at ".recreate". The new archive will have a different archive ID. - With --target the original archive is not replaced, instead a new archive is created. + With ``--target`` the original archive is not replaced, instead a new archive is created. When rechunking space usage can be substantial, expect at least the entire deduplicated size of the archives using the previous chunker params. @@ -3554,7 +3554,7 @@ class Archiver: 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', action='store_true', default=False, - help='if tag objects are specified with --exclude-if-present, don\'t omit the tag ' + help='if tag objects are specified with ``--exclude-if-present``, don\'t omit the tag ' 'objects themselves from the backup archive') exclude_group.add_argument('--pattern', action=ArgparsePatternAction, @@ -3583,7 +3583,7 @@ class Archiver: '"borg help compression" command for details.') archive_group.add_argument('--recompress', dest='recompress', nargs='?', default='never', const='if-different', choices=('never', 'if-different', 'always'), - help='recompress data chunks according to --compression if "if-different". ' + help='recompress data chunks according to ``--compression`` if "if-different". ' 'When "always", chunks that are already compressed that way are not skipped, ' 'but compressed again. Only the algorithm is considered for "if-different", ' 'not the compression level (if any).') @@ -3871,7 +3871,7 @@ class Archiver: group.add_argument('-a', '--glob-archives', dest='glob_archives', default=None, help='only consider archive names matching the glob. ' 'sh: rules apply, see "borg help patterns". ' - '--prefix and --glob-archives are mutually exclusive.') + '``--prefix`` and ``--glob-archives`` are mutually exclusive.') if sort_by: sort_by_default = 'timestamp' From bd701e58c51e5ddceca49ab9114afbc38f302109 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 12:20:25 +0200 Subject: [PATCH 1061/1387] docs: backticks for option values --- src/borg/archiver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index bcbc51c9..c1216fbd 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -3583,15 +3583,15 @@ class Archiver: '"borg help compression" command for details.') archive_group.add_argument('--recompress', dest='recompress', nargs='?', default='never', const='if-different', choices=('never', 'if-different', 'always'), - help='recompress data chunks according to ``--compression`` if "if-different". ' - 'When "always", chunks that are already compressed that way are not skipped, ' - 'but compressed again. Only the algorithm is considered for "if-different", ' + help='recompress data chunks according to ``--compression`` if `if-different`. ' + 'When `always`, chunks that are already compressed that way are not skipped, ' + 'but compressed again. Only the algorithm is considered for `if-different`, ' 'not the compression level (if any).') archive_group.add_argument('--chunker-params', dest='chunker_params', type=ChunkerParams, default=CHUNKER_PARAMS, metavar='PARAMS', help='specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, ' - 'HASH_MASK_BITS, HASH_WINDOW_SIZE) or "default" to use the current defaults. ' + 'HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the current defaults. ' 'default: %d,%d,%d,%d' % CHUNKER_PARAMS) subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='', From 5aa865c8d8f735538fbf25e639aa2498a32aa5cc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 12:27:53 +0200 Subject: [PATCH 1062/1387] docs: double backticks for --options --- docs/deployment/central-backup-server.rst | 2 +- docs/faq.rst | 4 ++-- docs/usage/notes.rst | 2 +- docs/usage_general.rst.inc | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/deployment/central-backup-server.rst b/docs/deployment/central-backup-server.rst index 68c8fdda..ee90326e 100644 --- a/docs/deployment/central-backup-server.rst +++ b/docs/deployment/central-backup-server.rst @@ -55,7 +55,7 @@ Borg is instructed to restrict clients into their own paths: The client will be able to access any file or subdirectory inside of ``/home/backup/repos/`` but no other directories. You can allow a client to access several separate directories by passing multiple -`--restrict-to-path` flags, for instance: ``borg serve --restrict-to-path /home/backup/repos/ --restrict-to-path /home/backup/repos/``, +``--restrict-to-path`` flags, for instance: ``borg serve --restrict-to-path /home/backup/repos/ --restrict-to-path /home/backup/repos/``, which could make sense if multiple machines belong to one person which should then have access to all the backups of their machines. diff --git a/docs/faq.rst b/docs/faq.rst index 46c846c8..9108252a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -388,7 +388,7 @@ yet noticed on the server. Try these settings: ClientAliveCountMax 3 If you have multiple borg create ... ; borg create ... commands in a already -serialized way in a single script, you need to give them --lock-wait N (with N +serialized way in a single script, you need to give them ``--lock-wait N`` (with N being a bit more than the time the server needs to terminate broken down connections and release the lock). @@ -440,7 +440,7 @@ Can I backup my root partition (/) with Borg? Backing up your entire root partition works just fine, but remember to exclude directories that make no sense to backup, such as /dev, /proc, -/sys, /tmp and /run, and to use --one-file-system if you only want to +/sys, /tmp and /run, and to use ``--one-file-system`` if you only want to backup the root partition (and not any mounted devices e.g.). If it crashes with a UnicodeError, what can I do? diff --git a/docs/usage/notes.rst b/docs/usage/notes.rst index 21d3cf4b..758649f5 100644 --- a/docs/usage/notes.rst +++ b/docs/usage/notes.rst @@ -63,7 +63,7 @@ of them. --read-special ~~~~~~~~~~~~~~ -The --read-special option is special - you do not want to use it for normal +The ``--read-special`` option is special - you do not want to use it for normal full-filesystem backups, but rather after carefully picking some targets for it. The option ``--read-special`` triggers special treatment for block and char diff --git a/docs/usage_general.rst.inc b/docs/usage_general.rst.inc index 03c41d3a..f349512a 100644 --- a/docs/usage_general.rst.inc +++ b/docs/usage_general.rst.inc @@ -75,7 +75,7 @@ Type of log output The log level of the builtin logging configuration defaults to WARNING. This is because we want Borg to be mostly silent and only output warnings, errors and critical messages, unless output has been requested -by supplying an option that implies output (eg, --list or --progress). +by supplying an option that implies output (e.g. ``--list`` or ``--progress``). Log levels: DEBUG < INFO < WARNING < ERROR < CRITICAL @@ -97,7 +97,7 @@ to get critical level output. While you can set misc. log levels, do not expect that every command will give different output on different log levels - it's just a possibility. -.. warning:: Options --critical and --error are provided for completeness, +.. warning:: Options ``--critical`` and ``--error`` are provided for completeness, their usage is not recommended as you might miss important information. Return codes From 26970cd4e9c3d7a78952c8e3692dcfa67eb10f9c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 12:28:16 +0200 Subject: [PATCH 1063/1387] delete docs/misc/compression.conf --- docs/misc/compression.conf | 56 -------------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 docs/misc/compression.conf diff --git a/docs/misc/compression.conf b/docs/misc/compression.conf deleted file mode 100644 index 881f5fe9..00000000 --- a/docs/misc/compression.conf +++ /dev/null @@ -1,56 +0,0 @@ -# example config file for --compression-from option -# -# Format of non-comment / non-empty lines: -# : -# compression-spec is same format as for --compression option -# path/filename pattern is same format as for --exclude option - -# archives / files: -none:*.gz -none:*.tgz -none:*.bz2 -none:*.tbz2 -none:*.xz -none:*.txz -none:*.lzma -none:*.lzo -none:*.zip -none:*.rar -none:*.7z - -# audio: -none:*.mp3 -none:*.ogg -none:*.oga -none:*.flac -none:*.aac -none:*.m4a - -# video: -none:*.mp4 -none:*.mkv -none:*.m4v -none:*.avi -none:*.mpg -none:*.mpeg -none:*.webm -none:*.vob -none:*.ts -none:*.ogv -none:*.mov -none:*.flv -none:*.ogm - -# pictures/images -none:*.jpg -none:*.jpeg -none:*.png -none:*.gif - -# disk images -none:*.dmg - -# software archives -none:*.rpm -none:*.deb -none:*.msi From 772be8fa97a97d40aa0fa34bee1f402e9d01da9f Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 12:28:42 +0200 Subject: [PATCH 1064/1387] python setup.py build_usage --- docs/usage/check.rst.inc | 12 +++++++----- docs/usage/create.rst.inc | 8 ++++---- docs/usage/delete.rst.inc | 4 +++- docs/usage/diff.rst.inc | 2 +- docs/usage/export-tar.rst.inc | 6 +++--- docs/usage/info.rst.inc | 4 +++- docs/usage/init.rst.inc | 7 ++++--- docs/usage/key_import.rst.inc | 2 +- docs/usage/list.rst.inc | 18 ++++++++++++++---- docs/usage/mount.rst.inc | 6 ++++-- docs/usage/prune.rst.inc | 18 +++++++++++------- docs/usage/recreate.rst.inc | 18 +++++++++--------- 12 files changed, 64 insertions(+), 41 deletions(-) diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index 7e021838..047da6b2 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -18,7 +18,7 @@ optional arguments ``--archives-only`` | only perform archives checks ``--verify-data`` - | perform cryptographic archive data integrity verification (conflicts with --repository-only) + | perform cryptographic archive data integrity verification (conflicts with ``--repository-only``) ``--repair`` | attempt to repair any inconsistencies found ``--save-space`` @@ -29,7 +29,9 @@ optional arguments filters ``-P``, ``--prefix`` - | only consider archive names starting with this prefix + | only consider archive names starting with this prefix. + ``-a``, ``--glob-archives`` + | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. ``--sort-by`` | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp ``--first N`` @@ -54,7 +56,7 @@ First, the underlying repository data files are checked: stored in the segments. - If you use a remote repo server via ssh:, the repo check is executed on the repo server without causing significant network traffic. -- The repository check can be skipped using the --archives-only option. +- The repository check can be skipped using the ``--archives-only`` option. Second, the consistency and correctness of the archive metadata is verified: @@ -76,9 +78,9 @@ Second, the consistency and correctness of the archive metadata is verified: decryption and this is always done client-side, because key access will be required). - The archive checks can be time consuming, they can be skipped using the - --repository-only option. + ``--repository-only`` option. -The --verify-data option will perform a full integrity verification (as opposed to +The ``--verify-data`` option will perform a full integrity verification (as opposed to checking the CRC32 of the segment) of data, which means reading the data from the repository, decrypting and decompressing it. This is a cryptographic verification, which will detect (accidental) corruption. For encrypted repositories it is diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index e253336e..c97d1448 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -93,7 +93,7 @@ In the archive name, you may use the following placeholders: {now}, {utcnow}, {fqdn}, {hostname}, {user} and some others. To speed up pulling backups over sshfs and similar network file systems which do -not provide correct inode information the --ignore-inode flag can be used. This +not provide correct inode information the ``--ignore-inode`` flag can be used. This potentially decreases reliability of change detection, while avoiding always reading all files on these file systems. @@ -102,7 +102,7 @@ creation of a new archive to ensure fast operation. This is because the file cac is used to determine changed files quickly uses absolute filenames. If this is not possible, consider creating a bind mount to a stable location. -The --progress option shows (from left to right) Original, Compressed and Deduplicated +The ``--progress`` option shows (from left to right) Original, Compressed and Deduplicated (O, C and D, respectively), then the Number of files (N) processed so far, followed by the currently processed path. @@ -111,8 +111,8 @@ See the output of the "borg help placeholders" command for more help on placehol .. man NOTES -The --exclude patterns are not like tar. In tar --exclude .bundler/gems will -exclude foo/.bundler/gems. In borg it will not, you need to use --exclude +The ``--exclude`` patterns are not like tar. In tar ``--exclude`` .bundler/gems will +exclude foo/.bundler/gems. In borg it will not, you need to use ``--exclude`` '\*/.bundler/gems' to get the same effect. See ``borg help patterns`` for more information. diff --git a/docs/usage/delete.rst.inc b/docs/usage/delete.rst.inc index d8f727f0..18e9d539 100644 --- a/docs/usage/delete.rst.inc +++ b/docs/usage/delete.rst.inc @@ -27,7 +27,9 @@ optional arguments filters ``-P``, ``--prefix`` - | only consider archive names starting with this prefix + | only consider archive names starting with this prefix. + ``-a``, ``--glob-archives`` + | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. ``--sort-by`` | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp ``--first N`` diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index 53d6467d..0de6e909 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -58,7 +58,7 @@ are compared, which is very fast. For archives prior to Borg 1.1 chunk contents are compared by default. If you did not create the archives with different chunker params, -pass --same-chunker-params. +pass ``--same-chunker-params``. Note that the chunker params changed from Borg 0.xx to 1.0. See the output of the "borg help patterns" command for more help on exclude patterns. \ No newline at end of file diff --git a/docs/usage/export-tar.rst.inc b/docs/usage/export-tar.rst.inc index d5f46498..327a0874 100644 --- a/docs/usage/export-tar.rst.inc +++ b/docs/usage/export-tar.rst.inc @@ -42,7 +42,7 @@ This command creates a tarball from an archive. When giving '-' as the output FILE, Borg will write a tar stream to standard output. -By default (--tar-filter=auto) Borg will detect whether the FILE should be compressed +By default (``--tar-filter=auto``) Borg will detect whether the FILE should be compressed based on its file extension and pipe the tarball through an appropriate filter before writing it to FILE: @@ -50,7 +50,7 @@ before writing it to FILE: - .tar.bz2: bzip2 - .tar.xz: xz -Alternatively a --tar-filter program may be explicitly specified. It should +Alternatively a ``--tar-filter`` program may be explicitly specified. It should read the uncompressed tar stream from stdin and write a compressed/filtered tar stream to stdout. @@ -61,7 +61,7 @@ BSD flags, ACLs, extended attributes (xattrs), atime and ctime are not exported. Timestamp resolution is limited to whole seconds, not the nanosecond resolution otherwise supported by Borg. -A --sparse option (as found in borg extract) is not supported. +A ``--sparse`` option (as found in borg extract) is not supported. By default the entire archive is extracted but a subset of files and directories can be selected by passing a list of ``PATHs`` as arguments. diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index 4c382f10..879dee14 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -21,7 +21,9 @@ optional arguments filters ``-P``, ``--prefix`` - | only consider archive names starting with this prefix + | only consider archive names starting with this prefix. + ``-a``, ``--glob-archives`` + | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. ``--sort-by`` | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp ``--first N`` diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index 856af365..ec99fd66 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -84,7 +84,8 @@ Encryption modes On modern Intel/AMD CPUs (except very cheap ones), AES is usually hardware-accelerated. -BLAKE2b is faster than SHA256 on Intel/AMD 64-bit CPUs, +BLAKE2b is faster than SHA256 on Intel/AMD 64-bit CPUs +(except AMD Ryzen and future CPUs with SHA extensions), which makes `authenticated-blake2` faster than `none` and `authenticated`. On modern ARM CPUs, NEON provides hardware acceleration for SHA256 making it faster @@ -95,7 +96,7 @@ Hardware acceleration is always used automatically when available. `repokey` and `keyfile` use AES-CTR-256 for encryption and HMAC-SHA256 for authentication in an encrypt-then-MAC (EtM) construction. The chunk ID hash is HMAC-SHA256 as well (with a separate key). -These modes are compatible with borg 1.0.x. +These modes are compatible with Borg 1.0.x. `repokey-blake2` and `keyfile-blake2` are also authenticated encryption modes, but use BLAKE2b-256 instead of HMAC-SHA256 for authentication. The chunk ID @@ -105,7 +106,7 @@ These modes are new and *not* compatible with Borg 1.0.x. `authenticated` mode uses no encryption, but authenticates repository contents through the same HMAC-SHA256 hash as the `repokey` and `keyfile` modes (it uses it as the chunk ID hash). The key is stored like `repokey`. -This mode is new and *not* compatible with borg 1.0.x. +This mode is new and *not* compatible with Borg 1.0.x. `authenticated-blake2` is like `authenticated`, but uses the keyed BLAKE2b-256 hash from the other blake2 modes. diff --git a/docs/usage/key_import.rst.inc b/docs/usage/key_import.rst.inc index 86ab25b6..c6b9b5ea 100644 --- a/docs/usage/key_import.rst.inc +++ b/docs/usage/key_import.rst.inc @@ -16,7 +16,7 @@ positional arguments optional arguments ``--paper`` - | interactively import from a backup done with --paper + | interactively import from a backup done with ``--paper`` :ref:`common_options` | diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index a56cbecd..3c9d27e9 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -21,16 +21,18 @@ optional arguments | specify format for file listing | (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") ``--json`` - | Only valid for listing repository contents. Format output as JSON. The form of --format is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "barchive" key is therefore not available. + | Only valid for listing repository contents. Format output as JSON. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "barchive" key is therefore not available. ``--json-lines`` - | Only valid for listing archive contents. Format output as JSON Lines. The form of --format is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. + | Only valid for listing archive contents. Format output as JSON Lines. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. :ref:`common_options` | filters ``-P``, ``--prefix`` - | only consider archive names starting with this prefix + | only consider archive names starting with this prefix. + ``-a``, ``--glob-archives`` + | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. ``--sort-by`` | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp ``--first N`` @@ -63,7 +65,7 @@ See the "borg help patterns" command for more help on exclude patterns. .. man NOTES -The following keys are available for --format: +The following keys are available for ``--format``: - NEWLINE: OS dependent line separator - NL: alias of NEWLINE @@ -108,12 +110,20 @@ Keys for listing archive files: - isoctime - isoatime + - blake2b + - blake2s - md5 - sha1 - sha224 - sha256 - sha384 + - sha3_224 + - sha3_256 + - sha3_384 + - sha3_512 - sha512 + - shake_128 + - shake_256 - archiveid - archivename diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index bfb0a24c..dd013838 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -25,7 +25,9 @@ optional arguments filters ``-P``, ``--prefix`` - | only consider archive names starting with this prefix + | only consider archive names starting with this prefix. + ``-a``, ``--glob-archives`` + | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. ``--sort-by`` | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp ``--first N`` @@ -54,7 +56,7 @@ supported by borg: - versions: when used with a repository mount, this gives a merged, versioned view of the files in the archives. EXPERIMENTAL, layout may change in future. - allow_damaged_files: by default damaged files (where missing chunks were - replaced with runs of zeros by borg check --repair) are not readable and + replaced with runs of zeros by borg check ``--repair``) are not readable and return EIO (I/O error). Set this option to read such files. The BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is meant for advanced users diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index fb23c7fc..e9c86051 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -37,14 +37,18 @@ optional arguments | number of monthly archives to keep ``-y``, ``--keep-yearly`` | number of yearly archives to keep - ``-P``, ``--prefix`` - | only consider archive names starting with this prefix ``--save-space`` | work slower, but using less space :ref:`common_options` | +filters + ``-P``, ``--prefix`` + | only consider archive names starting with this prefix. + ``-a``, ``--glob-archives`` + | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + Description ~~~~~~~~~~~ @@ -55,7 +59,7 @@ automated backup scripts wanting to keep a certain number of historic backups. Also, prune automatically removes checkpoint archives (incomplete archives left behind by interrupted backup runs) except if the checkpoint is the latest archive (and thus still needed). Checkpoint archives are not considered when -comparing archive counts against the retention limits (--keep-X). +comparing archive counts against the retention limits (``--keep-X``). If a prefix is set with -P, then only archives that start with the prefix are considered for deletion and only those archives count towards the totals @@ -68,14 +72,14 @@ If you have multiple sequences of archives with different data sets (e.g. from different machines) in one shared repository, use one prune call per data set that matches only the respective archives using the -P option. -The "--keep-within" option takes an argument of the form "", -where char is "H", "d", "w", "m", "y". For example, "--keep-within 2d" means +The ``--keep-within`` option takes an argument of the form "", +where char is "H", "d", "w", "m", "y". For example, ``--keep-within 2d`` means to keep all archives that were created within the past 48 hours. "1m" is taken to mean "31d". The archives kept with this option do not count towards the totals specified by any other options. A good procedure is to thin out more and more the older your backups get. -As an example, "--keep-daily 7" means to keep the latest backup on each day, +As an example, ``--keep-daily 7`` means to keep the latest backup on each day, up to 7 most recent days with backups (days without backups do not count). The rules are applied from secondly to yearly, and backups selected by previous rules do not count towards those of later rules. The time that each backup @@ -83,6 +87,6 @@ starts is used for pruning purposes. Dates and times are interpreted in the local timezone, and weeks go from Monday to Sunday. Specifying a negative number of archives to keep means that there is no limit. -The "--keep-last N" option is doing the same as "--keep-secondly N" (and it will +The ``--keep-last N`` option is doing the same as ``--keep-secondly N`` (and it will keep the last N archives under the assumption that you do not create more than one backup archive in the same second). \ No newline at end of file diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index fff3578a..26bbdb38 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -37,7 +37,7 @@ Exclusion options ``--exclude-if-present NAME`` | exclude directories that are tagged by containing a filesystem object with the given NAME ``--keep-exclude-tags``, ``--keep-tag-files`` - | if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive + | if tag objects are specified with ``--exclude-if-present``, don't omit the tag objects themselves from the backup archive ``--pattern PATTERN`` | experimental: include/exclude paths matching PATTERN ``--patterns-from PATTERNFILE`` @@ -55,9 +55,9 @@ Archive options ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the "borg help compression" command for details. ``--recompress`` - | recompress data chunks according to --compression if "if-different". When "always", chunks that are already compressed that way are not skipped, but compressed again. Only the algorithm is considered for "if-different", not the compression level (if any). + | recompress data chunks according to ``--compression`` if `if-different`. When `always`, chunks that are already compressed that way are not skipped, but compressed again. Only the algorithm is considered for `if-different`, not the compression level (if any). ``--chunker-params PARAMS`` - | specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or "default" to use the current defaults. default: 19,23,21,4095 + | specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the current defaults. default: 19,23,21,4095 Description ~~~~~~~~~~~ @@ -66,33 +66,33 @@ Recreate the contents of existing archives. This is an *experimental* feature. Do *not* use this on your only backup. ---exclude, --exclude-from, --exclude-if-present, --keep-exclude-tags, and PATH +``--exclude``, ``--exclude-from``, ``--exclude-if-present``, ``--keep-exclude-tags``, and PATH have the exact same semantics as in "borg create". If PATHs are specified the resulting archive will only contain files from these PATHs. Note that all paths in an archive are relative, therefore absolute patterns/paths -will *not* match (--exclude, --exclude-from, PATHs). +will *not* match (``--exclude``, ``--exclude-from``, PATHs). ---recompress allows to change the compression of existing data in archives. +``--recompress`` allows to change the compression of existing data in archives. Due to how Borg stores compressed size information this might display incorrect information for archives that were not recreated at the same time. There is no risk of data loss by this. ---chunker-params will re-chunk all files in the archive, this can be +``--chunker-params`` will re-chunk all files in the archive, this can be used to have upgraded Borg 0.xx or Attic archives deduplicate with Borg 1.x archives. **USE WITH CAUTION.** Depending on the PATHs and patterns given, recreate can be used to permanently delete files from archives. -When in doubt, use "--dry-run --verbose --list" to see how patterns/PATHS are +When in doubt, use ``--dry-run --verbose --list`` to see how patterns/PATHS are interpreted. The archive being recreated is only removed after the operation completes. The archive that is built during the operation exists at the same time at ".recreate". The new archive will have a different archive ID. -With --target the original archive is not replaced, instead a new archive is created. +With ``--target`` the original archive is not replaced, instead a new archive is created. When rechunking space usage can be substantial, expect at least the entire deduplicated size of the archives using the previous chunker params. From b08064bb4ed4bbafcd17281c6b5c92a63cd8a79d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 12:29:13 +0200 Subject: [PATCH 1065/1387] docs: turn smartypants back on, since --options are now in `` --- docs/conf.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 81c397bc..4804bcff 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -152,9 +152,7 @@ html_last_updated_fmt = '%Y-%m-%d' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -# -# This is disabled to avoid mangling --options-that-appear-in-texts. -html_use_smartypants = False +html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { From 5d9beb5dd2ded48dbac33983a142ff0f30cf4c62 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 12:33:01 +0200 Subject: [PATCH 1066/1387] docs: html: less annoying `` in running text --- docs/borg_theme/css/borg.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 3cb50522..0f97bc36 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -60,3 +60,11 @@ dt code { border-left: 2px solid #4e4a4a;; border-right: 2px solid #4e4a4a;; } + +p .literal, +p .literal span { + border: none; + padding: 0; + color: black; /* slight contrast with #404040 of regular text */ + background: none; +} From dd8a815327209198ee68f3174cdc960acd313e37 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 12:35:24 +0200 Subject: [PATCH 1067/1387] docs: html: format `option values` like in man pages --- docs/borg_theme/css/borg.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 0f97bc36..ae4ce24c 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -68,3 +68,13 @@ p .literal span { color: black; /* slight contrast with #404040 of regular text */ background: none; } + +cite { + white-space: nowrap; + color: black; /* slight contrast with #404040 of regular text */ + font-size: 75%; + font-family: Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter", + "DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace; + font-style: normal; + text-decoration: underline; +} From 97089fe14172ce1ca678310c5f1572ed74e10e34 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 11:54:06 +0200 Subject: [PATCH 1068/1387] init: note possible denial of service with "none" mode --- src/borg/archiver.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 95ef1f0c..2f01d5a1 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2453,12 +2453,14 @@ class Archiver: | Hash/MAC | Not encrypted | Not encrypted, | Encrypted (AEAD w/ AES) | | | no auth | but authenticated | and authenticated | +----------+---------------+------------------------+--------------------------+ - | SHA-256 | none | authenticated | repokey, keyfile | + | SHA-256 | none | `authenticated` | repokey, keyfile | +----------+---------------+------------------------+--------------------------+ - | BLAKE2b | n/a | authenticated-blake2 | repokey-blake2, | - | | | | keyfile-blake2 | + | BLAKE2b | n/a | `authenticated-blake2` | `repokey-blake2`, | + | | | | `keyfile-blake2` | +----------+---------------+------------------------+--------------------------+ + `Marked modes` are new in Borg 1.1 and are not backwards-compatible with Borg 1.0.x. + On modern Intel/AMD CPUs (except very cheap ones), AES is usually hardware-accelerated. BLAKE2b is faster than SHA256 on Intel/AMD 64-bit CPUs @@ -2491,7 +2493,8 @@ class Archiver: `none` mode uses no encryption and no authentication. It uses SHA256 as chunk ID hash. Not recommended, rather consider using an authenticated or - authenticated/encrypted mode. + authenticated/encrypted mode. This mode has possible denial-of-service issues + when running ``borg create`` on contents controlled by an attacker. Use it only for new repositories where no encryption is wanted **and** when compatibility with 1.0.x is important. If compatibility with 1.0.x is not important, use `authenticated-blake2` or `authenticated` instead. From a04625cd13799a1697b91288cf5803906a564f46 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 12:07:12 +0200 Subject: [PATCH 1069/1387] nanorst: better inline formatting in tables --- src/borg/archiver.py | 9 +++++++-- src/borg/nanorst.py | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 2f01d5a1..496d14d4 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2449,16 +2449,21 @@ class Archiver: Encryption modes ++++++++++++++++ + .. nanorst: inline-fill + +----------+---------------+------------------------+--------------------------+ | Hash/MAC | Not encrypted | Not encrypted, | Encrypted (AEAD w/ AES) | | | no auth | but authenticated | and authenticated | +----------+---------------+------------------------+--------------------------+ - | SHA-256 | none | `authenticated` | repokey, keyfile | + | SHA-256 | none | `authenticated` | repokey | + | | | | keyfile | +----------+---------------+------------------------+--------------------------+ - | BLAKE2b | n/a | `authenticated-blake2` | `repokey-blake2`, | + | BLAKE2b | n/a | `authenticated-blake2` | `repokey-blake2` | | | | | `keyfile-blake2` | +----------+---------------+------------------------+--------------------------+ + .. nanorst: inline-replace + `Marked modes` are new in Borg 1.1 and are not backwards-compatible with Borg 1.0.x. On modern Intel/AMD CPUs (except very cheap ones), AES is usually diff --git a/src/borg/nanorst.py b/src/borg/nanorst.py index 113a86a1..ba4ad2a3 100644 --- a/src/borg/nanorst.py +++ b/src/borg/nanorst.py @@ -58,6 +58,7 @@ def rst_to_text(text, state_hook=None, references=None): state_hook = state_hook or (lambda old_state, new_state, out: None) references = references or {} state = 'text' + inline_mode = 'replace' text = TextPecker(text) out = io.StringIO() @@ -117,17 +118,26 @@ def rst_to_text(text, state_hook=None, references=None): directive, is_directive, arguments = text.readline().partition('::') text.read(1) if not is_directive: + # partition: if the separator is not in the text, the leftmost output is the entire input + if directive == 'nanorst: inline-fill': + inline_mode = 'fill' + elif directive == 'nanorst: inline-replace': + inline_mode = 'replace' continue process_directive(directive, arguments.strip(), out, state_hook) continue if state in inline_single and char == state: state_hook(state, 'text', out) state = 'text' + if inline_mode == 'fill': + out.write(2 * ' ') continue if state == '``' and char == next == '`': state_hook(state, 'text', out) state = 'text' text.read(1) + if inline_mode == 'fill': + out.write(4 * ' ') continue if state == '**' and char == next == '*': state_hook(state, 'text', out) From 868749579301089f4f1836ee94301e7eaa1da81e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 12:12:55 +0200 Subject: [PATCH 1070/1387] docs: css: avoid scroll bars on tables --- docs/borg_theme/css/borg.css | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index ae4ce24c..0ba4c01f 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -61,6 +61,14 @@ dt code { border-right: 2px solid #4e4a4a;; } +/* the rtd theme has "nowrap" here which causes tables to have scroll bars. + * undo that setting. it does not seem to cause issues, even when making the + * viewport narrow. + */ +.wy-table-responsive table td, .wy-table-responsive table th { + white-space: normal; +} + p .literal, p .literal span { border: none; @@ -73,8 +81,8 @@ cite { white-space: nowrap; color: black; /* slight contrast with #404040 of regular text */ font-size: 75%; - font-family: Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter", - "DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace; + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", + "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; font-style: normal; text-decoration: underline; } From 02ada0348617385f0249f11f9a257d6bcf5d67e0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 14:29:04 +0200 Subject: [PATCH 1071/1387] docs: remove more boxes around monospace --- docs/borg_theme/css/borg.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 0ba4c01f..2145284b 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -69,6 +69,12 @@ dt code { white-space: normal; } +code, +.rst-content tt.literal, +.rst-content tt.literal, +.rst-content code.literal, +.rst-content tt, +.rst-content code, p .literal, p .literal span { border: none; From 0288fff6b714dff5cb60286aaed5a59820352b9c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 14:29:14 +0200 Subject: [PATCH 1072/1387] docs: explain formatting --- docs/usage/general.rst | 4 ++++ docs/usage_general.rst.inc | 24 +++++++++++++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/usage/general.rst b/docs/usage/general.rst index 77b1b9ac..127ab667 100644 --- a/docs/usage/general.rst +++ b/docs/usage/general.rst @@ -5,6 +5,10 @@ Borg consists of a number of commands. Each command accepts a number of arguments and options and interprets various environment variables. The following sections will describe each command in detail. +Commands, options, parameters, paths and such are ``set in fixed-width``. +Option values are `underlined`. Borg has few options accepting a fixed set +of values (e.g. ``--encryption`` of :ref:`borg_init`). + .. container:: experimental Experimental features are marked with red stripes on the sides, like this paragraph. diff --git a/docs/usage_general.rst.inc b/docs/usage_general.rst.inc index f349512a..1aea4298 100644 --- a/docs/usage_general.rst.inc +++ b/docs/usage_general.rst.inc @@ -105,14 +105,16 @@ Return codes Borg can exit with the following return codes (rc): -:: - - 0 = success (logged as INFO) - 1 = warning (operation reached its normal end, but there were warnings - - you should check the log, logged as WARNING) - 2 = error (like a fatal error, a local or remote exception, the operation - did not reach its normal end, logged as ERROR) - 128+N = killed by signal N (e.g. 137 == kill -9) +=========== ======= +Return code Meaning +=========== ======= +0 success (logged as INFO) +1 warning (operation reached its normal end, but there were warnings -- + you should check the log, logged as WARNING) +2 error (like a fatal error, a local or remote exception, the operation + did not reach its normal end, logged as ERROR) +128+N killed by signal N (e.g. 137 == kill -9) +=========== ======= If you use ``--show-rc``, the return code is also logged at the indicated level as the last log entry. @@ -127,8 +129,8 @@ Borg uses some environment variables for automation: General: BORG_REPO When set, use the value to give the default repository location. If a command needs an archive - parameter, you can abbreviate as `::archive`. If a command needs a repository parameter, you - can either leave it away or abbreviate as `::`, if a positional parameter is required. + parameter, you can abbreviate as ``::archive``. If a command needs a repository parameter, you + can either leave it away or abbreviate as ``::``, if a positional parameter is required. BORG_PASSPHRASE When set, use the value to answer the passphrase question for encrypted repositories. It is used when a passphrase is needed to access an encrypted repo as well as when a new @@ -337,7 +339,7 @@ Cache files (client only): Network (only for client/server operation): If your repository is remote, all deduplicated (and optionally compressed/ - encrypted) data of course has to go over the connection (ssh: repo url). + encrypted) data of course has to go over the connection (``ssh://`` repo url). If you use a locally mounted network filesystem, additionally some copy operations used for transaction support also go over the connection. If you backup multiple sources to one target repository, additional traffic From b1205a7932468aff314b0e91e15bbf498b788ba6 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 14:32:37 +0200 Subject: [PATCH 1073/1387] docs: fix too small text in tables --- docs/borg_theme/css/borg.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 2145284b..805e72e5 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -69,6 +69,18 @@ dt code { white-space: normal; } +/* for some reason the rtd theme makes text in tables very small. + * fix that. + */ +.wy-table td, +.rst-content table.docutils td, +.rst-content table.field-list td, +.wy-table th, +.rst-content table.docutils th, +.rst-content table.field-list th { + font-size: 100%; +} + code, .rst-content tt.literal, .rst-content tt.literal, From 28f944bd9122af5c6abc7239596ff4760f65df3e Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 16:40:56 +0200 Subject: [PATCH 1074/1387] init: remove short option for --append-only --- src/borg/archiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 496d14d4..c7879659 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2516,7 +2516,7 @@ class Archiver: subparser.add_argument('-e', '--encryption', dest='encryption', required=True, choices=('none', 'keyfile', 'repokey', 'keyfile-blake2', 'repokey-blake2', 'authenticated'), help='select encryption key mode **(required)**') - subparser.add_argument('-a', '--append-only', dest='append_only', action='store_true', + subparser.add_argument('--append-only', dest='append_only', action='store_true', help='create an append-only mode repository') subparser.add_argument('--storage-quota', dest='storage_quota', default=None, type=parse_storage_quota, From 3f72790b5d842cef869ec2980ac1f3742b79fef3 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 17 Jun 2017 16:44:13 +0200 Subject: [PATCH 1075/1387] upgrade: remove short option for --inplace --- src/borg/archiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index c7879659..4536b83d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -3485,7 +3485,7 @@ class Archiver: subparser.add_argument('-n', '--dry-run', dest='dry_run', default=False, action='store_true', help='do not change repository') - subparser.add_argument('-i', '--inplace', dest='inplace', + subparser.add_argument('--inplace', dest='inplace', default=False, action='store_true', help="""rewrite repository in place, with no chance of going back to older versions of the repository.""") From b7b6abca7a15c8a5ce44bd0d04cc12799c22826e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 17 Jun 2017 19:32:39 +0200 Subject: [PATCH 1076/1387] hashindex: more tests for basics KeyError test failing due to bug. --- src/borg/testsuite/hashindex.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 81c1d22d..31e3d7a7 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -44,6 +44,15 @@ class HashIndexTestCase(BaseTestCase): # Test delete for x in range(50): del idx[H(x)] + # Test some keys still in there + for x in range(50, 100): + assert H(x) in idx + # Test some keys not there any more + for x in range(50): + assert H(x) not in idx + # Test delete non-existing key + for x in range(50): + self.assert_raises(KeyError, idx.__delitem__, H(x)) self.assert_equal(len(idx), 50) idx_name = tempfile.NamedTemporaryFile() idx.write(idx_name.name) From 72ef24cbc0d6008498defea618dee98f3d3eabc0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 17 Jun 2017 20:00:06 +0200 Subject: [PATCH 1077/1387] hashindex: implement KeyError --- src/borg/_hashindex.c | 4 +++- src/borg/hashindex.pyx | 9 +++++++-- src/borg/helpers.py | 2 +- src/borg/testsuite/hashindex.py | 4 ++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/borg/_hashindex.c b/src/borg/_hashindex.c index 14206efe..d49574bc 100644 --- a/src/borg/_hashindex.c +++ b/src/borg/_hashindex.c @@ -160,6 +160,8 @@ hashindex_lookup(HashIndex *index, const void *key, int *start_idx) } else if(BUCKET_MATCHES_KEY(index, idx, key)) { if (didx != -1) { + // note: although lookup is logically a read-only operation, + // we optimize (change) the hashindex here "on the fly". memcpy(BUCKET_ADDR(index, didx), BUCKET_ADDR(index, idx), index->bucket_size); BUCKET_MARK_DELETED(index, idx); idx = didx; @@ -592,7 +594,7 @@ hashindex_delete(HashIndex *index, const void *key) { int idx = hashindex_lookup(index, key, NULL); if (idx < 0) { - return 1; + return -1; } BUCKET_MARK_DELETED(index, idx); index->num_entries -= 1; diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index bf84d6d4..b8e86d14 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -9,7 +9,7 @@ from libc.errno cimport errno from cpython.exc cimport PyErr_SetFromErrnoWithFilename from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release -API_VERSION = '1.1_04' +API_VERSION = '1.1_05' cdef extern from "_hashindex.c": @@ -117,7 +117,12 @@ cdef class IndexBase: def __delitem__(self, key): assert len(key) == self.key_size - if not hashindex_delete(self.index, key): + rc = hashindex_delete(self.index, key) + if rc == 1: + return # success + if rc == -1: + raise KeyError(key) + if rc == 0: raise Exception('hashindex_delete failed') def get(self, key, default=None): diff --git a/src/borg/helpers.py b/src/borg/helpers.py index fdacd141..9100bedf 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -131,7 +131,7 @@ class MandatoryFeatureUnsupported(Error): def check_extension_modules(): from . import platform, compress, item - if hashindex.API_VERSION != '1.1_04': + if hashindex.API_VERSION != '1.1_05': raise ExtensionModuleError if chunker.API_VERSION != '1.1_01': raise ExtensionModuleError diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 31e3d7a7..0b4b3bc5 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -73,11 +73,11 @@ class HashIndexTestCase(BaseTestCase): def test_nsindex(self): self._generic_test(NSIndex, lambda x: (x, x), - 'b96ec1ddabb4278cc92261ee171f7efc979dc19397cc5e89b778f05fa25bf93f') + '85f72b036c692c8266e4f51ccf0cff2147204282b5e316ae508d30a448d88fef') def test_chunkindex(self): self._generic_test(ChunkIndex, lambda x: (x, x, x), - '9d437a1e145beccc790c69e66ba94fc17bd982d83a401c9c6e524609405529d8') + 'c83fdf33755fc37879285f2ecfc5d1f63b97577494902126b6fb6f3e4d852488') def test_resize(self): n = 2000 # Must be >= MIN_BUCKETS From 726051b9d1a0d79f76f743b1dcb46ae0ace52c71 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 17 Jun 2017 20:17:08 +0200 Subject: [PATCH 1078/1387] fix double delete in rebuild_refcounts in case of the Manifest having an IntegrityError, the entry for the manifest was already deleted. --- src/borg/archive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 91239bdc..ecb34b06 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1328,8 +1328,8 @@ class ArchiveChecker: Missing and/or incorrect data is repaired when detected """ - # Exclude the manifest from chunks - del self.chunks[Manifest.MANIFEST_ID] + # Exclude the manifest from chunks (manifest entry might be already deleted from self.chunks) + self.chunks.pop(Manifest.MANIFEST_ID, None) def mark_as_possibly_superseded(id_): if self.chunks.get(id_, ChunkIndexEntry(0, 0, 0)).refcount == 0: From 3fea2ac05e84dfbac3bc168aec824d582caeacfa Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 01:24:27 +0200 Subject: [PATCH 1079/1387] docs: switch to a fresher theme incidentally, fixes font size mismatches on firefox and chrome, at the same time. marvellous. --- docs/_templates/globaltoc.html | 10 ++++++++ docs/_templates/logo-text.html | 5 ++++ docs/borg_theme/css/borg.css | 40 +++++++++++++++++++++++++++--- docs/conf.py | 45 +++++++++++++++++----------------- requirements.d/docs.txt | 2 ++ 5 files changed, 76 insertions(+), 26 deletions(-) create mode 100644 docs/_templates/globaltoc.html create mode 100644 docs/_templates/logo-text.html create mode 100644 requirements.d/docs.txt diff --git a/docs/_templates/globaltoc.html b/docs/_templates/globaltoc.html new file mode 100644 index 00000000..7841437c --- /dev/null +++ b/docs/_templates/globaltoc.html @@ -0,0 +1,10 @@ + diff --git a/docs/_templates/logo-text.html b/docs/_templates/logo-text.html new file mode 100644 index 00000000..dde5c929 --- /dev/null +++ b/docs/_templates/logo-text.html @@ -0,0 +1,5 @@ + diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 805e72e5..52a14eba 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -39,12 +39,45 @@ dt code { font-weight: normal; } -.experimental, +/* bootstrap has a .container class which clashes with docutils' container class. */ +.docutils.container { + width: auto; + margin: 0; + padding: 0; +} + +/* the default (38px) produces a jumpy baseline in Firefox on Linux. */ +h1 { + font-size: 36px; +} + +.text-logo { + background-color: #000200; + color: #00dd00; +} + +.text-logo:hover, +.text-logo:active, +.text-logo:focus { + color: #5afe57; +} + +/* by default the top and bottom margins are unequal which looks a bit unbalanced. */ +.sidebar-block { + padding: 0; + margin: 14px 0 14px 0; +} + +#borg-documentation .external img { + width: 100%; +} + +.container.experimental, #debugging-facilities, #borg-recreate { /* don't change text dimensions */ - margin: 0 -40px; /* padding below + border width */ - padding: 0 20px; /* 20 px visual margin between edge of text and the border */ + margin: 0 -30px; /* padding below + border width */ + padding: 0 10px; /* 10 px visual margin between edge of text and the border */ /* fallback for browsers that don't have repeating-linear-gradient: thick, red lines */ border-left: 20px solid red; border-right: 20px solid red; @@ -98,7 +131,6 @@ p .literal span { cite { white-space: nowrap; color: black; /* slight contrast with #404040 of regular text */ - font-size: 75%; font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; font-style: normal; diff --git a/docs/conf.py b/docs/conf.py index 4804bcff..8f6b867c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,8 +19,6 @@ sys.path.insert(0, os.path.abspath('../src')) from borg import __version__ as sw_version -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' - # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. @@ -51,7 +49,8 @@ copyright = '2010-2014 Jonas Borgström, 2015-2017 The Borg Collective (see AUTH # built documents. # # The short X.Y version. -version = sw_version.split('-')[0] +split_char = '+' if '+' in sw_version else '-' +version = sw_version.split(split_char)[0] # The full version, including alpha/beta/rc tags. release = version @@ -100,25 +99,21 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -#html_theme = '' -if not on_rtd: # only import and set the theme if we're building docs locally - import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - html_style = 'css/borg.css' -else: - html_context = { - 'css_files': [ - 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', - 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', - '_static/css/borg.css', - ], - } +import guzzle_sphinx_theme + +html_theme_path = guzzle_sphinx_theme.html_theme_path() +html_theme = 'guzzle_sphinx_theme' + + +def setup(app): + app.add_stylesheet('css/borg.css') # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +html_theme_options = { + 'project_nav_name': 'Borg %s' % version, +} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = ['_themes'] @@ -132,7 +127,7 @@ else: # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = '_static/logo.png' +html_logo = '_static/logo.svg' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 @@ -156,9 +151,9 @@ html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { - 'index': ['sidebarlogo.html', 'sidebarusefullinks.html', 'searchbox.html'], - '**': ['sidebarlogo.html', 'relations.html', 'searchbox.html', 'localtoc.html', 'sidebarusefullinks.html'] + '**': ['logo-text.html', 'searchbox.html', 'globaltoc.html'], } + # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} @@ -248,7 +243,13 @@ man_pages = [ 1), ] -extensions = ['sphinx.ext.extlinks', 'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] +extensions = [ + 'sphinx.ext.extlinks', + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', +] extlinks = { 'issue': ('https://github.com/borgbackup/borg/issues/%s', '#'), diff --git a/requirements.d/docs.txt b/requirements.d/docs.txt new file mode 100644 index 00000000..b63e8185 --- /dev/null +++ b/requirements.d/docs.txt @@ -0,0 +1,2 @@ +sphinx +guzzle_sphinx_theme From 7df1140ab934e2cc3988c45f86c7f9a0e0cc3006 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 01:24:37 +0200 Subject: [PATCH 1080/1387] docs: installation: fix accidental block quotes --- docs/installation.rst | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index c9947f81..962e11f8 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -173,20 +173,20 @@ Features & platforms Besides regular file and directory structures, |project_name| can preserve - * Symlinks (stored as symlink, the symlink is not followed) - * Special files: +* Symlinks (stored as symlink, the symlink is not followed) +* Special files: - * Character and block device files (restored via mknod) - * FIFOs ("named pipes") - * Special file *contents* can be backed up in ``--read-special`` mode. - By default the metadata to create them with mknod(2), mkfifo(2) etc. is stored. - * Hardlinked regular files, devices, FIFOs (considering all items in the same archive) - * Timestamps in nanosecond precision: mtime, atime, ctime - * Permissions: + * Character and block device files (restored via mknod) + * FIFOs ("named pipes") + * Special file *contents* can be backed up in ``--read-special`` mode. + By default the metadata to create them with mknod(2), mkfifo(2) etc. is stored. +* Hardlinked regular files, devices, FIFOs (considering all items in the same archive) +* Timestamps in nanosecond precision: mtime, atime, ctime +* Permissions: - * IDs of owning user and owning group - * Names of owning user and owning group (if the IDs can be resolved) - * Unix Mode/Permissions (u/g/o permissions, suid, sgid, sticky) + * IDs of owning user and owning group + * Names of owning user and owning group (if the IDs can be resolved) + * Unix Mode/Permissions (u/g/o permissions, suid, sgid, sticky) On some platforms additional features are supported: From 887df51eefafe591c72aef80ecad6054e2dad9a6 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 01:31:32 +0200 Subject: [PATCH 1081/1387] docs: development: update docs remarks --- docs/development.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/development.rst b/docs/development.rst index 47e57725..37cc9ad6 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -237,9 +237,10 @@ Building the docs with Sphinx The documentation (in reStructuredText format, .rst) is in docs/. -To build the html version of it, you need to have sphinx installed:: +To build the html version of it, you need to have Sphinx installed +(in your Borg virtualenv with Python 3):: - pip3 install sphinx sphinx_rtd_theme # important: this will install sphinx with Python 3 + pip install -r requirements.d/docs.txt Now run:: @@ -248,7 +249,7 @@ Now run:: Then point a web browser at docs/_build/html/index.html. -The website is updated automatically through Github web hooks on the +The website is updated automatically by ReadTheDocs through GitHub web hooks on the main repository. Using Vagrant From e880c7cac61d271ee099dd72ddca807b3b8a06b1 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 01:37:32 +0200 Subject: [PATCH 1082/1387] docs: sidebar pixel-tweak --- docs/borg_theme/css/borg.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 52a14eba..3a514579 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -65,7 +65,7 @@ h1 { /* by default the top and bottom margins are unequal which looks a bit unbalanced. */ .sidebar-block { padding: 0; - margin: 14px 0 14px 0; + margin: 14px 0 24px 0; } #borg-documentation .external img { From 658fd2521f99309e7e6a77b1b1b90486c037fdc1 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 01:53:14 +0200 Subject: [PATCH 1083/1387] docs: fix overeager CSS selector ballooning badges --- docs/borg_theme/css/borg.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 3a514579..3f4560b3 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -68,7 +68,7 @@ h1 { margin: 14px 0 24px 0; } -#borg-documentation .external img { +#borg-documentation h1 + p .external img { width: 100%; } From 334fa9f3223d9531ddc35d0a8c66b690c34ca3e0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 01:56:36 +0200 Subject: [PATCH 1084/1387] docs: add some cell-padding to tables --- docs/borg_theme/css/borg.css | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 3f4560b3..591ba0b9 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -94,24 +94,9 @@ h1 { border-right: 2px solid #4e4a4a;; } -/* the rtd theme has "nowrap" here which causes tables to have scroll bars. - * undo that setting. it does not seem to cause issues, even when making the - * viewport narrow. - */ -.wy-table-responsive table td, .wy-table-responsive table th { - white-space: normal; -} - -/* for some reason the rtd theme makes text in tables very small. - * fix that. - */ -.wy-table td, -.rst-content table.docutils td, -.rst-content table.field-list td, -.wy-table th, -.rst-content table.docutils th, -.rst-content table.field-list th { - font-size: 100%; +table.docutils td, +table.docutils th { + padding: .2em; } code, From 8aa745ddbd8ee1cddb3374673eb3eb08a9d5a8da Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 10 Jun 2017 17:59:41 +0200 Subject: [PATCH 1085/1387] create: --no-cache-sync --- conftest.py | 6 +- docs/internals/frontends.rst | 1 + src/borg/archiver.py | 4 +- src/borg/cache.py | 259 +++++++++++++++++++++++++++++---- src/borg/hashindex.pyx | 19 ++- src/borg/helpers.py | 10 +- src/borg/repository.py | 2 +- src/borg/testsuite/archiver.py | 20 ++- 8 files changed, 282 insertions(+), 39 deletions(-) diff --git a/conftest.py b/conftest.py index e85ae6ef..cc428be1 100644 --- a/conftest.py +++ b/conftest.py @@ -62,16 +62,16 @@ def pytest_report_header(config, startdir): class DefaultPatches: def __init__(self, request): - self.org_cache_wipe_cache = borg.cache.Cache.wipe_cache + self.org_cache_wipe_cache = borg.cache.LocalCache.wipe_cache def wipe_should_not_be_called(*a, **kw): raise AssertionError("Cache wipe was triggered, if this is part of the test add @pytest.mark.allow_cache_wipe") if 'allow_cache_wipe' not in request.keywords: - borg.cache.Cache.wipe_cache = wipe_should_not_be_called + borg.cache.LocalCache.wipe_cache = wipe_should_not_be_called request.addfinalizer(self.undo) def undo(self): - borg.cache.Cache.wipe_cache = self.org_cache_wipe_cache + borg.cache.LocalCache.wipe_cache = self.org_cache_wipe_cache @pytest.fixture(autouse=True) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 4000bede..c41d427e 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -504,6 +504,7 @@ Errors Operations - cache.begin_transaction + - cache.download_chunks, appears with ``borg create --no-cache-sync`` - cache.commit - cache.sync diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 4536b83d..579a0bfc 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -504,7 +504,7 @@ class Archiver: t0_monotonic = time.monotonic() if not dry_run: with Cache(repository, key, manifest, do_files=args.cache_files, progress=args.progress, - lock_wait=self.lock_wait) as cache: + lock_wait=self.lock_wait, permit_adhoc_cache=args.no_cache_sync) as cache: archive = Archive(repository, key, manifest, args.location.archive, cache=cache, create=True, checkpoint_interval=args.checkpoint_interval, numeric_owner=args.numeric_owner, noatime=args.noatime, noctime=args.noctime, @@ -2826,6 +2826,8 @@ class Archiver: help='only display items with the given status characters') subparser.add_argument('--json', action='store_true', help='output stats as JSON (implies --stats)') + subparser.add_argument('--no-cache-sync', dest='no_cache_sync', action='store_true', + help='experimental: do not synchronize the cache') exclude_group = subparser.add_argument_group('Exclusion options') exclude_group.add_argument('-e', '--exclude', dest='patterns', diff --git a/src/borg/cache.py b/src/borg/cache.py index 70d6f029..280df6c7 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -4,6 +4,7 @@ import shutil import stat from binascii import unhexlify from collections import namedtuple +from time import perf_counter import msgpack @@ -30,6 +31,7 @@ from .crypto.file_integrity import IntegrityCheckedFile, DetachedIntegrityChecke from .locking import Lock from .platform import SaveFile from .remote import cache_if_remote +from .repository import LIST_SCAN_LIMIT FileCacheEntry = namedtuple('FileCacheEntry', 'age inode size mtime chunk_ids') @@ -347,6 +349,69 @@ class Cache: os.remove(config) # kill config first shutil.rmtree(path) + def __new__(cls, repository, key, manifest, path=None, sync=True, do_files=False, warn_if_unencrypted=True, + progress=False, lock_wait=None, permit_adhoc_cache=False): + def local(): + return LocalCache(repository=repository, key=key, manifest=manifest, path=path, sync=sync, + do_files=do_files, warn_if_unencrypted=warn_if_unencrypted, progress=progress, + lock_wait=lock_wait) + + def adhoc(): + return AdHocCache(repository=repository, key=key, manifest=manifest) + + if not permit_adhoc_cache: + return local() + + # ad-hoc cache may be permitted, but if the local cache is in sync it'd be stupid to invalidate + # it by needlessly using the ad-hoc cache. + # Check if the local cache exists and is in sync. + + cache_config = CacheConfig(repository, path, lock_wait) + if cache_config.exists(): + with cache_config: + cache_in_sync = cache_config.manifest_id == manifest.id + # Don't nest cache locks + if cache_in_sync: + # Local cache is in sync, use it + logger.debug('Cache: choosing local cache (in sync)') + return local() + logger.debug('Cache: choosing ad-hoc cache (local cache does not exist or is not in sync)') + return adhoc() + + +class CacheStatsMixin: + str_format = """\ +All archives: {0.total_size:>20s} {0.total_csize:>20s} {0.unique_csize:>20s} + + Unique chunks Total chunks +Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" + + def __str__(self): + return self.str_format.format(self.format_tuple()) + + Summary = namedtuple('Summary', ['total_size', 'total_csize', 'unique_size', 'unique_csize', 'total_unique_chunks', + 'total_chunks']) + + def stats(self): + # XXX: this should really be moved down to `hashindex.pyx` + stats = self.Summary(*self.chunks.summarize())._asdict() + return stats + + def format_tuple(self): + stats = self.stats() + for field in ['total_size', 'total_csize', 'unique_csize']: + stats[field] = format_file_size(stats[field]) + return self.Summary(**stats) + + def chunks_stored_size(self): + return self.stats()['unique_csize'] + + +class LocalCache(CacheStatsMixin): + """ + Persistent, local (client-side) cache. + """ + def __init__(self, repository, key, manifest, path=None, sync=True, do_files=False, warn_if_unencrypted=True, progress=False, lock_wait=None): """ @@ -394,31 +459,6 @@ class Cache: def __exit__(self, exc_type, exc_val, exc_tb): self.close() - def __str__(self): - fmt = """\ -All archives: {0.total_size:>20s} {0.total_csize:>20s} {0.unique_csize:>20s} - - Unique chunks Total chunks -Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" - return fmt.format(self.format_tuple()) - - Summary = namedtuple('Summary', ['total_size', 'total_csize', 'unique_size', 'unique_csize', 'total_unique_chunks', - 'total_chunks']) - - def stats(self): - # XXX: this should really be moved down to `hashindex.pyx` - stats = self.Summary(*self.chunks.summarize())._asdict() - return stats - - def format_tuple(self): - stats = self.stats() - for field in ['total_size', 'total_csize', 'unique_csize']: - stats[field] = format_file_size(stats[field]) - return self.Summary(**stats) - - def chunks_stored_size(self): - return self.stats()['unique_csize'] - def create(self): """Create a new empty cache at `self.path` """ @@ -547,10 +587,14 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" archive indexes. """ archive_path = os.path.join(self.path, 'chunks.archive.d') + # An index of chunks were the size had to be fetched + chunks_fetched_size_index = ChunkIndex() # Instrumentation processed_item_metadata_bytes = 0 processed_item_metadata_chunks = 0 compact_chunks_archive_saved_space = 0 + fetched_chunks_for_csize = 0 + fetched_bytes_for_csize = 0 def mkpath(id, suffix=''): id_hex = bin_to_hex(id) @@ -588,6 +632,34 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" except FileNotFoundError: pass + def fetch_missing_csize(chunk_idx): + """ + Archives created with AdHocCache will have csize=0 in all chunk list entries whose + chunks were already in the repository. + + Scan *chunk_idx* for entries where csize=0 and fill in the correct information. + """ + nonlocal fetched_chunks_for_csize + nonlocal fetched_bytes_for_csize + + all_missing_ids = chunk_idx.zero_csize_ids() + fetch_ids = [] + for id_ in all_missing_ids: + already_fetched_entry = chunks_fetched_size_index.get(id_) + if already_fetched_entry: + entry = chunk_idx[id_]._replace(csize=already_fetched_entry.csize) + assert entry.size == already_fetched_entry.size, 'Chunk size mismatch' + chunk_idx[id_] = entry + else: + fetch_ids.append(id_) + + for id_, data in zip(fetch_ids, decrypted_repository.repository.get_many(fetch_ids)): + entry = chunk_idx[id_]._replace(csize=len(data)) + chunk_idx[id_] = entry + chunks_fetched_size_index[id_] = entry + fetched_chunks_for_csize += 1 + fetched_bytes_for_csize += len(data) + def fetch_and_build_idx(archive_id, decrypted_repository, chunk_idx): nonlocal processed_item_metadata_bytes nonlocal processed_item_metadata_chunks @@ -603,6 +675,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" processed_item_metadata_chunks += 1 sync.feed(data) if self.do_cache: + fetch_missing_csize(chunk_idx) write_archive_index(archive_id, chunk_idx) def write_archive_index(archive_id, chunk_idx): @@ -698,8 +771,13 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" chunk_idx = chunk_idx or ChunkIndex(master_index_capacity) logger.info('Fetching archive index for %s ...', archive_name) fetch_and_build_idx(archive_id, decrypted_repository, chunk_idx) + if not self.do_cache: + fetch_missing_csize(chunk_idx) pi.finish() - logger.debug('Cache sync: processed %s bytes (%d chunks) of metadata', + logger.debug('Cache sync: had to fetch %s (%d chunks) because no archive had a csize set for them ' + '(due to --no-cache-sync)', + format_file_size(fetched_bytes_for_csize), fetched_chunks_for_csize) + logger.debug('Cache sync: processed %s (%d chunks) of metadata', format_file_size(processed_item_metadata_bytes), processed_item_metadata_chunks) logger.debug('Cache sync: compact chunks.archive.d storage saved %s bytes', format_file_size(compact_chunks_archive_saved_space)) @@ -843,3 +921,132 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" entry = FileCacheEntry(age=0, inode=st.st_ino, size=st.st_size, mtime=int_to_bigint(mtime_ns), chunk_ids=ids) self.files[path_hash] = msgpack.packb(entry) self._newest_mtime = max(self._newest_mtime or 0, mtime_ns) + + +class AdHocCache(CacheStatsMixin): + """ + Ad-hoc, non-persistent cache. + + Compared to the standard LocalCache the AdHocCache does not maintain accurate reference count, + nor does it provide a files cache (which would require persistence). Chunks that were not added + during the current AdHocCache lifetime won't have correct size/csize set (0 bytes) and will + have an infinite reference count (MAX_VALUE). + """ + + str_format = """\ +All archives: unknown unknown unknown + + Unique chunks Total chunks +Chunk index: {0.total_unique_chunks:20d} unknown""" + + def __init__(self, repository, key, manifest, warn_if_unencrypted=True): + self.repository = repository + self.key = key + self.manifest = manifest + self._txn_active = False + + self.security_manager = SecurityManager(repository) + self.security_manager.assert_secure(manifest, key) + + logger.warning('Note: --no-cache-sync is an experimental feature.') + + # Public API + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + files = None + do_files = False + + def file_known_and_unchanged(self, path_hash, st, ignore_inode=False): + pass + + def memorize_file(self, path_hash, st, ids): + pass + + def add_chunk(self, id, chunk, stats, overwrite=False, wait=True): + assert not overwrite, 'AdHocCache does not permit overwrites — trying to use it for recreate?' + if not self._txn_active: + self._begin_txn() + size = len(chunk) + refcount = self.seen_chunk(id, size) + if refcount: + return self.chunk_incref(id, stats) + data = self.key.encrypt(chunk) + csize = len(data) + self.repository.put(id, data, wait=wait) + self.chunks.add(id, 1, size, csize) + stats.update(size, csize, not refcount) + return ChunkListEntry(id, size, csize) + + def seen_chunk(self, id, size=None): + return self.chunks.get(id, ChunkIndexEntry(0, None, None)).refcount + + def chunk_incref(self, id, stats): + if not self._txn_active: + self._begin_txn() + count, size, csize = self.chunks.incref(id) + stats.update(size, csize, False) + return ChunkListEntry(id, size, csize) + + def chunk_decref(self, id, stats, wait=True): + if not self._txn_active: + self._begin_txn() + count, size, csize = self.chunks.decref(id) + if count == 0: + del self.chunks[id] + self.repository.delete(id, wait=wait) + stats.update(-size, -csize, True) + else: + stats.update(-size, -csize, False) + + def commit(self): + if not self._txn_active: + return + self.security_manager.save(self.manifest, self.key) + self._txn_active = False + + def rollback(self): + self._txn_active = False + del self.chunks + + # Private API + + def _begin_txn(self): + self._txn_active = True + # Explicitly set the initial hash table capacity to avoid performance issues + # due to hash table "resonance". + # Since we're creating an archive, add 10 % from the start. + num_chunks = len(self.repository) + capacity = int(num_chunks / ChunkIndex.MAX_LOAD_FACTOR * 1.1) + self.chunks = ChunkIndex(capacity) + pi = ProgressIndicatorPercent(total=num_chunks, msg='Downloading chunk list... %3.0f%%', + msgid='cache.download_chunks') + t0 = perf_counter() + num_requests = 0 + marker = None + while True: + result = self.repository.list(limit=LIST_SCAN_LIMIT, marker=marker) + num_requests += 1 + if not result: + break + pi.show(increase=len(result)) + marker = result[-1] + # All chunks from the repository have a refcount of MAX_VALUE, which is sticky, + # therefore we can't/won't delete them. Chunks we added ourselves in this transaction + # (e.g. checkpoint archives) are tracked correctly. + init_entry = ChunkIndexEntry(refcount=ChunkIndex.MAX_VALUE, size=0, csize=0) + for id_ in result: + self.chunks[id_] = init_entry + assert len(self.chunks) == num_chunks + # LocalCache does not contain the manifest, either. + del self.chunks[self.manifest.MANIFEST_ID] + duration = perf_counter() - t0 + pi.finish() + logger.debug('AdHocCache: downloaded %d chunk IDs in %.2f s (%d requests), ~%s/s', + num_chunks, duration, num_requests, format_file_size(num_chunks * 34 / duration)) + # Chunk IDs in a list are encoded in 34 bytes: 1 byte msgpack header, 1 byte length, 32 ID bytes. + # Protocol overhead is neglected in this calculation. diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index b8e86d14..0d271ad6 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -8,8 +8,9 @@ from libc.stdint cimport uint32_t, UINT32_MAX, uint64_t from libc.errno cimport errno from cpython.exc cimport PyErr_SetFromErrnoWithFilename from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release +from cpython.bytes cimport PyBytes_FromStringAndSize -API_VERSION = '1.1_05' +API_VERSION = '1.1_06' cdef extern from "_hashindex.c": @@ -410,6 +411,22 @@ cdef class ChunkIndex(IndexBase): break self._add(key, (key + self.key_size)) + def zero_csize_ids(self): + cdef void *key = NULL + cdef uint32_t *values + entries = [] + while True: + key = hashindex_next_key(self.index, key) + if not key: + break + values = (key + self.key_size) + refcount = _le32toh(values[0]) + assert refcount <= _MAX_VALUE, "invalid reference count" + if _le32toh(values[2]) == 0: + # csize == 0 + entries.append(PyBytes_FromStringAndSize( key, self.key_size)) + return entries + cdef class ChunkKeyIterator: cdef ChunkIndex idx diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 9100bedf..870d2021 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -131,7 +131,7 @@ class MandatoryFeatureUnsupported(Error): def check_extension_modules(): from . import platform, compress, item - if hashindex.API_VERSION != '1.1_05': + if hashindex.API_VERSION != '1.1_06': raise ExtensionModuleError if chunker.API_VERSION != '1.1_01': raise ExtensionModuleError @@ -2010,7 +2010,7 @@ class BorgJsonEncoder(json.JSONEncoder): from .repository import Repository from .remote import RemoteRepository from .archive import Archive - from .cache import Cache + from .cache import LocalCache, AdHocCache if isinstance(o, Repository) or isinstance(o, RemoteRepository): return { 'id': bin_to_hex(o.id), @@ -2018,11 +2018,15 @@ class BorgJsonEncoder(json.JSONEncoder): } if isinstance(o, Archive): return o.info() - if isinstance(o, Cache): + if isinstance(o, LocalCache): return { 'path': o.path, 'stats': o.stats(), } + if isinstance(o, AdHocCache): + return { + 'stats': o.stats(), + } return super().default(o) diff --git a/src/borg/repository.py b/src/borg/repository.py index d15d51b1..f73e9cf5 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -34,7 +34,7 @@ TAG_PUT = 0 TAG_DELETE = 1 TAG_COMMIT = 2 -LIST_SCAN_LIMIT = 10000 # repo.list() / .scan() result count limit the borg client uses +LIST_SCAN_LIMIT = 100000 # repo.list() / .scan() result count limit the borg client uses FreeSpace = partial(defaultdict, int) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index e9bcef5e..4e7cb5a5 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -34,7 +34,7 @@ import borg from .. import xattr, helpers, platform from ..archive import Archive, ChunkBuffer, flags_noatime, flags_normal from ..archiver import Archiver, parse_storage_quota -from ..cache import Cache +from ..cache import Cache, LocalCache from ..constants import * # NOQA from ..crypto.low_level import bytes_to_long, num_aes_blocks from ..crypto.key import KeyfileKeyBase, RepoKey, KeyfileKey, Passphrase, TAMRequiredError @@ -1031,6 +1031,18 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert out_list.index('d x/a') < out_list.index('- x/a/foo_a') assert out_list.index('d x/b') < out_list.index('- x/b/foo_b') + def test_create_no_cache_sync(self): + self.create_test_files() + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('delete', '--cache-only', self.repository_location) + create_json = json.loads(self.cmd('create', '--no-cache-sync', self.repository_location + '::test', 'input', + '--json', '--error')) # ignore experimental warning + info_json = json.loads(self.cmd('info', self.repository_location + '::test', '--json')) + create_stats = create_json['cache']['stats'] + info_stats = info_json['cache']['stats'] + assert create_stats == info_stats + self.cmd('check', self.repository_location) + def test_extract_pattern_opt(self): self.cmd('init', '--encryption=repokey', self.repository_location) self.create_regular_file('file1', size=1024 * 80) @@ -1509,14 +1521,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('create', self.repository_location + '::test', 'input') else: called = False - wipe_cache_safe = Cache.wipe_cache + wipe_cache_safe = LocalCache.wipe_cache def wipe_wrapper(*args): nonlocal called called = True wipe_cache_safe(*args) - with patch.object(Cache, 'wipe_cache', wipe_wrapper): + with patch.object(LocalCache, 'wipe_cache', wipe_wrapper): self.cmd('create', self.repository_location + '::test', 'input') assert called @@ -2223,7 +2235,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) with Cache(repository, key, manifest, sync=False) as cache: original_chunks = cache.chunks - cache.destroy(repository) + Cache.destroy(repository) with Cache(repository, key, manifest) as cache: correct_chunks = cache.chunks assert original_chunks is not correct_chunks From fc7c56034528271ee4fdb3d21e5f9f803681edd4 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 10 Jun 2017 19:17:17 +0200 Subject: [PATCH 1086/1387] AdHocCache: fix size not propagating to incref --- src/borg/cache.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 280df6c7..cb628a4e 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -974,7 +974,7 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" size = len(chunk) refcount = self.seen_chunk(id, size) if refcount: - return self.chunk_incref(id, stats) + return self.chunk_incref(id, stats, size_=size) data = self.key.encrypt(chunk) csize = len(data) self.repository.put(id, data, wait=wait) @@ -985,12 +985,12 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" def seen_chunk(self, id, size=None): return self.chunks.get(id, ChunkIndexEntry(0, None, None)).refcount - def chunk_incref(self, id, stats): + def chunk_incref(self, id, stats, size_=None): if not self._txn_active: self._begin_txn() count, size, csize = self.chunks.incref(id) - stats.update(size, csize, False) - return ChunkListEntry(id, size, csize) + stats.update(size or size_, csize, False) + return ChunkListEntry(id, size or size_, csize) def chunk_decref(self, id, stats, wait=True): if not self._txn_active: From 3c8257432ae93d8eb5c12ce528339bc68f3ae8bd Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sat, 10 Jun 2017 19:22:20 +0200 Subject: [PATCH 1087/1387] cache sync: fetch_missing_csize don't check ids against empty idx This is always the case if self.do_cache is False. --- src/borg/cache.py | 19 +++++++++++-------- src/borg/testsuite/archiver.py | 3 +++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index cb628a4e..1550ae38 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -644,14 +644,17 @@ class LocalCache(CacheStatsMixin): all_missing_ids = chunk_idx.zero_csize_ids() fetch_ids = [] - for id_ in all_missing_ids: - already_fetched_entry = chunks_fetched_size_index.get(id_) - if already_fetched_entry: - entry = chunk_idx[id_]._replace(csize=already_fetched_entry.csize) - assert entry.size == already_fetched_entry.size, 'Chunk size mismatch' - chunk_idx[id_] = entry - else: - fetch_ids.append(id_) + if len(chunks_fetched_size_index): + for id_ in all_missing_ids: + already_fetched_entry = chunks_fetched_size_index.get(id_) + if already_fetched_entry: + entry = chunk_idx[id_]._replace(csize=already_fetched_entry.csize) + assert entry.size == already_fetched_entry.size, 'Chunk size mismatch' + chunk_idx[id_] = entry + else: + fetch_ids.append(id_) + else: + fetch_ids = all_missing_ids for id_, data in zip(fetch_ids, decrypted_repository.repository.get_many(fetch_ids)): entry = chunk_idx[id_]._replace(csize=len(data)) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 4e7cb5a5..b0b0a9b7 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1041,6 +1041,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): create_stats = create_json['cache']['stats'] info_stats = info_json['cache']['stats'] assert create_stats == info_stats + self.cmd('delete', '--cache-only', self.repository_location) + self.cmd('create', '--no-cache-sync', self.repository_location + '::test2', 'input') + self.cmd('info', self.repository_location) self.cmd('check', self.repository_location) def test_extract_pattern_opt(self): From 5eeca3493b27c200e40bbd7463a03b4a2db5f84b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 11 Jun 2017 11:29:41 +0200 Subject: [PATCH 1088/1387] TestAdHocCache --- src/borg/archiver.py | 2 +- src/borg/cache.py | 14 +++++-- src/borg/testsuite/cache.py | 83 ++++++++++++++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 579a0bfc..3275bf0b 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2827,7 +2827,7 @@ class Archiver: subparser.add_argument('--json', action='store_true', help='output stats as JSON (implies --stats)') subparser.add_argument('--no-cache-sync', dest='no_cache_sync', action='store_true', - help='experimental: do not synchronize the cache') + help='experimental: do not synchronize the cache. Implies --no-files-cache.') exclude_group = subparser.add_argument_group('Exclusion options') exclude_group.add_argument('-e', '--exclude', dest='patterns', diff --git a/src/borg/cache.py b/src/borg/cache.py index 1550ae38..db306420 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -587,7 +587,7 @@ class LocalCache(CacheStatsMixin): archive indexes. """ archive_path = os.path.join(self.path, 'chunks.archive.d') - # An index of chunks were the size had to be fetched + # An index of chunks whose size had to be fetched chunks_fetched_size_index = ChunkIndex() # Instrumentation processed_item_metadata_bytes = 0 @@ -965,7 +965,7 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" do_files = False def file_known_and_unchanged(self, path_hash, st, ignore_inode=False): - pass + return None def memorize_file(self, path_hash, st, ids): pass @@ -986,7 +986,15 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" return ChunkListEntry(id, size, csize) def seen_chunk(self, id, size=None): - return self.chunks.get(id, ChunkIndexEntry(0, None, None)).refcount + if not self._txn_active: + self._begin_txn() + entry = self.chunks.get(id, ChunkIndexEntry(0, None, None)) + if entry.refcount and size and not entry.size: + # The LocalCache has existing size information and uses *size* to make an effort at detecting collisions. + # This is of course not possible for the AdHocCache. + # Here *size* is used to update the chunk's size information, which will be zero for existing chunks. + self.chunks[id] = entry._replace(size=size) + return entry.refcount def chunk_incref(self, id, stats, size_=None): if not self._txn_active: diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py index 6f6452a1..6cce0cb7 100644 --- a/src/borg/testsuite/cache.py +++ b/src/borg/testsuite/cache.py @@ -1,11 +1,19 @@ import io +import os.path from msgpack import packb import pytest -from ..hashindex import ChunkIndex, CacheSynchronizer from .hashindex import H +from .key import TestKey +from ..archive import Statistics +from ..cache import AdHocCache +from ..compress import CompressionSpec +from ..crypto.key import RepoKey +from ..hashindex import ChunkIndex, CacheSynchronizer +from ..helpers import Manifest +from ..repository import Repository class TestCacheSynchronizer: @@ -196,3 +204,76 @@ class TestCacheSynchronizer: assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234, 5678) sync.feed(data) assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234, 5678) + + +class TestAdHocCache: + @pytest.yield_fixture + def repository(self, tmpdir): + self.repository_location = os.path.join(str(tmpdir), 'repository') + with Repository(self.repository_location, exclusive=True, create=True) as repository: + repository.put(H(1), b'1234') + repository.put(Manifest.MANIFEST_ID, b'5678') + yield repository + + @pytest.fixture + def key(self, repository, monkeypatch): + monkeypatch.setenv('BORG_PASSPHRASE', 'test') + key = RepoKey.create(repository, TestKey.MockArgs()) + key.compressor = CompressionSpec('none').compressor + return key + + @pytest.fixture + def manifest(self, repository, key): + Manifest(key, repository).write() + return Manifest.load(repository, key=key, operations=Manifest.NO_OPERATION_CHECK)[0] + + @pytest.fixture + def cache(self, repository, key, manifest): + return AdHocCache(repository, key, manifest) + + def test_does_not_contain_manifest(self, cache): + assert not cache.seen_chunk(Manifest.MANIFEST_ID) + + def test_does_not_delete_existing_chunks(self, repository, cache): + assert cache.seen_chunk(H(1)) == ChunkIndex.MAX_VALUE + cache.chunk_decref(H(1), Statistics()) + assert repository.get(H(1)) == b'1234' + + def test_does_not_overwrite(self, cache): + with pytest.raises(AssertionError): + cache.add_chunk(H(1), b'5678', Statistics(), overwrite=True) + + def test_seen_chunk_add_chunk_size(self, cache): + assert cache.add_chunk(H(1), b'5678', Statistics()) == (H(1), 4, 0) + + def test_deletes_chunks_during_lifetime(self, cache, repository): + """E.g. checkpoint archives""" + cache.add_chunk(H(5), b'1010', Statistics()) + assert cache.seen_chunk(H(5)) == 1 + cache.chunk_decref(H(5), Statistics()) + assert not cache.seen_chunk(H(5)) + with pytest.raises(Repository.ObjectNotFound): + repository.get(H(5)) + + def test_files_cache(self, cache): + assert cache.file_known_and_unchanged(bytes(32), None) is None + assert not cache.do_files + assert cache.files is None + + def test_txn(self, cache): + assert not cache._txn_active + cache.seen_chunk(H(5)) + assert cache._txn_active + assert cache.chunks + cache.rollback() + assert not cache._txn_active + assert not hasattr(cache, 'chunks') + + def test_incref_after_add_chunk(self, cache): + assert cache.add_chunk(H(3), b'5678', Statistics()) == (H(3), 4, 47) + assert cache.chunk_incref(H(3), Statistics()) == (H(3), 4, 47) + + def test_existing_incref_after_add_chunk(self, cache): + """This case occurs with part files, see Archive.chunk_file.""" + assert cache.add_chunk(H(1), b'5678', Statistics()) == (H(1), 4, 0) + assert cache.chunk_incref(H(1), Statistics()) == (H(1), 4, 0) From 2cbff48fd3fec440620be2a1cc99bbc26b12b5dc Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 11 Jun 2017 20:11:34 +0200 Subject: [PATCH 1089/1387] AdHocCache: explicate chunk_incref assertion --- src/borg/cache.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/borg/cache.py b/src/borg/cache.py index db306420..595962da 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -1001,6 +1001,9 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" self._begin_txn() count, size, csize = self.chunks.incref(id) stats.update(size or size_, csize, False) + # When size is 0 and size_ is not given, then this chunk has not been locally visited yet (seen_chunk with + # size or add_chunk); we can't add references to those (size=0 is invalid) and generally don't try to. + assert size or size_ return ChunkListEntry(id, size or size_, csize) def chunk_decref(self, id, stats, wait=True): From 0462a561c1e3181cffb89401fcecdfbd25529ceb Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 12 Jun 2017 09:16:05 +0200 Subject: [PATCH 1090/1387] item: explicate csize isn't memorizable --- src/borg/item.pyx | 1 + src/borg/testsuite/item.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 5ca93404..91fe57ee 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -189,6 +189,7 @@ class Item(PropDict): If memorize is True, the computed size value will be stored into the item. """ attr = 'csize' if compressed else 'size' + assert not (compressed and memorize), 'Item does not have a csize field.' try: if from_chunks: raise AttributeError diff --git a/src/borg/testsuite/item.py b/src/borg/testsuite/item.py index 785a962c..f9d72f87 100644 --- a/src/borg/testsuite/item.py +++ b/src/borg/testsuite/item.py @@ -154,6 +154,11 @@ def test_item_file_size(): ChunkListEntry(csize=1, size=2000, id=None), ]) assert item.get_size() == 3000 + with pytest.raises(AssertionError): + item.get_size(compressed=True, memorize=True) + assert item.get_size(compressed=True) == 2 + item.get_size(memorize=True) + assert item.size == 3000 def test_item_file_size_no_chunks(): From 4689fd0c2298e71d1d728b4a92cd817b2dc66af5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 02:04:21 +0200 Subject: [PATCH 1091/1387] cache: explain fetch_missing_csize cost --- src/borg/cache.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/borg/cache.py b/src/borg/cache.py index 595962da..47b53deb 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -656,6 +656,8 @@ class LocalCache(CacheStatsMixin): else: fetch_ids = all_missing_ids + # This is potentially a rather expensive operation, but it's hard to tell at this point + # if it's a problem in practice (hence the experimental status of --no-cache-sync). for id_, data in zip(fetch_ids, decrypted_repository.repository.get_many(fetch_ids)): entry = chunk_idx[id_]._replace(csize=len(data)) chunk_idx[id_] = entry From 48e815883f231e0d733e5449813e4736c0071444 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 02:07:37 +0200 Subject: [PATCH 1092/1387] docs: usage: fix unintended block quota in common options --- docs/usage/common-options.rst.inc | 72 +++++++++++++++---------------- docs/usage/help.rst.inc | 9 ++++ docs/usage/init.rst.inc | 18 +++++--- docs/usage/upgrade.rst.inc | 2 +- setup.py | 6 +-- 5 files changed, 62 insertions(+), 45 deletions(-) diff --git a/docs/usage/common-options.rst.inc b/docs/usage/common-options.rst.inc index 7299aa72..6bc18eba 100644 --- a/docs/usage/common-options.rst.inc +++ b/docs/usage/common-options.rst.inc @@ -1,36 +1,36 @@ - ``-h``, ``--help`` - | show this help message and exit - ``--critical`` - | work on log level CRITICAL - ``--error`` - | work on log level ERROR - ``--warning`` - | work on log level WARNING (default) - ``--info``, ``-v``, ``--verbose`` - | work on log level INFO - ``--debug`` - | enable debug output, work on log level DEBUG - ``--debug-topic TOPIC`` - | enable TOPIC debugging (can be specified multiple times). The logger path is borg.debug. if TOPIC is not fully qualified. - ``-p``, ``--progress`` - | show progress information - ``--log-json`` - | Output one JSON object per log line instead of formatted text. - ``--lock-wait N`` - | wait for the lock, but max. N seconds (default: 1). - ``--show-version`` - | show/log the borg version - ``--show-rc`` - | show/log the return code (rc) - ``--no-files-cache`` - | do not load/update the file metadata cache used to detect unchanged files - ``--umask M`` - | set umask to M (local and remote, default: 0077) - ``--remote-path PATH`` - | use PATH as borg executable on the remote (default: "borg") - ``--remote-ratelimit rate`` - | set remote network upload rate limit in kiByte/s (default: 0=unlimited) - ``--consider-part-files`` - | treat part files like normal files (e.g. to list/extract them) - ``--debug-profile FILE`` - | Write execution profile in Borg format into FILE. For local use a Python-compatible file can be generated by suffixing FILE with ".pyprof". \ No newline at end of file +``-h``, ``--help`` + | show this help message and exit +``--critical`` + | work on log level CRITICAL +``--error`` + | work on log level ERROR +``--warning`` + | work on log level WARNING (default) +``--info``, ``-v``, ``--verbose`` + | work on log level INFO +``--debug`` + | enable debug output, work on log level DEBUG +``--debug-topic TOPIC`` + | enable TOPIC debugging (can be specified multiple times). The logger path is borg.debug. if TOPIC is not fully qualified. +``-p``, ``--progress`` + | show progress information +``--log-json`` + | Output one JSON object per log line instead of formatted text. +``--lock-wait N`` + | wait for the lock, but max. N seconds (default: 1). +``--show-version`` + | show/log the borg version +``--show-rc`` + | show/log the return code (rc) +``--no-files-cache`` + | do not load/update the file metadata cache used to detect unchanged files +``--umask M`` + | set umask to M (local and remote, default: 0077) +``--remote-path PATH`` + | use PATH as borg executable on the remote (default: "borg") +``--remote-ratelimit rate`` + | set remote network upload rate limit in kiByte/s (default: 0=unlimited) +``--consider-part-files`` + | treat part files like normal files (e.g. to list/extract them) +``--debug-profile FILE`` + | Write execution profile in Borg format into FILE. For local use a Python-compatible file can be generated by suffixing FILE with ".pyprof". \ No newline at end of file diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index 40789083..8d158ae7 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -72,6 +72,15 @@ Path full-match, selector `pf:` Other include/exclude patterns that would normally match will be ignored. Same logic applies for exclude. +.. note:: + + `re:`, `sh:` and `fm:` patterns are all implemented on top of the Python SRE + engine. It is very easy to formulate patterns for each of these types which + requires an inordinate amount of time to match paths. If untrusted users + are able to supply patterns, ensure they cannot supply `re:` patterns. + Further, ensure that `sh:` and `fm:` patterns only contain a handful of + wildcards at most. + Exclusions can be passed via the command line option `--exclude`. When used from within a shell the patterns should be quoted to protect them from expansion. diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index ec99fd66..88fe11c7 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -15,7 +15,7 @@ positional arguments optional arguments ``-e``, ``--encryption`` | select encryption key mode **(required)** - ``-a``, ``--append-only`` + ``--append-only`` | create an append-only mode repository ``--storage-quota`` | Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota. @@ -72,16 +72,23 @@ the encryption/decryption key or other secrets. Encryption modes ++++++++++++++++ +.. nanorst: inline-fill + +----------+---------------+------------------------+--------------------------+ | Hash/MAC | Not encrypted | Not encrypted, | Encrypted (AEAD w/ AES) | | | no auth | but authenticated | and authenticated | +----------+---------------+------------------------+--------------------------+ -| SHA-256 | none | authenticated | repokey, keyfile | +| SHA-256 | none | `authenticated` | repokey | +| | | | keyfile | +----------+---------------+------------------------+--------------------------+ -| BLAKE2b | n/a | authenticated-blake2 | repokey-blake2, | -| | | | keyfile-blake2 | +| BLAKE2b | n/a | `authenticated-blake2` | `repokey-blake2` | +| | | | `keyfile-blake2` | +----------+---------------+------------------------+--------------------------+ +.. nanorst: inline-replace + +`Marked modes` are new in Borg 1.1 and are not backwards-compatible with Borg 1.0.x. + On modern Intel/AMD CPUs (except very cheap ones), AES is usually hardware-accelerated. BLAKE2b is faster than SHA256 on Intel/AMD 64-bit CPUs @@ -114,7 +121,8 @@ This mode is new and *not* compatible with Borg 1.0.x. `none` mode uses no encryption and no authentication. It uses SHA256 as chunk ID hash. Not recommended, rather consider using an authenticated or -authenticated/encrypted mode. +authenticated/encrypted mode. This mode has possible denial-of-service issues +when running ``borg create`` on contents controlled by an attacker. Use it only for new repositories where no encryption is wanted **and** when compatibility with 1.0.x is important. If compatibility with 1.0.x is not important, use `authenticated-blake2` or `authenticated` instead. diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index bbf51724..cc40e8b2 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -15,7 +15,7 @@ positional arguments optional arguments ``-n``, ``--dry-run`` | do not change repository - ``-i``, ``--inplace`` + ``--inplace`` | rewrite repository in place, with no chance of going back to older | versions of the repository. ``--force`` diff --git a/setup.py b/setup.py index 864b352a..3b9b495b 100644 --- a/setup.py +++ b/setup.py @@ -274,7 +274,7 @@ class build_usage(Command): if 'create' in choices: common_options = [group for group in choices['create']._action_groups if group.title == 'Common options'][0] with open('docs/usage/common-options.rst.inc', 'w') as doc: - self.write_options_group(common_options, doc, False) + self.write_options_group(common_options, doc, False, base_indent=0) return is_subcommand @@ -294,7 +294,7 @@ class build_usage(Command): else: self.write_options_group(group, fp) - def write_options_group(self, group, fp, with_title=True): + def write_options_group(self, group, fp, with_title=True, base_indent=4): def is_positional_group(group): return any(not o.option_strings for o in group._group_actions) @@ -303,7 +303,7 @@ class build_usage(Command): return '\n'.join('| ' + line for line in text.splitlines()) def shipout(text): - fp.write(textwrap.indent('\n'.join(text), ' ' * 4)) + fp.write(textwrap.indent('\n'.join(text), ' ' * base_indent)) if not group._group_actions: return From 125b02c5c8495ef57e1808eba0bcbea1efbe4d14 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 02:10:10 +0200 Subject: [PATCH 1093/1387] docs: list: fix unintended block quote --- docs/usage/list.rst.inc | 96 +++++++++++++++++++---------------------- src/borg/helpers.py | 24 +++++------ 2 files changed, 56 insertions(+), 64 deletions(-) diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index 3c9d27e9..a1fedea9 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -67,66 +67,58 @@ See the "borg help patterns" command for more help on exclude patterns. The following keys are available for ``--format``: - - NEWLINE: OS dependent line separator - - NL: alias of NEWLINE - - NUL: NUL character for creating print0 / xargs -0 like output, see barchive/bpath - - SPACE - - TAB - - CR - - LF +- NEWLINE: OS dependent line separator +- NL: alias of NEWLINE +- NUL: NUL character for creating print0 / xargs -0 like output, see barchive/bpath +- SPACE +- TAB +- CR +- LF Keys for listing repository archives: - - archive, name: archive name interpreted as text (might be missing non-text characters, see barchive) - - barchive: verbatim archive name, can contain any character except NUL - - time: time of creation of the archive - - id: internal ID of the archive +- archive, name: archive name interpreted as text (might be missing non-text characters, see barchive) +- barchive: verbatim archive name, can contain any character except NUL +- time: time of creation of the archive +- id: internal ID of the archive Keys for listing archive files: - - type - - mode - - uid - - gid - - user - - group - - path: path interpreted as text (might be missing non-text characters, see bpath) - - bpath: verbatim POSIX path, can contain any character except NUL - - source: link target for links (identical to linktarget) - - linktarget - - flags +- type +- mode +- uid +- gid +- user +- group +- path: path interpreted as text (might be missing non-text characters, see bpath) +- bpath: verbatim POSIX path, can contain any character except NUL +- source: link target for links (identical to linktarget) +- linktarget +- flags - - size - - csize: compressed size - - dsize: deduplicated size - - dcsize: deduplicated compressed size - - num_chunks: number of chunks in this file - - unique_chunks: number of unique chunks in this file +- size +- csize: compressed size +- dsize: deduplicated size +- dcsize: deduplicated compressed size +- num_chunks: number of chunks in this file +- unique_chunks: number of unique chunks in this file - - mtime - - ctime - - atime - - isomtime - - isoctime - - isoatime +- mtime +- ctime +- atime +- isomtime +- isoctime +- isoatime - - blake2b - - blake2s - - md5 - - sha1 - - sha224 - - sha256 - - sha384 - - sha3_224 - - sha3_256 - - sha3_384 - - sha3_512 - - sha512 - - shake_128 - - shake_256 +- md5 +- sha1 +- sha224 +- sha256 +- sha384 +- sha512 - - archiveid - - archivename - - extra: prepends {source} with " -> " for soft links and " link to " for hard links +- archiveid +- archivename +- extra: prepends {source} with " -> " for soft links and " link to " for hard links - - health: either "healthy" (file ok) or "broken" (if file has all-zero replacement chunks) +- health: either "healthy" (file ok) or "broken" (if file has all-zero replacement chunks) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 9100bedf..aa7f841c 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1507,13 +1507,13 @@ class BaseFormatter: @staticmethod def keys_help(): - return " - NEWLINE: OS dependent line separator\n" \ - " - NL: alias of NEWLINE\n" \ - " - NUL: NUL character for creating print0 / xargs -0 like output, see barchive/bpath\n" \ - " - SPACE\n" \ - " - TAB\n" \ - " - CR\n" \ - " - LF" + return "- NEWLINE: OS dependent line separator\n" \ + "- NL: alias of NEWLINE\n" \ + "- NUL: NUL character for creating print0 / xargs -0 like output, see barchive/bpath\n" \ + "- SPACE\n" \ + "- TAB\n" \ + "- CR\n" \ + "- LF" class ArchiveFormatter(BaseFormatter): @@ -1535,10 +1535,10 @@ class ArchiveFormatter(BaseFormatter): @staticmethod def keys_help(): - return " - archive, name: archive name interpreted as text (might be missing non-text characters, see barchive)\n" \ - " - barchive: verbatim archive name, can contain any character except NUL\n" \ - " - time: time of creation of the archive\n" \ - " - id: internal ID of the archive" + return "- archive, name: archive name interpreted as text (might be missing non-text characters, see barchive)\n" \ + "- barchive: verbatim archive name, can contain any character except NUL\n" \ + "- time: time of creation of the archive\n" \ + "- id: internal ID of the archive" class ItemFormatter(BaseFormatter): @@ -1590,7 +1590,7 @@ class ItemFormatter(BaseFormatter): for group in cls.KEY_GROUPS: for key in group: keys.remove(key) - text = " - " + key + text = "- " + key if key in cls.KEY_DESCRIPTIONS: text += ": " + cls.KEY_DESCRIPTIONS[key] help.append(text) From 89f884588d16ffb97b2ad6600d9004615f56f8ce Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 02:12:47 +0200 Subject: [PATCH 1094/1387] docs: index: disable syntax highlight (bash) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 67e7a766..96afc1f0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,5 +1,5 @@ .. include:: global.rst.inc - +.. highlight:: none Borg Documentation ================== From 5e9f069ddea5206a47b5668d51f177da6fa79cdd Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 02:28:49 +0200 Subject: [PATCH 1095/1387] docs: neater table borders --- docs/borg_theme/css/borg.css | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 591ba0b9..3394f49c 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -94,11 +94,39 @@ h1 { border-right: 2px solid #4e4a4a;; } -table.docutils td, -table.docutils th { +table.docutils:not(.footnote) td, +table.docutils:not(.footnote) th { padding: .2em; } +table.docutils:not(.footnote) { + border-collapse: collapse; + border: none; +} + +table.docutils:not(.footnote) td, +table.docutils:not(.footnote) th { + border: 1px solid #ddd; +} + +table.docutils:not(.footnote) tr:first-child th { + border-top: 0; +} + +table.docutils:not(.footnote) tr:last-child td { + border-bottom: 0; +} + +table.docutils:not(.footnote) tr td:first-child, +table.docutils:not(.footnote) tr th:first-child { + border-left: 0; +} + +table.docutils:not(.footnote) tr td:last-child, +table.docutils:not(.footnote) tr th:last-child { + border-right: 0; +} + code, .rst-content tt.literal, .rst-content tt.literal, From bde828d74790389a94915db193251b8ad624358d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 02:34:25 +0200 Subject: [PATCH 1096/1387] docs: faq: fix typo and strange link text --- docs/faq.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 9108252a..64de9c48 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -355,12 +355,12 @@ Thus: - have media at another place - have a relatively recent backup on your media -How do I report security issue with |project_name|? ---------------------------------------------------- +How do I report a security issue with Borg? +------------------------------------------- -Send a private email to the :ref:`security-contact` if you think you -have discovered a security issue. Please disclose security issues -responsibly. +Send a private email to the :ref:`security contact ` +if you think you have discovered a security issue. +Please disclose security issues responsibly. Common issues ############# From 9031490cfd725293f37bcca61199788f8c9a06e0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 10 Jun 2017 20:58:27 +0200 Subject: [PATCH 1097/1387] update CHANGES (master) --- docs/changes.rst | 88 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index f167a116..837993c1 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -155,13 +155,25 @@ Compatibility notes: - Repositories in the "authenticated" mode are now treated as the unencrypted repositories they are. +- The client-side temporary repository cache now holds unencrypted data for better speed. + +- borg init: removed the short form of --append-only (-a). + +- borg upgrade: removed the short form of --inplace (-i). New features: -- integrity checking for important files used by borg: +- reimplemented the RepositoryCache, size-limited caching of decrypted repo + contents, integrity checked via xxh64. #2515 +- reduced space usage of chunks.archive.d. Existing caches are migrated during + a cache sync. #235 #2638 +- integrity checking using xxh64 for important files used by borg, #1101: - repository: index and hints files - - cache: chunks and files caches, archive.chunks.d + - cache: chunks and files caches, chunks.archive.d +- improve cache sync speed, #1729 +- create: new --no-cache-sync option +- add repository mandatory feature flags infrastructure, #1806 - Verify most operations against SecurityManager. Location, manifest timestamp and key types are now checked for almost all non-debug commands. #2487 - implement storage quotas, #2517 @@ -170,6 +182,11 @@ New features: - borg export-tar, #2519 - list: --json-lines instead of --json for archive contents, #2439 - add --debug-profile option (and also "borg debug convert-profile"), #2473 +- implement --glob-archives/-a, #2448 +- normalize authenticated key modes for better naming consistency: + + - rename "authenticated" to "authenticated-blake2" (uses blake2b) + - implement "authenticated" mode (uses hmac-sha256) Fixes: @@ -179,19 +196,35 @@ Fixes: error message when parsing fails. - mount: check whether llfuse is installed before asking for passphrase, #2540 - mount: do pre-mount checks before opening repository, #2541 -- FUSE: fix crash if empty (None) xattr is read, #2534 +- fuse: + + - fix crash if empty (None) xattr is read, #2534 + - fix read(2) caching data in metadata cache + - fix negative uid/gid crash (fix crash when mounting archives + of external drives made on cygwin), #2674 + - redo ItemCache, on top of object cache + - use decrypted cache + - remove unnecessary normpaths - serve: ignore --append-only when initializing a repository (borg init), #2501 +- serve: fix incorrect type of exception_short for Errors, #2513 - fix --exclude and --exclude-from recursing into directories, #2469 - init: don't allow creating nested repositories, #2563 - --json: fix encryption[mode] not being the cmdline name - remote: propagate Error.traceback correctly -- serve: fix incorrect type of exception_short for Errors, #2513 - fix remote logging and progress, #2241 - implement --debug-topic for remote servers - remote: restore "Remote:" prefix (as used in 1.0.x) - rpc negotiate: enable v3 log protocol only for supported clients - fix --progress and logging in general for remote +- fix parse_version, add tests, #2556 +- repository: truncate segments (and also some other files) before unlinking, #2557 +- recreate: keep timestamps as in original archive, #2384 +- recreate: if single archive is not processed, exit 2 +- patterns: don't recurse with ! / --exclude for pf:, #2509 +- cache sync: fix n^2 behaviour in lookup_name +- extract: don't write to disk with --stdout (affected non-regular-file items), #2645 +- hashindex: implement KeyError, more tests Other changes: @@ -205,31 +238,66 @@ Other changes: - support common options on mid-level commands (e.g. borg *key* export) - make --progress a common option - increase DEFAULT_SEGMENTS_PER_DIR to 1000 +- chunker: fix invalid use of types (function only used by tests) +- chunker: don't do uint32_t >> 32 +- fuse: + - add instrumentation (--debug and SIGUSR1/SIGINFO) + - reduced memory usage for repository mounts by lazily instantiating archives + - improved archive load times +- info: use CacheSynchronizer & HashIndex.stats_against (better performance) - docs: - init: document --encryption as required - security: OpenSSL usage - security: used implementations; note python libraries - security: security track record of OpenSSL and msgpack - - quotas: local repo disclaimer - - quotas: clarify compatbility; only relevant to serve side + - patterns: document denial of service (regex, wildcards) + - init: note possible denial of service with "none" mode + - init: document SHA extension is supported in OpenSSL and thus SHA is + faster on AMD Ryzen than blake2b. - book: use A4 format, new builder option format. - book: create appendices - data structures: explain repository compaction - data structures: add chunk layout diagram - data structures: integrity checking + - data structures: demingle cache and repo index - Attic FAQ: separate section for attic stuff - FAQ: I get an IntegrityError or similar - what now? + - FAQ: Can I use Borg on SMR hard drives?, #2252 + - FAQ: specify "using inline shell scripts" - add systemd warning regarding placeholders, #2543 - xattr: document API - add docs/misc/borg-data-flow data flow chart - debugging facilities - README: how to help the project, #2550 - README: add bountysource badge, #2558 + - fresh new theme + tweaking - logo: vectorized (PDF and SVG) versions - frontends: use headlines - you can link to them - - sphinx: disable smartypants, avoids mangled Unicode options like "—exclude" + - mark --pattern, --patterns-from as experimental + - highlight experimental features in online docs + - remove regex based pattern examples, #2458 + - nanorst for "borg help TOPIC" and --help + - split deployment + - deployment: hosting repositories + - deployment: automated backups to a local hard drive + - development: vagrant, windows10 requirements + - development: update docs remarks + - split usage docs, #2627 + - usage: avoid bash highlight, [options] instead of + - usage: add benchmark page + - helpers: truncate_and_unlink doc + - don't suggest to leak BORG_PASSPHRASE + - internals: columnize rather long ToC [webkit fixup] + internals: manifest & feature flags + - internals: more HashIndex details + - internals: fix ASCII art equations + - internals: edited obj graph related sections a bit + - internals: layers image + description + - fix way too small figures in pdf + - index: disable syntax highlight (bash) + - improve options formatting, fix accidental block quotes - testing / checking: @@ -240,12 +308,18 @@ Other changes: - testsuite.archiver: normalise pytest.raises vs. assert_raises - add test for preserved intermediate folder permissions, #2477 - key: add round-trip test + - remove attic dependency of the tests, #2505 + - enable remote tests on cygwin + - tests: suppress tar's future timestamp warning + - cache sync: add more refcount tests + - repository: add tests, including corruption tests - vagrant: - control VM cpus and pytest workers via env vars VMCPUS and XDISTN - update cleaning workdir - fix openbsd shell + - add OpenIndiana - packaging: From c7dda0aca9c554fd6c55418186ceeca22aa375ba Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 12:12:50 +0200 Subject: [PATCH 1098/1387] docs: assorted formatting fixes --- docs/usage/create.rst.inc | 2 ++ docs/usage/help.rst.inc | 62 ++++++++++++------------------------- src/borg/archiver.py | 64 +++++++++++++-------------------------- 3 files changed, 43 insertions(+), 85 deletions(-) diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index c97d1448..2a76c0dd 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -25,6 +25,8 @@ optional arguments | only display items with the given status characters ``--json`` | output stats as JSON (implies --stats) + ``--no-cache-sync`` + | experimental: do not synchronize the cache. Implies --no-files-cache. :ref:`common_options` | diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index 8d158ae7..87fbed52 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -8,7 +8,7 @@ borg help patterns File patterns support these styles: fnmatch, shell, regular expressions, path prefixes and path full-matches. By default, fnmatch is used for -`--exclude` patterns and shell-style is used for the experimental `--pattern` +``--exclude`` patterns and shell-style is used for the experimental ``--pattern`` option. If followed by a colon (':') the first two characters of a pattern are used as a @@ -17,8 +17,7 @@ non-default style is desired or when the desired pattern starts with two alphanumeric characters followed by a colon (i.e. `aa:something/*`). `Fnmatch `_, selector `fm:` - - This is the default style for --exclude and --exclude-from. + This is the default style for ``--exclude`` and ``--exclude-from``. These patterns use a variant of shell pattern syntax, with '\*' matching any number of characters, '?' matching any single character, '[...]' matching any single character specified, including ranges, and '[!...]' @@ -33,7 +32,6 @@ two alphanumeric characters followed by a colon (i.e. `aa:something/*`). separator, a '\*' is appended before matching is attempted. Shell-style patterns, selector `sh:` - This is the default style for --pattern and --patterns-from. Like fnmatch patterns these are similar to shell patterns. The difference is that the pattern may include `**/` for matching zero or more directory @@ -41,7 +39,6 @@ Shell-style patterns, selector `sh:` exception of any path separator. Regular expressions, selector `re:` - Regular expressions similar to those found in Perl are supported. Unlike shell patterns regular expressions are not required to match the complete path and any substring match is sufficient. It is strongly recommended to @@ -52,12 +49,10 @@ Regular expressions, selector `re:` the re module `_. Path prefix, selector `pp:` - This pattern style is useful to match whole sub-directories. The pattern `pp:/data/bar` matches `/data/bar` and everything therein. Path full-match, selector `pf:` - This pattern style is useful to match whole paths. This is kind of a pseudo pattern as it can not have any variable or unspecified parts - the full, precise path must be given. @@ -81,11 +76,11 @@ Path full-match, selector `pf:` Further, ensure that `sh:` and `fm:` patterns only contain a handful of wildcards at most. -Exclusions can be passed via the command line option `--exclude`. When used +Exclusions can be passed via the command line option ``--exclude``. When used from within a shell the patterns should be quoted to protect them from expansion. -The `--exclude-from` option permits loading exclusion patterns from a text +The ``--exclude-from`` option permits loading exclusion patterns from a text file with one pattern per line. Lines empty or starting with the number sign ('#') after removing whitespace on both ends are ignored. The optional style selector prefix is also supported for patterns loaded from a file. Due to @@ -125,7 +120,7 @@ Examples:: .. container:: experimental A more general and easier to use way to define filename matching patterns exists - with the experimental `--pattern` and `--patterns-from` options. Using these, you + with the experimental ``--pattern`` and ``--patterns-from`` options. Using these, you may specify the backup roots (starting points) and patterns for inclusion/exclusion. A root path starts with the prefix `R`, followed by a path (a plain path, not a file pattern). An include rule starts with the prefix +, an exclude rule starts @@ -134,15 +129,15 @@ Examples:: path. The first matching pattern is used so if an include pattern matches before an exclude pattern, the file is backed up. - Note that the default pattern style for `--pattern` and `--patterns-from` is + Note that the default pattern style for ``--pattern`` and ``--patterns-from`` is shell style (`sh:`), so those patterns behave similar to rsync include/exclude patterns. The pattern style can be set via the `P` prefix. - Patterns (`--pattern`) and excludes (`--exclude`) from the command line are - considered first (in the order of appearance). Then patterns from `--patterns-from` - are added. Exclusion patterns from `--exclude-from` files are appended last. + Patterns (``--pattern``) and excludes (``--exclude``) from the command line are + considered first (in the order of appearance). Then patterns from ``--patterns-from`` + are added. Exclusion patterns from ``--exclude-from`` files are appended last. - An example `--patterns-from` file could look like that:: + An example ``--patterns-from`` file could look like that:: # "sh:" pattern style is the default, so the following line is not needed: P sh @@ -163,49 +158,39 @@ borg help placeholders ~~~~~~~~~~~~~~~~~~~~~~ -Repository (or Archive) URLs, --prefix and --remote-path values support these +Repository (or Archive) URLs, ``--prefix`` and ``--remote-path`` values support these placeholders: {hostname} - The (short) hostname of the machine. {fqdn} - The full name of the machine. {now} - The current local date and time, by default in ISO-8601 format. You can also supply your own `format string `_, e.g. {now:%Y-%m-%d_%H:%M:%S} {utcnow} - The current UTC date and time, by default in ISO-8601 format. You can also supply your own `format string `_, e.g. {utcnow:%Y-%m-%d_%H:%M:%S} {user} - The user name (or UID, if no name is available) of the user running borg. {pid} - The current process ID. {borgversion} - The version of borg, e.g.: 1.0.8rc1 {borgmajor} - The version of borg, only the major version, e.g.: 1 {borgminor} - The version of borg, only major and minor version, e.g.: 1.0 {borgpatch} - The version of borg, only major, minor and patch version, e.g.: 1.0.8 If literal curly braces need to be used, double them for escaping:: @@ -234,20 +219,26 @@ borg help compression ~~~~~~~~~~~~~~~~~~~~~ +It is no problem to mix different compression methods in one repo, +deduplication is done on the source data chunks (not on the compressed +or encrypted data). + +If some specific chunk was once compressed and stored into the repo, creating +another backup that also uses this chunk will not change the stored chunk. +So if you use different compression specs for the backups, whichever stores a +chunk first determines its compression. See also borg recreate. + Compression is lz4 by default. If you want something else, you have to specify what you want. Valid compression specifiers are: none - Do not compress. lz4 - Use lz4 compression. High speed, low compression. (default) zlib[,L] - Use zlib ("gz") compression. Medium speed, medium compression. If you do not explicitely give the compression level L (ranging from 0 to 9), it will use level 6. @@ -255,7 +246,6 @@ zlib[,L] overhead) is usually pointless, you better use "none" compression. lzma[,L] - Use lzma ("xz") compression. Low speed, high compression. If you do not explicitely give the compression level L (ranging from 0 to 9), it will use level 6. @@ -264,7 +254,6 @@ lzma[,L] lots of CPU cycles and RAM. auto,C[,L] - Use a built-in heuristic to decide per chunk whether to compress or not. The heuristic tries with lz4 whether the data is compressible. For incompressible data, it will not use compression (uses "none"). @@ -279,14 +268,3 @@ Examples:: borg create --compression auto,lzma,6 REPO::ARCHIVE data borg create --compression auto,lzma ... -General remarks: - -It is no problem to mix different compression methods in one repo, -deduplication is done on the source data chunks (not on the compressed -or encrypted data). - -If some specific chunk was once compressed and stored into the repo, creating -another backup that also uses this chunk will not change the stored chunk. -So if you use different compression specs for the backups, whichever stores a -chunk first determines its compression. See also borg recreate. - diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 3275bf0b..346bb8a4 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1857,7 +1857,7 @@ class Archiver: helptext['patterns'] = textwrap.dedent(''' File patterns support these styles: fnmatch, shell, regular expressions, path prefixes and path full-matches. By default, fnmatch is used for - `--exclude` patterns and shell-style is used for the experimental `--pattern` + ``--exclude`` patterns and shell-style is used for the experimental ``--pattern`` option. If followed by a colon (':') the first two characters of a pattern are used as a @@ -1866,8 +1866,7 @@ class Archiver: two alphanumeric characters followed by a colon (i.e. `aa:something/*`). `Fnmatch `_, selector `fm:` - - This is the default style for --exclude and --exclude-from. + This is the default style for ``--exclude`` and ``--exclude-from``. These patterns use a variant of shell pattern syntax, with '\*' matching any number of characters, '?' matching any single character, '[...]' matching any single character specified, including ranges, and '[!...]' @@ -1882,7 +1881,6 @@ class Archiver: separator, a '\*' is appended before matching is attempted. Shell-style patterns, selector `sh:` - This is the default style for --pattern and --patterns-from. Like fnmatch patterns these are similar to shell patterns. The difference is that the pattern may include `**/` for matching zero or more directory @@ -1890,7 +1888,6 @@ class Archiver: exception of any path separator. Regular expressions, selector `re:` - Regular expressions similar to those found in Perl are supported. Unlike shell patterns regular expressions are not required to match the complete path and any substring match is sufficient. It is strongly recommended to @@ -1901,12 +1898,10 @@ class Archiver: the re module `_. Path prefix, selector `pp:` - This pattern style is useful to match whole sub-directories. The pattern `pp:/data/bar` matches `/data/bar` and everything therein. Path full-match, selector `pf:` - This pattern style is useful to match whole paths. This is kind of a pseudo pattern as it can not have any variable or unspecified parts - the full, precise path must be given. @@ -1930,11 +1925,11 @@ class Archiver: Further, ensure that `sh:` and `fm:` patterns only contain a handful of wildcards at most. - Exclusions can be passed via the command line option `--exclude`. When used + Exclusions can be passed via the command line option ``--exclude``. When used from within a shell the patterns should be quoted to protect them from expansion. - The `--exclude-from` option permits loading exclusion patterns from a text + The ``--exclude-from`` option permits loading exclusion patterns from a text file with one pattern per line. Lines empty or starting with the number sign ('#') after removing whitespace on both ends are ignored. The optional style selector prefix is also supported for patterns loaded from a file. Due to @@ -1974,7 +1969,7 @@ class Archiver: .. container:: experimental A more general and easier to use way to define filename matching patterns exists - with the experimental `--pattern` and `--patterns-from` options. Using these, you + with the experimental ``--pattern`` and ``--patterns-from`` options. Using these, you may specify the backup roots (starting points) and patterns for inclusion/exclusion. A root path starts with the prefix `R`, followed by a path (a plain path, not a file pattern). An include rule starts with the prefix +, an exclude rule starts @@ -1983,15 +1978,15 @@ class Archiver: path. The first matching pattern is used so if an include pattern matches before an exclude pattern, the file is backed up. - Note that the default pattern style for `--pattern` and `--patterns-from` is + Note that the default pattern style for ``--pattern`` and ``--patterns-from`` is shell style (`sh:`), so those patterns behave similar to rsync include/exclude patterns. The pattern style can be set via the `P` prefix. - Patterns (`--pattern`) and excludes (`--exclude`) from the command line are - considered first (in the order of appearance). Then patterns from `--patterns-from` - are added. Exclusion patterns from `--exclude-from` files are appended last. + Patterns (``--pattern``) and excludes (``--exclude``) from the command line are + considered first (in the order of appearance). Then patterns from ``--patterns-from`` + are added. Exclusion patterns from ``--exclude-from`` files are appended last. - An example `--patterns-from` file could look like that:: + An example ``--patterns-from`` file could look like that:: # "sh:" pattern style is the default, so the following line is not needed: P sh @@ -2006,49 +2001,39 @@ class Archiver: # don't backup the other home directories - /home/*\n\n''') helptext['placeholders'] = textwrap.dedent(''' - Repository (or Archive) URLs, --prefix and --remote-path values support these + Repository (or Archive) URLs, ``--prefix`` and ``--remote-path`` values support these placeholders: {hostname} - The (short) hostname of the machine. {fqdn} - The full name of the machine. {now} - The current local date and time, by default in ISO-8601 format. You can also supply your own `format string `_, e.g. {now:%Y-%m-%d_%H:%M:%S} {utcnow} - The current UTC date and time, by default in ISO-8601 format. You can also supply your own `format string `_, e.g. {utcnow:%Y-%m-%d_%H:%M:%S} {user} - The user name (or UID, if no name is available) of the user running borg. {pid} - The current process ID. {borgversion} - The version of borg, e.g.: 1.0.8rc1 {borgmajor} - The version of borg, only the major version, e.g.: 1 {borgminor} - The version of borg, only major and minor version, e.g.: 1.0 {borgpatch} - The version of borg, only major, minor and patch version, e.g.: 1.0.8 If literal curly braces need to be used, double them for escaping:: @@ -2071,20 +2056,26 @@ class Archiver: double all percent signs (``{hostname}-{now:%Y-%m-%d_%H:%M:%S}`` becomes ``{hostname}-{now:%%Y-%%m-%%d_%%H:%%M:%%S}``).\n\n''') helptext['compression'] = textwrap.dedent(''' + It is no problem to mix different compression methods in one repo, + deduplication is done on the source data chunks (not on the compressed + or encrypted data). + + If some specific chunk was once compressed and stored into the repo, creating + another backup that also uses this chunk will not change the stored chunk. + So if you use different compression specs for the backups, whichever stores a + chunk first determines its compression. See also borg recreate. + Compression is lz4 by default. If you want something else, you have to specify what you want. Valid compression specifiers are: none - Do not compress. lz4 - Use lz4 compression. High speed, low compression. (default) zlib[,L] - Use zlib ("gz") compression. Medium speed, medium compression. If you do not explicitely give the compression level L (ranging from 0 to 9), it will use level 6. @@ -2092,7 +2083,6 @@ class Archiver: overhead) is usually pointless, you better use "none" compression. lzma[,L] - Use lzma ("xz") compression. Low speed, high compression. If you do not explicitely give the compression level L (ranging from 0 to 9), it will use level 6. @@ -2101,7 +2091,6 @@ class Archiver: lots of CPU cycles and RAM. auto,C[,L] - Use a built-in heuristic to decide per chunk whether to compress or not. The heuristic tries with lz4 whether the data is compressible. For incompressible data, it will not use compression (uses "none"). @@ -2114,18 +2103,7 @@ class Archiver: borg create --compression zlib REPO::ARCHIVE data borg create --compression zlib,1 REPO::ARCHIVE data borg create --compression auto,lzma,6 REPO::ARCHIVE data - borg create --compression auto,lzma ... - - General remarks: - - It is no problem to mix different compression methods in one repo, - deduplication is done on the source data chunks (not on the compressed - or encrypted data). - - If some specific chunk was once compressed and stored into the repo, creating - another backup that also uses this chunk will not change the stored chunk. - So if you use different compression specs for the backups, whichever stores a - chunk first determines its compression. See also borg recreate.\n\n''') + borg create --compression auto,lzma ...\n\n''') def do_help(self, parser, commands, args): if not args.topic: From 1e2835eb1df7300e71d54b8e808051a0acfb6c55 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 12:13:28 +0200 Subject: [PATCH 1099/1387] docs: man pages, usage. --- docs/man/borg-benchmark-crud.1 | 2 +- docs/man/borg-benchmark.1 | 2 +- docs/man/borg-break-lock.1 | 2 +- docs/man/borg-change-passphrase.1 | 2 +- docs/man/borg-check.1 | 15 ++-- docs/man/borg-common.1 | 2 +- docs/man/borg-compression.1 | 60 ++++++---------- docs/man/borg-create.1 | 23 +++--- docs/man/borg-delete.1 | 7 +- docs/man/borg-diff.1 | 8 +-- docs/man/borg-export-tar.1 | 12 ++-- docs/man/borg-extract.1 | 6 +- docs/man/borg-info.1 | 7 +- docs/man/borg-init.1 | 31 ++++++--- docs/man/borg-key-change-passphrase.1 | 46 +++++++++++- docs/man/borg-key-export.1 | 2 +- docs/man/borg-key-import.1 | 4 +- docs/man/borg-key-migrate-to-repokey.1 | 2 +- docs/man/borg-key.1 | 2 +- docs/man/borg-list.1 | 29 +++----- docs/man/borg-mount.1 | 9 ++- docs/man/borg-patterns.1 | 79 ++++++++++----------- docs/man/borg-placeholders.1 | 96 ++++++++++---------------- docs/man/borg-prune.1 | 24 ++++--- docs/man/borg-recreate.1 | 26 +++---- docs/man/borg-rename.1 | 2 +- docs/man/borg-serve.1 | 8 ++- docs/man/borg-umount.1 | 4 +- docs/man/borg-upgrade.1 | 4 +- docs/man/borg-with-lock.1 | 2 +- docs/man/borg.1 | 75 ++++++++++++++------ 31 files changed, 321 insertions(+), 272 deletions(-) diff --git a/docs/man/borg-benchmark-crud.1 b/docs/man/borg-benchmark-crud.1 index ed1a5e1e..c763aae3 100644 --- a/docs/man/borg-benchmark-crud.1 +++ b/docs/man/borg-benchmark-crud.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-BENCHMARK-CRUD 1 "2017-05-17" "" "borg backup tool" +.TH BORG-BENCHMARK-CRUD 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-benchmark-crud \- Benchmark Create, Read, Update, Delete for archives. . diff --git a/docs/man/borg-benchmark.1 b/docs/man/borg-benchmark.1 index 0f46eb8a..79e356ac 100644 --- a/docs/man/borg-benchmark.1 +++ b/docs/man/borg-benchmark.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-BENCHMARK 1 "2017-05-17" "" "borg backup tool" +.TH BORG-BENCHMARK 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-benchmark \- benchmark command . diff --git a/docs/man/borg-break-lock.1 b/docs/man/borg-break-lock.1 index a7275b0c..7b4291cd 100644 --- a/docs/man/borg-break-lock.1 +++ b/docs/man/borg-break-lock.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-BREAK-LOCK 1 "2017-05-17" "" "borg backup tool" +.TH BORG-BREAK-LOCK 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-break-lock \- Break the repository lock (e.g. in case it was left by a dead borg. . diff --git a/docs/man/borg-change-passphrase.1 b/docs/man/borg-change-passphrase.1 index a4649a71..d5b3edbf 100644 --- a/docs/man/borg-change-passphrase.1 +++ b/docs/man/borg-change-passphrase.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CHANGE-PASSPHRASE 1 "2017-05-17" "" "borg backup tool" +.TH BORG-CHANGE-PASSPHRASE 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-change-passphrase \- Change repository key file passphrase . diff --git a/docs/man/borg-check.1 b/docs/man/borg-check.1 index cb694cac..cf2996a2 100644 --- a/docs/man/borg-check.1 +++ b/docs/man/borg-check.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CHECK 1 "2017-05-17" "" "borg backup tool" +.TH BORG-CHECK 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-check \- Check repository consistency . @@ -55,7 +55,7 @@ stored in the segments. If you use a remote repo server via ssh:, the repo check is executed on the repo server without causing significant network traffic. .IP \(bu 2 -The repository check can be skipped using the \-\-archives\-only option. +The repository check can be skipped using the \fB\-\-archives\-only\fP option. .UNINDENT .sp Second, the consistency and correctness of the archive metadata is verified: @@ -84,10 +84,10 @@ decryption and this is always done client\-side, because key access will be required). .IP \(bu 2 The archive checks can be time consuming, they can be skipped using the -\-\-repository\-only option. +\fB\-\-repository\-only\fP option. .UNINDENT .sp -The \-\-verify\-data option will perform a full integrity verification (as opposed to +The \fB\-\-verify\-data\fP option will perform a full integrity verification (as opposed to checking the CRC32 of the segment) of data, which means reading the data from the repository, decrypting and decompressing it. This is a cryptographic verification, which will detect (accidental) corruption. For encrypted repositories it is @@ -113,7 +113,7 @@ only perform repository checks only perform archives checks .TP .B \-\-verify\-data -perform cryptographic archive data integrity verification (conflicts with \-\-repository\-only) +perform cryptographic archive data integrity verification (conflicts with \fB\-\-repository\-only\fP) .TP .B \-\-repair attempt to repair any inconsistencies found @@ -125,7 +125,10 @@ work slower, but using less space .INDENT 0.0 .TP .B \-P\fP,\fB \-\-prefix -only consider archive names starting with this prefix +only consider archive names starting with this prefix. +.TP +.B \-a\fP,\fB \-\-glob\-archives +only consider archive names matching the glob. sh: rules apply, see "borg help patterns". \fB\-\-prefix\fP and \fB\-\-glob\-archives\fP are mutually exclusive. .TP .B \-\-sort\-by Comma\-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp diff --git a/docs/man/borg-common.1 b/docs/man/borg-common.1 index 223fd33a..f48ccb6c 100644 --- a/docs/man/borg-common.1 +++ b/docs/man/borg-common.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-COMMON 1 "2017-05-17" "" "borg backup tool" +.TH BORG-COMMON 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-common \- Common options of Borg commands . diff --git a/docs/man/borg-compression.1 b/docs/man/borg-compression.1 index 9e176f22..3347a658 100644 --- a/docs/man/borg-compression.1 +++ b/docs/man/borg-compression.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-COMPRESSION 1 "2017-05-17" "" "borg backup tool" +.TH BORG-COMPRESSION 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-compression \- Details regarding compression . @@ -32,57 +32,48 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH DESCRIPTION .sp +It is no problem to mix different compression methods in one repo, +deduplication is done on the source data chunks (not on the compressed +or encrypted data). +.sp +If some specific chunk was once compressed and stored into the repo, creating +another backup that also uses this chunk will not change the stored chunk. +So if you use different compression specs for the backups, whichever stores a +chunk first determines its compression. See also borg recreate. +.sp Compression is lz4 by default. If you want something else, you have to specify what you want. .sp Valid compression specifiers are: -.sp -none .INDENT 0.0 -.INDENT 3.5 +.TP +.B none Do not compress. -.UNINDENT -.UNINDENT -.sp -lz4 -.INDENT 0.0 -.INDENT 3.5 +.TP +.B lz4 Use lz4 compression. High speed, low compression. (default) -.UNINDENT -.UNINDENT -.sp -zlib[,L] -.INDENT 0.0 -.INDENT 3.5 +.TP +.B zlib[,L] Use zlib ("gz") compression. Medium speed, medium compression. If you do not explicitely give the compression level L (ranging from 0 to 9), it will use level 6. Giving level 0 (means "no compression", but still has zlib protocol overhead) is usually pointless, you better use "none" compression. -.UNINDENT -.UNINDENT -.sp -lzma[,L] -.INDENT 0.0 -.INDENT 3.5 +.TP +.B lzma[,L] Use lzma ("xz") compression. Low speed, high compression. If you do not explicitely give the compression level L (ranging from 0 to 9), it will use level 6. Giving levels above 6 is pointless and counterproductive because it does not compress better due to the buffer size used by borg \- but it wastes lots of CPU cycles and RAM. -.UNINDENT -.UNINDENT -.sp -auto,C[,L] -.INDENT 0.0 -.INDENT 3.5 +.TP +.B auto,C[,L] Use a built\-in heuristic to decide per chunk whether to compress or not. The heuristic tries with lz4 whether the data is compressible. For incompressible data, it will not use compression (uses "none"). For compressible data, it uses the given C[,L] compression \- with C[,L] being any valid compression specifier. .UNINDENT -.UNINDENT .sp Examples: .INDENT 0.0 @@ -99,17 +90,6 @@ borg create \-\-compression auto,lzma ... .fi .UNINDENT .UNINDENT -.sp -General remarks: -.sp -It is no problem to mix different compression methods in one repo, -deduplication is done on the source data chunks (not on the compressed -or encrypted data). -.sp -If some specific chunk was once compressed and stored into the repo, creating -another backup that also uses this chunk will not change the stored chunk. -So if you use different compression specs for the backups, whichever stores a -chunk first determines its compression. See also borg recreate. .SH AUTHOR The Borg Collective .\" Generated by docutils manpage writer. diff --git a/docs/man/borg-create.1 b/docs/man/borg-create.1 index 0cae7ca0..ec8d7b52 100644 --- a/docs/man/borg-create.1 +++ b/docs/man/borg-create.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CREATE 1 "2017-05-17" "" "borg backup tool" +.TH BORG-CREATE 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-create \- Create new archive . @@ -54,7 +54,7 @@ In the archive name, you may use the following placeholders: {now}, {utcnow}, {fqdn}, {hostname}, {user} and some others. .sp To speed up pulling backups over sshfs and similar network file systems which do -not provide correct inode information the \-\-ignore\-inode flag can be used. This +not provide correct inode information the \fB\-\-ignore\-inode\fP flag can be used. This potentially decreases reliability of change detection, while avoiding always reading all files on these file systems. .sp @@ -63,7 +63,7 @@ creation of a new archive to ensure fast operation. This is because the file cac is used to determine changed files quickly uses absolute filenames. If this is not possible, consider creating a bind mount to a stable location. .sp -The \-\-progress option shows (from left to right) Original, Compressed and Deduplicated +The \fB\-\-progress\fP option shows (from left to right) Original, Compressed and Deduplicated (O, C and D, respectively), then the Number of files (N) processed so far, followed by the currently processed path. .sp @@ -98,6 +98,9 @@ only display items with the given status characters .TP .B \-\-json output stats as JSON (implies \-\-stats) +.TP +.B \-\-no\-cache\-sync +experimental: do not synchronize the cache. Implies \-\-no\-files\-cache. .UNINDENT .SS Exclusion options .INDENT 0.0 @@ -118,10 +121,10 @@ exclude directories that are tagged by containing a filesystem object with the g if tag objects are specified with \-\-exclude\-if\-present, don\(aqt omit the tag objects themselves from the backup archive .TP .BI \-\-pattern \ PATTERN -include/exclude paths matching PATTERN +experimental: include/exclude paths matching PATTERN .TP .BI \-\-patterns\-from \ PATTERNFILE -read include/exclude patterns from PATTERNFILE, one per line +experimental: read include/exclude patterns from PATTERNFILE, one per line .UNINDENT .SS Filesystem options .INDENT 0.0 @@ -181,11 +184,7 @@ $ borg create /path/to/repo::my\-files \e \-\-exclude \(aq*.pyc\(aq # Backup home directories excluding image thumbnails (i.e. only -# /home/*/.thumbnails is excluded, not /home/*/*/.thumbnails) -$ borg create /path/to/repo::my\-files /home \e - \-\-exclude \(aqre:^/home/[^/]+/\e.thumbnails/\(aq - -# Do the same using a shell\-style pattern +# /home//.thumbnails is excluded, not /home/*/*/.thumbnails etc.) $ borg create /path/to/repo::my\-files /home \e \-\-exclude \(aqsh:/home/*/.thumbnails\(aq @@ -238,8 +237,8 @@ $ borg create /path/to/repo::daily\-projectA\-{now:%Y\-%m\-%d} projectA .UNINDENT .SH NOTES .sp -The \-\-exclude patterns are not like tar. In tar \-\-exclude .bundler/gems will -exclude foo/.bundler/gems. In borg it will not, you need to use \-\-exclude +The \fB\-\-exclude\fP patterns are not like tar. In tar \fB\-\-exclude\fP .bundler/gems will +exclude foo/.bundler/gems. In borg it will not, you need to use \fB\-\-exclude\fP \(aq*/.bundler/gems\(aq to get the same effect. See \fBborg help patterns\fP for more information. .sp diff --git a/docs/man/borg-delete.1 b/docs/man/borg-delete.1 index c7c96aa1..2e889153 100644 --- a/docs/man/borg-delete.1 +++ b/docs/man/borg-delete.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-DELETE 1 "2017-05-17" "" "borg backup tool" +.TH BORG-DELETE 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-delete \- Delete an existing repository or archives . @@ -66,7 +66,10 @@ work slower, but using less space .INDENT 0.0 .TP .B \-P\fP,\fB \-\-prefix -only consider archive names starting with this prefix +only consider archive names starting with this prefix. +.TP +.B \-a\fP,\fB \-\-glob\-archives +only consider archive names matching the glob. sh: rules apply, see "borg help patterns". \fB\-\-prefix\fP and \fB\-\-glob\-archives\fP are mutually exclusive. .TP .B \-\-sort\-by Comma\-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp diff --git a/docs/man/borg-diff.1 b/docs/man/borg-diff.1 index b69a792b..ad030a67 100644 --- a/docs/man/borg-diff.1 +++ b/docs/man/borg-diff.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-DIFF 1 "2017-05-17" "" "borg backup tool" +.TH BORG-DIFF 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-diff \- Diff contents of two archives . @@ -47,7 +47,7 @@ are compared, which is very fast. .sp For archives prior to Borg 1.1 chunk contents are compared by default. If you did not create the archives with different chunker params, -pass \-\-same\-chunker\-params. +pass \fB\-\-same\-chunker\-params\fP\&. Note that the chunker params changed from Borg 0.xx to 1.0. .sp See the output of the "borg help patterns" command for more help on exclude patterns. @@ -97,10 +97,10 @@ exclude directories that are tagged by containing a filesystem object with the g if tag objects are specified with \-\-exclude\-if\-present, don\(aqt omit the tag objects themselves from the backup archive .TP .BI \-\-pattern \ PATTERN -include/exclude paths matching PATTERN +experimental: include/exclude paths matching PATTERN .TP .BI \-\-patterns\-from \ PATTERNFILE -read include/exclude patterns from PATTERNFILE, one per line +experimental: read include/exclude patterns from PATTERNFILE, one per line .UNINDENT .SH EXAMPLES .INDENT 0.0 diff --git a/docs/man/borg-export-tar.1 b/docs/man/borg-export-tar.1 index 73cd9815..515b4d84 100644 --- a/docs/man/borg-export-tar.1 +++ b/docs/man/borg-export-tar.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-EXPORT-TAR 1 "2017-05-17" "" "borg backup tool" +.TH BORG-EXPORT-TAR 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-export-tar \- Export archive contents as a tarball . @@ -39,7 +39,7 @@ This command creates a tarball from an archive. .sp When giving \(aq\-\(aq as the output FILE, Borg will write a tar stream to standard output. .sp -By default (\-\-tar\-filter=auto) Borg will detect whether the FILE should be compressed +By default (\fB\-\-tar\-filter=auto\fP) Borg will detect whether the FILE should be compressed based on its file extension and pipe the tarball through an appropriate filter before writing it to FILE: .INDENT 0.0 @@ -51,7 +51,7 @@ before writing it to FILE: \&.tar.xz: xz .UNINDENT .sp -Alternatively a \-\-tar\-filter program may be explicitly specified. It should +Alternatively a \fB\-\-tar\-filter\fP program may be explicitly specified. It should read the uncompressed tar stream from stdin and write a compressed/filtered tar stream to stdout. .sp @@ -62,7 +62,7 @@ BSD flags, ACLs, extended attributes (xattrs), atime and ctime are not exported. Timestamp resolution is limited to whole seconds, not the nanosecond resolution otherwise supported by Borg. .sp -A \-\-sparse option (as found in borg extract) is not supported. +A \fB\-\-sparse\fP option (as found in borg extract) is not supported. .sp By default the entire archive is extracted but a subset of files and directories can be selected by passing a list of \fBPATHs\fP as arguments. @@ -103,10 +103,10 @@ exclude paths matching PATTERN read exclude patterns from EXCLUDEFILE, one per line .TP .BI \-\-pattern \ PATTERN -include/exclude paths matching PATTERN +experimental: include/exclude paths matching PATTERN .TP .BI \-\-patterns\-from \ PATTERNFILE -read include/exclude patterns from PATTERNFILE, one per line +experimental: read include/exclude patterns from PATTERNFILE, one per line .TP .BI \-\-strip\-components \ NUMBER Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. diff --git a/docs/man/borg-extract.1 b/docs/man/borg-extract.1 index dbe9c353..13a71ab7 100644 --- a/docs/man/borg-extract.1 +++ b/docs/man/borg-extract.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-EXTRACT 1 "2017-05-17" "" "borg backup tool" +.TH BORG-EXTRACT 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-extract \- Extract archive contents . @@ -76,10 +76,10 @@ exclude paths matching PATTERN read exclude patterns from EXCLUDEFILE, one per line .TP .BI \-\-pattern \ PATTERN -include/exclude paths matching PATTERN +experimental: include/exclude paths matching PATTERN .TP .BI \-\-patterns\-from \ PATTERNFILE -read include/exclude patterns from PATTERNFILE, one per line +experimental: read include/exclude patterns from PATTERNFILE, one per line .TP .B \-\-numeric\-owner only obey numeric user and group identifiers diff --git a/docs/man/borg-info.1 b/docs/man/borg-info.1 index f235a866..338d3ca6 100644 --- a/docs/man/borg-info.1 +++ b/docs/man/borg-info.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-INFO 1 "2017-05-17" "" "borg backup tool" +.TH BORG-INFO 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-info \- Show archive details such as disk space used . @@ -67,7 +67,10 @@ format output as JSON .INDENT 0.0 .TP .B \-P\fP,\fB \-\-prefix -only consider archive names starting with this prefix +only consider archive names starting with this prefix. +.TP +.B \-a\fP,\fB \-\-glob\-archives +only consider archive names matching the glob. sh: rules apply, see "borg help patterns". \fB\-\-prefix\fP and \fB\-\-glob\-archives\fP are mutually exclusive. .TP .B \-\-sort\-by Comma\-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp diff --git a/docs/man/borg-init.1 b/docs/man/borg-init.1 index e25b9ca1..9576afa8 100644 --- a/docs/man/borg-init.1 +++ b/docs/man/borg-init.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-INIT 1 "2017-06-11" "" "borg backup tool" +.TH BORG-INIT 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-init \- Initialize an empty repository . @@ -81,6 +81,8 @@ a different keyboard layout. You can change your passphrase for existing repos at any time, it won\(aqt affect the encryption/decryption key or other secrets. .SS Encryption modes +.\" nanorst: inline-fill +. .TS center; |l|l|l|l|. @@ -103,9 +105,10 @@ SHA\-256 T} T{ none T} T{ -authenticated +\fIauthenticated\fP T} T{ -repokey, keyfile +repokey +keyfile T} _ T{ @@ -113,17 +116,22 @@ BLAKE2b T} T{ n/a T} T{ -authenticated\-blake2 +\fIauthenticated\-blake2\fP T} T{ -repokey\-blake2, -keyfile\-blake2 +\fIrepokey\-blake2\fP +\fIkeyfile\-blake2\fP T} _ .TE +.\" nanorst: inline-replace +. +.sp +\fIMarked modes\fP are new in Borg 1.1 and are not backwards\-compatible with Borg 1.0.x. .sp On modern Intel/AMD CPUs (except very cheap ones), AES is usually hardware\-accelerated. -BLAKE2b is faster than SHA256 on Intel/AMD 64\-bit CPUs, +BLAKE2b is faster than SHA256 on Intel/AMD 64\-bit CPUs +(except AMD Ryzen and future CPUs with SHA extensions), which makes \fIauthenticated\-blake2\fP faster than \fInone\fP and \fIauthenticated\fP\&. .sp On modern ARM CPUs, NEON provides hardware acceleration for SHA256 making it faster @@ -134,7 +142,7 @@ Hardware acceleration is always used automatically when available. \fIrepokey\fP and \fIkeyfile\fP use AES\-CTR\-256 for encryption and HMAC\-SHA256 for authentication in an encrypt\-then\-MAC (EtM) construction. The chunk ID hash is HMAC\-SHA256 as well (with a separate key). -These modes are compatible with borg 1.0.x. +These modes are compatible with Borg 1.0.x. .sp \fIrepokey\-blake2\fP and \fIkeyfile\-blake2\fP are also authenticated encryption modes, but use BLAKE2b\-256 instead of HMAC\-SHA256 for authentication. The chunk ID @@ -144,7 +152,7 @@ These modes are new and \fInot\fP compatible with Borg 1.0.x. \fIauthenticated\fP mode uses no encryption, but authenticates repository contents through the same HMAC\-SHA256 hash as the \fIrepokey\fP and \fIkeyfile\fP modes (it uses it as the chunk ID hash). The key is stored like \fIrepokey\fP\&. -This mode is new and \fInot\fP compatible with borg 1.0.x. +This mode is new and \fInot\fP compatible with Borg 1.0.x. .sp \fIauthenticated\-blake2\fP is like \fIauthenticated\fP, but uses the keyed BLAKE2b\-256 hash from the other blake2 modes. @@ -152,7 +160,8 @@ This mode is new and \fInot\fP compatible with Borg 1.0.x. .sp \fInone\fP mode uses no encryption and no authentication. It uses SHA256 as chunk ID hash. Not recommended, rather consider using an authenticated or -authenticated/encrypted mode. +authenticated/encrypted mode. This mode has possible denial\-of\-service issues +when running \fBborg create\fP on contents controlled by an attacker. Use it only for new repositories where no encryption is wanted \fBand\fP when compatibility with 1.0.x is important. If compatibility with 1.0.x is not important, use \fIauthenticated\-blake2\fP or \fIauthenticated\fP instead. @@ -172,7 +181,7 @@ repository to create .B \-e\fP,\fB \-\-encryption select encryption key mode \fB(required)\fP .TP -.B \-a\fP,\fB \-\-append\-only +.B \-\-append\-only create an append\-only mode repository .TP .B \-\-storage\-quota diff --git a/docs/man/borg-key-change-passphrase.1 b/docs/man/borg-key-change-passphrase.1 index 6457d3a7..88d1b2c1 100644 --- a/docs/man/borg-key-change-passphrase.1 +++ b/docs/man/borg-key-change-passphrase.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-CHANGE-PASSPHRASE 1 "2017-05-17" "" "borg backup tool" +.TH BORG-KEY-CHANGE-PASSPHRASE 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-key-change-passphrase \- Change repository key file passphrase . @@ -43,6 +43,50 @@ See \fIborg\-common(1)\fP for common options of Borg commands. .SS arguments .sp REPOSITORY +.SH EXAMPLES +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# Create a key file protected repository +$ borg init \-\-encryption=keyfile \-v /path/to/repo +Initializing repository at "/path/to/repo" +Enter new passphrase: +Enter same passphrase again: +Remember your passphrase. Your data will be inaccessible without it. +Key in "/root/.config/borg/keys/mnt_backup" created. +Keep this key safe. Your data will be inaccessible without it. +Synchronizing chunks cache... +Archives: 0, w/ cached Idx: 0, w/ outdated Idx: 0, w/o cached Idx: 0. +Done. + +# Change key file passphrase +$ borg key change\-passphrase \-v /path/to/repo +Enter passphrase for key /root/.config/borg/keys/mnt_backup: +Enter new passphrase: +Enter same passphrase again: +Remember your passphrase. Your data will be inaccessible without it. +Key updated +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Fully automated using environment variables: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ BORG_NEW_PASSPHRASE=old borg init \-e=repokey repo +# now "old" is the current passphrase. +$ BORG_PASSPHRASE=old BORG_NEW_PASSPHRASE=new borg key change\-passphrase repo +# now "new" is the current passphrase. +.ft P +.fi +.UNINDENT +.UNINDENT .SH SEE ALSO .sp \fIborg\-common(1)\fP diff --git a/docs/man/borg-key-export.1 b/docs/man/borg-key-export.1 index 9736bba3..23688683 100644 --- a/docs/man/borg-key-export.1 +++ b/docs/man/borg-key-export.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-EXPORT 1 "2017-05-17" "" "borg backup tool" +.TH BORG-KEY-EXPORT 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-key-export \- Export the repository key for backup . diff --git a/docs/man/borg-key-import.1 b/docs/man/borg-key-import.1 index 2ab5af4c..92a1754d 100644 --- a/docs/man/borg-key-import.1 +++ b/docs/man/borg-key-import.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-IMPORT 1 "2017-05-17" "" "borg backup tool" +.TH BORG-KEY-IMPORT 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-key-import \- Import the repository key from backup . @@ -56,7 +56,7 @@ path to the backup .INDENT 0.0 .TP .B \-\-paper -interactively import from a backup done with \-\-paper +interactively import from a backup done with \fB\-\-paper\fP .UNINDENT .SH SEE ALSO .sp diff --git a/docs/man/borg-key-migrate-to-repokey.1 b/docs/man/borg-key-migrate-to-repokey.1 index 17842979..0d408612 100644 --- a/docs/man/borg-key-migrate-to-repokey.1 +++ b/docs/man/borg-key-migrate-to-repokey.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-MIGRATE-TO-REPOKEY 1 "2017-05-17" "" "borg backup tool" +.TH BORG-KEY-MIGRATE-TO-REPOKEY 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-key-migrate-to-repokey \- Migrate passphrase -> repokey . diff --git a/docs/man/borg-key.1 b/docs/man/borg-key.1 index e61f8e30..0915aa5f 100644 --- a/docs/man/borg-key.1 +++ b/docs/man/borg-key.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY 1 "2017-05-17" "" "borg backup tool" +.TH BORG-KEY 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-key \- Manage a keyfile or repokey of a repository . diff --git a/docs/man/borg-list.1 b/docs/man/borg-list.1 index 66bcf1c1..3bceff41 100644 --- a/docs/man/borg-list.1 +++ b/docs/man/borg-list.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-LIST 1 "2017-05-17" "" "borg backup tool" +.TH BORG-LIST 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-list \- List archive or repository contents . @@ -61,16 +61,19 @@ specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") .TP .B \-\-json -Only valid for listing repository contents. Format output as JSON. The form of \-\-format is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "barchive" key is therefore not available. +Only valid for listing repository contents. Format output as JSON. The form of \fB\-\-format\fP is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "barchive" key is therefore not available. .TP .B \-\-json\-lines -Only valid for listing archive contents. Format output as JSON Lines. The form of \-\-format is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. +Only valid for listing archive contents. Format output as JSON Lines. The form of \fB\-\-format\fP is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. .UNINDENT .SS filters .INDENT 0.0 .TP .B \-P\fP,\fB \-\-prefix -only consider archive names starting with this prefix +only consider archive names starting with this prefix. +.TP +.B \-a\fP,\fB \-\-glob\-archives +only consider archive names matching the glob. sh: rules apply, see "borg help patterns". \fB\-\-prefix\fP and \fB\-\-glob\-archives\fP are mutually exclusive. .TP .B \-\-sort\-by Comma\-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp @@ -100,10 +103,10 @@ exclude directories that are tagged by containing a filesystem object with the g if tag objects are specified with \-\-exclude\-if\-present, don\(aqt omit the tag objects themselves from the backup archive .TP .BI \-\-pattern \ PATTERN -include/exclude paths matching PATTERN +experimental: include/exclude paths matching PATTERN .TP .BI \-\-patterns\-from \ PATTERNFILE -read include/exclude patterns from PATTERNFILE, one per line +experimental: read include/exclude patterns from PATTERNFILE, one per line .UNINDENT .SH EXAMPLES .INDENT 0.0 @@ -138,9 +141,7 @@ drwxrwxr\-x user user 0 Sun, 2015\-02\-01 11:00:00 code/myproject .UNINDENT .SH NOTES .sp -The following keys are available for \-\-format: -.INDENT 0.0 -.INDENT 3.5 +The following keys are available for \fB\-\-format\fP: .INDENT 0.0 .IP \(bu 2 NEWLINE: OS dependent line separator @@ -157,13 +158,9 @@ CR .IP \(bu 2 LF .UNINDENT -.UNINDENT -.UNINDENT .sp Keys for listing repository archives: .INDENT 0.0 -.INDENT 3.5 -.INDENT 0.0 .IP \(bu 2 archive, name: archive name interpreted as text (might be missing non\-text characters, see barchive) .IP \(bu 2 @@ -173,13 +170,9 @@ time: time of creation of the archive .IP \(bu 2 id: internal ID of the archive .UNINDENT -.UNINDENT -.UNINDENT .sp Keys for listing archive files: .INDENT 0.0 -.INDENT 3.5 -.INDENT 0.0 .IP \(bu 2 type .IP \(bu 2 @@ -247,8 +240,6 @@ extra: prepends {source} with " \-> " for soft links and " link to " for hard li .IP \(bu 2 health: either "healthy" (file ok) or "broken" (if file has all\-zero replacement chunks) .UNINDENT -.UNINDENT -.UNINDENT .SH SEE ALSO .sp \fIborg\-common(1)\fP, \fIborg\-info(1)\fP, \fIborg\-diff(1)\fP, \fIborg\-prune(1)\fP, \fIborg\-patterns(1)\fP diff --git a/docs/man/borg-mount.1 b/docs/man/borg-mount.1 index 298967af..d1892ac1 100644 --- a/docs/man/borg-mount.1 +++ b/docs/man/borg-mount.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-MOUNT 1 "2017-05-17" "" "borg backup tool" +.TH BORG-MOUNT 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-mount \- Mount archive or an entire repository as a FUSE filesystem . @@ -55,7 +55,7 @@ versions: when used with a repository mount, this gives a merged, versioned view of the files in the archives. EXPERIMENTAL, layout may change in future. .IP \(bu 2 allow_damaged_files: by default damaged files (where missing chunks were -replaced with runs of zeros by borg check \-\-repair) are not readable and +replaced with runs of zeros by borg check \fB\-\-repair\fP) are not readable and return EIO (I/O error). Set this option to read such files. .UNINDENT .sp @@ -95,7 +95,10 @@ Extra mount options .INDENT 0.0 .TP .B \-P\fP,\fB \-\-prefix -only consider archive names starting with this prefix +only consider archive names starting with this prefix. +.TP +.B \-a\fP,\fB \-\-glob\-archives +only consider archive names matching the glob. sh: rules apply, see "borg help patterns". \fB\-\-prefix\fP and \fB\-\-glob\-archives\fP are mutually exclusive. .TP .B \-\-sort\-by Comma\-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp diff --git a/docs/man/borg-patterns.1 b/docs/man/borg-patterns.1 index 9446b21f..239a441f 100644 --- a/docs/man/borg-patterns.1 +++ b/docs/man/borg-patterns.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PATTERNS 1 "2017-05-17" "" "borg backup tool" +.TH BORG-PATTERNS 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-patterns \- Details regarding patterns . @@ -34,16 +34,17 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .sp File patterns support these styles: fnmatch, shell, regular expressions, path prefixes and path full\-matches. By default, fnmatch is used for -\fI\-\-exclude\fP patterns and shell\-style is used for \fI\-\-pattern\fP\&. If followed -by a colon (\(aq:\(aq) the first two characters of a pattern are used as a +\fB\-\-exclude\fP patterns and shell\-style is used for the experimental \fB\-\-pattern\fP +option. +.sp +If followed by a colon (\(aq:\(aq) the first two characters of a pattern are used as a style selector. Explicit style selection is necessary when a non\-default style is desired or when the desired pattern starts with two alphanumeric characters followed by a colon (i.e. \fIaa:something/*\fP). -.sp -\fI\%Fnmatch\fP, selector \fIfm:\fP .INDENT 0.0 -.INDENT 3.5 -This is the default style for \-\-exclude and \-\-exclude\-from. +.TP +.B \fI\%Fnmatch\fP, selector \fIfm:\fP +This is the default style for \fB\-\-exclude\fP and \fB\-\-exclude\-from\fP\&. These patterns use a variant of shell pattern syntax, with \(aq*\(aq matching any number of characters, \(aq?\(aq matching any single character, \(aq[...]\(aq matching any single character specified, including ranges, and \(aq[!...]\(aq @@ -56,23 +57,15 @@ must match from the start to just before a path separator. Except for the root path, paths will never end in the path separator when matching is attempted. Thus, if a given pattern ends in a path separator, a \(aq*\(aq is appended before matching is attempted. -.UNINDENT -.UNINDENT -.sp -Shell\-style patterns, selector \fIsh:\fP -.INDENT 0.0 -.INDENT 3.5 +.TP +.B Shell\-style patterns, selector \fIsh:\fP This is the default style for \-\-pattern and \-\-patterns\-from. Like fnmatch patterns these are similar to shell patterns. The difference is that the pattern may include \fI**/\fP for matching zero or more directory levels, \fI*\fP for matching zero or more arbitrary characters with the exception of any path separator. -.UNINDENT -.UNINDENT -.sp -Regular expressions, selector \fIre:\fP -.INDENT 0.0 -.INDENT 3.5 +.TP +.B Regular expressions, selector \fIre:\fP Regular expressions similar to those found in Perl are supported. Unlike shell patterns regular expressions are not required to match the complete path and any substring match is sufficient. It is strongly recommended to @@ -81,20 +74,12 @@ separators (\(aq\(aq for Windows and \(aq/\(aq on other systems) in paths are always normalized to a forward slash (\(aq/\(aq) before applying a pattern. The regular expression syntax is described in the \fI\%Python documentation for the re module\fP\&. -.UNINDENT -.UNINDENT -.sp -Path prefix, selector \fIpp:\fP -.INDENT 0.0 -.INDENT 3.5 +.TP +.B Path prefix, selector \fIpp:\fP This pattern style is useful to match whole sub\-directories. The pattern \fIpp:/data/bar\fP matches \fI/data/bar\fP and everything therein. -.UNINDENT -.UNINDENT -.sp -Path full\-match, selector \fIpf:\fP -.INDENT 0.0 -.INDENT 3.5 +.TP +.B Path full\-match, selector \fIpf:\fP This pattern style is useful to match whole paths. This is kind of a pseudo pattern as it can not have any variable or unspecified parts \- the full, precise path must be given. @@ -109,13 +94,24 @@ If you use such a pattern to include a file, it will always be included Other include/exclude patterns that would normally match will be ignored. Same logic applies for exclude. .UNINDENT +.sp +\fBNOTE:\fP +.INDENT 0.0 +.INDENT 3.5 +\fIre:\fP, \fIsh:\fP and \fIfm:\fP patterns are all implemented on top of the Python SRE +engine. It is very easy to formulate patterns for each of these types which +requires an inordinate amount of time to match paths. If untrusted users +are able to supply patterns, ensure they cannot supply \fIre:\fP patterns. +Further, ensure that \fIsh:\fP and \fIfm:\fP patterns only contain a handful of +wildcards at most. +.UNINDENT .UNINDENT .sp -Exclusions can be passed via the command line option \fI\-\-exclude\fP\&. When used +Exclusions can be passed via the command line option \fB\-\-exclude\fP\&. When used from within a shell the patterns should be quoted to protect them from expansion. .sp -The \fI\-\-exclude\-from\fP option permits loading exclusion patterns from a text +The \fB\-\-exclude\-from\fP option permits loading exclusion patterns from a text file with one pattern per line. Lines empty or starting with the number sign (\(aq#\(aq) after removing whitespace on both ends are ignored. The optional style selector prefix is also supported for patterns loaded from a file. Due to @@ -159,26 +155,25 @@ $ borg create \-\-exclude\-from exclude.txt backup / .fi .UNINDENT .UNINDENT -.sp A more general and easier to use way to define filename matching patterns exists -with the \fI\-\-pattern\fP and \fI\-\-patterns\-from\fP options. Using these, you may specify -the backup roots (starting points) and patterns for inclusion/exclusion. A -root path starts with the prefix \fIR\fP, followed by a path (a plain path, not a +with the experimental \fB\-\-pattern\fP and \fB\-\-patterns\-from\fP options. Using these, you +may specify the backup roots (starting points) and patterns for inclusion/exclusion. +A root path starts with the prefix \fIR\fP, followed by a path (a plain path, not a file pattern). An include rule starts with the prefix +, an exclude rule starts with the prefix \-, both followed by a pattern. Inclusion patterns are useful to include paths that are contained in an excluded path. The first matching pattern is used so if an include pattern matches before an exclude pattern, the file is backed up. .sp -Note that the default pattern style for \fI\-\-pattern\fP and \fI\-\-patterns\-from\fP is +Note that the default pattern style for \fB\-\-pattern\fP and \fB\-\-patterns\-from\fP is shell style (\fIsh:\fP), so those patterns behave similar to rsync include/exclude patterns. The pattern style can be set via the \fIP\fP prefix. .sp -Patterns (\fI\-\-pattern\fP) and excludes (\fI\-\-exclude\fP) from the command line are -considered first (in the order of appearance). Then patterns from \fI\-\-patterns\-from\fP -are added. Exclusion patterns from \fI\-\-exclude\-from\fP files are appended last. +Patterns (\fB\-\-pattern\fP) and excludes (\fB\-\-exclude\fP) from the command line are +considered first (in the order of appearance). Then patterns from \fB\-\-patterns\-from\fP +are added. Exclusion patterns from \fB\-\-exclude\-from\fP files are appended last. .sp -An example \fI\-\-patterns\-from\fP file could look like that: +An example \fB\-\-patterns\-from\fP file could look like that: .INDENT 0.0 .INDENT 3.5 .sp diff --git a/docs/man/borg-placeholders.1 b/docs/man/borg-placeholders.1 index 72ae1046..3c3efbf8 100644 --- a/docs/man/borg-placeholders.1 +++ b/docs/man/borg-placeholders.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PLACEHOLDERS 1 "2017-05-17" "" "borg backup tool" +.TH BORG-PLACEHOLDERS 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-placeholders \- Details regarding placeholders . @@ -32,80 +32,42 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .SH DESCRIPTION .sp -Repository (or Archive) URLs, \-\-prefix and \-\-remote\-path values support these +Repository (or Archive) URLs, \fB\-\-prefix\fP and \fB\-\-remote\-path\fP values support these placeholders: -.sp -{hostname} .INDENT 0.0 -.INDENT 3.5 +.TP +.B {hostname} The (short) hostname of the machine. -.UNINDENT -.UNINDENT -.sp -{fqdn} -.INDENT 0.0 -.INDENT 3.5 +.TP +.B {fqdn} The full name of the machine. -.UNINDENT -.UNINDENT -.sp -{now} -.INDENT 0.0 -.INDENT 3.5 +.TP +.B {now} The current local date and time, by default in ISO\-8601 format. You can also supply your own \fI\%format string\fP, e.g. {now:%Y\-%m\-%d_%H:%M:%S} -.UNINDENT -.UNINDENT -.sp -{utcnow} -.INDENT 0.0 -.INDENT 3.5 +.TP +.B {utcnow} The current UTC date and time, by default in ISO\-8601 format. You can also supply your own \fI\%format string\fP, e.g. {utcnow:%Y\-%m\-%d_%H:%M:%S} -.UNINDENT -.UNINDENT -.sp -{user} -.INDENT 0.0 -.INDENT 3.5 +.TP +.B {user} The user name (or UID, if no name is available) of the user running borg. -.UNINDENT -.UNINDENT -.sp -{pid} -.INDENT 0.0 -.INDENT 3.5 +.TP +.B {pid} The current process ID. -.UNINDENT -.UNINDENT -.sp -{borgversion} -.INDENT 0.0 -.INDENT 3.5 +.TP +.B {borgversion} The version of borg, e.g.: 1.0.8rc1 -.UNINDENT -.UNINDENT -.sp -{borgmajor} -.INDENT 0.0 -.INDENT 3.5 +.TP +.B {borgmajor} The version of borg, only the major version, e.g.: 1 -.UNINDENT -.UNINDENT -.sp -{borgminor} -.INDENT 0.0 -.INDENT 3.5 +.TP +.B {borgminor} The version of borg, only major and minor version, e.g.: 1.0 -.UNINDENT -.UNINDENT -.sp -{borgpatch} -.INDENT 0.0 -.INDENT 3.5 +.TP +.B {borgpatch} The version of borg, only major, minor and patch version, e.g.: 1.0.8 .UNINDENT -.UNINDENT .sp If literal curly braces need to be used, double them for escaping: .INDENT 0.0 @@ -132,6 +94,20 @@ borg prune \-\-prefix \(aq{hostname}\-\(aq ... .fi .UNINDENT .UNINDENT +.sp +\fBNOTE:\fP +.INDENT 0.0 +.INDENT 3.5 +systemd uses a difficult, non\-standard syntax for command lines in unit files (refer to +the \fIsystemd.unit(5)\fP manual page). +.sp +When invoking borg from unit files, pay particular attention to escaping, +especially when using the now/utcnow placeholders, since systemd performs its own +%\-based variable replacement even in quoted text. To avoid interference from systemd, +double all percent signs (\fB{hostname}\-{now:%Y\-%m\-%d_%H:%M:%S}\fP +becomes \fB{hostname}\-{now:%%Y\-%%m\-%%d_%%H:%%M:%%S}\fP). +.UNINDENT +.UNINDENT .SH AUTHOR The Borg Collective .\" Generated by docutils manpage writer. diff --git a/docs/man/borg-prune.1 b/docs/man/borg-prune.1 index dcb817a6..941a398d 100644 --- a/docs/man/borg-prune.1 +++ b/docs/man/borg-prune.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PRUNE 1 "2017-05-17" "" "borg backup tool" +.TH BORG-PRUNE 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-prune \- Prune repository archives according to specified rules . @@ -42,7 +42,7 @@ automated backup scripts wanting to keep a certain number of historic backups. Also, prune automatically removes checkpoint archives (incomplete archives left behind by interrupted backup runs) except if the checkpoint is the latest archive (and thus still needed). Checkpoint archives are not considered when -comparing archive counts against the retention limits (\-\-keep\-X). +comparing archive counts against the retention limits (\fB\-\-keep\-X\fP). .sp If a prefix is set with \-P, then only archives that start with the prefix are considered for deletion and only those archives count towards the totals @@ -55,14 +55,14 @@ If you have multiple sequences of archives with different data sets (e.g. from different machines) in one shared repository, use one prune call per data set that matches only the respective archives using the \-P option. .sp -The "\-\-keep\-within" option takes an argument of the form "", -where char is "H", "d", "w", "m", "y". For example, "\-\-keep\-within 2d" means +The \fB\-\-keep\-within\fP option takes an argument of the form "", +where char is "H", "d", "w", "m", "y". For example, \fB\-\-keep\-within 2d\fP means to keep all archives that were created within the past 48 hours. "1m" is taken to mean "31d". The archives kept with this option do not count towards the totals specified by any other options. .sp A good procedure is to thin out more and more the older your backups get. -As an example, "\-\-keep\-daily 7" means to keep the latest backup on each day, +As an example, \fB\-\-keep\-daily 7\fP means to keep the latest backup on each day, up to 7 most recent days with backups (days without backups do not count). The rules are applied from secondly to yearly, and backups selected by previous rules do not count towards those of later rules. The time that each backup @@ -70,7 +70,7 @@ starts is used for pruning purposes. Dates and times are interpreted in the local timezone, and weeks go from Monday to Sunday. Specifying a negative number of archives to keep means that there is no limit. .sp -The "\-\-keep\-last N" option is doing the same as "\-\-keep\-secondly N" (and it will +The \fB\-\-keep\-last N\fP option is doing the same as \fB\-\-keep\-secondly N\fP (and it will keep the last N archives under the assumption that you do not create more than one backup archive in the same second). .SH OPTIONS @@ -121,12 +121,18 @@ number of monthly archives to keep .B \-y\fP,\fB \-\-keep\-yearly number of yearly archives to keep .TP -.B \-P\fP,\fB \-\-prefix -only consider archive names starting with this prefix -.TP .B \-\-save\-space work slower, but using less space .UNINDENT +.SS filters +.INDENT 0.0 +.TP +.B \-P\fP,\fB \-\-prefix +only consider archive names starting with this prefix. +.TP +.B \-a\fP,\fB \-\-glob\-archives +only consider archive names matching the glob. sh: rules apply, see "borg help patterns". \fB\-\-prefix\fP and \fB\-\-glob\-archives\fP are mutually exclusive. +.UNINDENT .SH EXAMPLES .sp Be careful, prune is a potentially dangerous command, it will remove backup diff --git a/docs/man/borg-recreate.1 b/docs/man/borg-recreate.1 index 0e22a320..9124d110 100644 --- a/docs/man/borg-recreate.1 +++ b/docs/man/borg-recreate.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-RECREATE 1 "2017-05-17" "" "borg backup tool" +.TH BORG-RECREATE 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-recreate \- Re-create archives . @@ -39,33 +39,33 @@ Recreate the contents of existing archives. .sp This is an \fIexperimental\fP feature. Do \fInot\fP use this on your only backup. .sp -\-\-exclude, \-\-exclude\-from, \-\-exclude\-if\-present, \-\-keep\-exclude\-tags, and PATH +\fB\-\-exclude\fP, \fB\-\-exclude\-from\fP, \fB\-\-exclude\-if\-present\fP, \fB\-\-keep\-exclude\-tags\fP, and PATH have the exact same semantics as in "borg create". If PATHs are specified the resulting archive will only contain files from these PATHs. .sp Note that all paths in an archive are relative, therefore absolute patterns/paths -will \fInot\fP match (\-\-exclude, \-\-exclude\-from, PATHs). +will \fInot\fP match (\fB\-\-exclude\fP, \fB\-\-exclude\-from\fP, PATHs). .sp -\-\-recompress allows to change the compression of existing data in archives. +\fB\-\-recompress\fP allows to change the compression of existing data in archives. Due to how Borg stores compressed size information this might display incorrect information for archives that were not recreated at the same time. There is no risk of data loss by this. .sp -\-\-chunker\-params will re\-chunk all files in the archive, this can be +\fB\-\-chunker\-params\fP will re\-chunk all files in the archive, this can be used to have upgraded Borg 0.xx or Attic archives deduplicate with Borg 1.x archives. .sp -USE WITH CAUTION. +\fBUSE WITH CAUTION.\fP Depending on the PATHs and patterns given, recreate can be used to permanently delete files from archives. -When in doubt, use "\-\-dry\-run \-\-verbose \-\-list" to see how patterns/PATHS are +When in doubt, use \fB\-\-dry\-run \-\-verbose \-\-list\fP to see how patterns/PATHS are interpreted. .sp The archive being recreated is only removed after the operation completes. The archive that is built during the operation exists at the same time at ".recreate". The new archive will have a different archive ID. .sp -With \-\-target the original archive is not replaced, instead a new archive is created. +With \fB\-\-target\fP the original archive is not replaced, instead a new archive is created. .sp When rechunking space usage can be substantial, expect at least the entire deduplicated size of the archives using the previous chunker params. @@ -114,13 +114,13 @@ exclude directories that contain a CACHEDIR.TAG file (\fI\%http://www.brynosauru exclude directories that are tagged by containing a filesystem object with the given NAME .TP .B \-\-keep\-exclude\-tags\fP,\fB \-\-keep\-tag\-files -if tag objects are specified with \-\-exclude\-if\-present, don\(aqt omit the tag objects themselves from the backup archive +if tag objects are specified with \fB\-\-exclude\-if\-present\fP, don\(aqt omit the tag objects themselves from the backup archive .TP .BI \-\-pattern \ PATTERN -include/exclude paths matching PATTERN +experimental: include/exclude paths matching PATTERN .TP .BI \-\-patterns\-from \ PATTERNFILE -read include/exclude patterns from PATTERNFILE, one per line +experimental: read include/exclude patterns from PATTERNFILE, one per line .UNINDENT .SS Archive options .INDENT 0.0 @@ -141,10 +141,10 @@ manually specify the archive creation date/time (UTC, yyyy\-mm\-ddThh:mm:ss form select compression algorithm, see the output of the "borg help compression" command for details. .TP .B \-\-recompress -recompress data chunks according to \-\-compression if "if\-different". When "always", chunks that are already compressed that way are not skipped, but compressed again. Only the algorithm is considered for "if\-different", not the compression level (if any). +recompress data chunks according to \fB\-\-compression\fP if \fIif\-different\fP\&. When \fIalways\fP, chunks that are already compressed that way are not skipped, but compressed again. Only the algorithm is considered for \fIif\-different\fP, not the compression level (if any). .TP .BI \-\-chunker\-params \ PARAMS -specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or "default" to use the current defaults. default: 19,23,21,4095 +specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or \fIdefault\fP to use the current defaults. default: 19,23,21,4095 .UNINDENT .SH EXAMPLES .INDENT 0.0 diff --git a/docs/man/borg-rename.1 b/docs/man/borg-rename.1 index 9c075314..17e65f43 100644 --- a/docs/man/borg-rename.1 +++ b/docs/man/borg-rename.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-RENAME 1 "2017-05-17" "" "borg backup tool" +.TH BORG-RENAME 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-rename \- Rename an existing archive . diff --git a/docs/man/borg-serve.1 b/docs/man/borg-serve.1 index 798d6486..5052c724 100644 --- a/docs/man/borg-serve.1 +++ b/docs/man/borg-serve.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-SERVE 1 "2017-05-17" "" "borg backup tool" +.TH BORG-SERVE 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-serve \- Start in server mode. This command is usually not used manually. . @@ -45,8 +45,14 @@ See \fIborg\-common(1)\fP for common options of Borg commands. .BI \-\-restrict\-to\-path \ PATH restrict repository access to PATH. Can be specified multiple times to allow the client access to several directories. Access to all sub\-directories is granted implicitly; PATH doesn\(aqt need to directly point to a repository. .TP +.BI \-\-restrict\-to\-repository \ PATH +restrict repository access. Only the repository located at PATH (no sub\-directories are considered) is accessible. Can be specified multiple times to allow the client access to several repositories. Unlike \-\-restrict\-to\-path sub\-directories are not accessible; PATH needs to directly point at a repository location. PATH may be an empty directory or the last element of PATH may not exist, in which case the client may initialize a repository there. +.TP .B \-\-append\-only only allow appending to repository segment files +.TP +.B \-\-storage\-quota +Override storage quota of the repository (e.g. 5G, 1.5T). When a new repository is initialized, sets the storage quota on the new repository as well. Default: no quota. .UNINDENT .SH EXAMPLES .sp diff --git a/docs/man/borg-umount.1 b/docs/man/borg-umount.1 index 26a3c1f6..21b3c2ee 100644 --- a/docs/man/borg-umount.1 +++ b/docs/man/borg-umount.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-UMOUNT 1 "2017-05-17" "" "borg backup tool" +.TH BORG-UMOUNT 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-umount \- un-mount the FUSE filesystem . @@ -101,7 +101,7 @@ bin boot etc home lib lib64 lost+found media mnt opt root sbin s .INDENT 0.0 .INDENT 3.5 \fBborgfs\fP will be automatically provided if you used a distribution -package, \fBpip\fP or \fBsetup.py\fP to install Borg\&. Users of the +package, \fBpip\fP or \fBsetup.py\fP to install Borg. Users of the standalone binary will have to manually create a symlink (see \fIpyinstaller\-binary\fP). .UNINDENT diff --git a/docs/man/borg-upgrade.1 b/docs/man/borg-upgrade.1 index ed9e419d..0d4cef89 100644 --- a/docs/man/borg-upgrade.1 +++ b/docs/man/borg-upgrade.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-UPGRADE 1 "2017-05-17" "" "borg backup tool" +.TH BORG-UPGRADE 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-upgrade \- upgrade a repository from a previous version . @@ -132,7 +132,7 @@ path to the repository to be upgraded .B \-n\fP,\fB \-\-dry\-run do not change repository .TP -.B \-i\fP,\fB \-\-inplace +.B \-\-inplace rewrite repository in place, with no chance of going back to older versions of the repository. .TP diff --git a/docs/man/borg-with-lock.1 b/docs/man/borg-with-lock.1 index 4099a911..c71bd28a 100644 --- a/docs/man/borg-with-lock.1 +++ b/docs/man/borg-with-lock.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH BORG-WITH-LOCK 1 "2017-05-17" "" "borg backup tool" +.TH BORG-WITH-LOCK 1 "2017-06-18" "" "borg backup tool" .SH NAME borg-with-lock \- run a user specified command with the repository lock held . diff --git a/docs/man/borg.1 b/docs/man/borg.1 index e6d15b0f..99493444 100644 --- a/docs/man/borg.1 +++ b/docs/man/borg.1 @@ -259,7 +259,7 @@ If you have set BORG_REPO (see above) and an archive location is needed, use The log level of the builtin logging configuration defaults to WARNING. This is because we want Borg to be mostly silent and only output warnings, errors and critical messages, unless output has been requested -by supplying an option that implies output (eg, \-\-list or \-\-progress). +by supplying an option that implies output (e.g. \fB\-\-list\fP or \fB\-\-progress\fP). .sp Log levels: DEBUG < INFO < WARNING < ERROR < CRITICAL .sp @@ -284,28 +284,50 @@ give different output on different log levels \- it\(aqs just a possibility. \fBWARNING:\fP .INDENT 0.0 .INDENT 3.5 -Options \-\-critical and \-\-error are provided for completeness, +Options \fB\-\-critical\fP and \fB\-\-error\fP are provided for completeness, their usage is not recommended as you might miss important information. .UNINDENT .UNINDENT .SS Return codes .sp Borg can exit with the following return codes (rc): -.INDENT 0.0 -.INDENT 3.5 -.sp -.nf -.ft C -0 = success (logged as INFO) -1 = warning (operation reached its normal end, but there were warnings \- - you should check the log, logged as WARNING) -2 = error (like a fatal error, a local or remote exception, the operation - did not reach its normal end, logged as ERROR) -128+N = killed by signal N (e.g. 137 == kill \-9) -.ft P -.fi -.UNINDENT -.UNINDENT +.TS +center; +|l|l|. +_ +T{ +Return code +T} T{ +Meaning +T} +_ +T{ +0 +T} T{ +success (logged as INFO) +T} +_ +T{ +1 +T} T{ +warning (operation reached its normal end, but there were warnings \-\- +you should check the log, logged as WARNING) +T} +_ +T{ +2 +T} T{ +error (like a fatal error, a local or remote exception, the operation +did not reach its normal end, logged as ERROR) +T} +_ +T{ +128+N +T} T{ +killed by signal N (e.g. 137 == kill \-9) +T} +_ +.TE .sp If you use \fB\-\-show\-rc\fP, the return code is also logged at the indicated level as the last log entry. @@ -319,18 +341,27 @@ Borg uses some environment variables for automation: .TP .B BORG_REPO When set, use the value to give the default repository location. If a command needs an archive -parameter, you can abbreviate as \fI::archive\fP\&. If a command needs a repository parameter, you -can either leave it away or abbreviate as \fI::\fP, if a positional parameter is required. +parameter, you can abbreviate as \fB::archive\fP\&. If a command needs a repository parameter, you +can either leave it away or abbreviate as \fB::\fP, if a positional parameter is required. .TP .B BORG_PASSPHRASE When set, use the value to answer the passphrase question for encrypted repositories. -It is used when a passphrase is needed to access a encrypted repo as well as when a new +It is used when a passphrase is needed to access an encrypted repo as well as when a new passphrase should be initially set when initializing an encrypted repo. See also BORG_NEW_PASSPHRASE. .TP +.B BORG_PASSCOMMAND +When set, use the standard output of the command (trailing newlines are stripped) to answer the +passphrase question for encrypted repositories. +It is used when a passphrase is needed to access an encrypted repo as well as when a new +passphrase should be initially set when initializing an encrypted repo. +If BORG_PASSPHRASE is also set, it takes precedence. +See also BORG_NEW_PASSPHRASE. +.TP .B BORG_NEW_PASSPHRASE When set, use the value to answer the passphrase question when a \fBnew\fP passphrase is asked for. -This variable is checked first. If it is not set, BORG_PASSPHRASE will be checked also. +This variable is checked first. If it is not set, BORG_PASSPHRASE and BORG_PASSCOMMAND will also +be checked. Main usecase for this is to fully automate \fBborg change\-passphrase\fP\&. .TP .B BORG_DISPLAY_PASSPHRASE @@ -537,7 +568,7 @@ depending on archive count and size \- see FAQ about how to reduce). .TP .B Network (only for client/server operation): If your repository is remote, all deduplicated (and optionally compressed/ -encrypted) data of course has to go over the connection (ssh: repo url). +encrypted) data of course has to go over the connection (\fBssh://\fP repo url). If you use a locally mounted network filesystem, additionally some copy operations used for transaction support also go over the connection. If you backup multiple sources to one target repository, additional traffic From 55e1a543856726a199e4931fb688135fb462bea5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Sun, 18 Jun 2017 13:32:12 +0200 Subject: [PATCH 1100/1387] AdHocCache: avoid divison by zero 0.01 ~ "one tick or less". ymmv. --- src/borg/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 47b53deb..fb113d93 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -1060,7 +1060,7 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" assert len(self.chunks) == num_chunks # LocalCache does not contain the manifest, either. del self.chunks[self.manifest.MANIFEST_ID] - duration = perf_counter() - t0 + duration = perf_counter() - t0 or 0.01 pi.finish() logger.debug('AdHocCache: downloaded %d chunk IDs in %.2f s (%d requests), ~%s/s', num_chunks, duration, num_requests, format_file_size(num_chunks * 34 / duration)) From f741ac231046ecd5bea1389fa86cf174ec0df7e6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 18 Jun 2017 05:07:46 +0200 Subject: [PATCH 1101/1387] CHANGES: add release date --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 837993c1..07227c7d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -131,7 +131,7 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= -Version 1.1.0b6 (unreleased) +Version 1.1.0b6 (2017-06-18) ---------------------------- Compatibility notes: From 0aac3a129d29f806d85e0d993a9e1fa75317e81d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 18 Jun 2017 05:28:33 +0200 Subject: [PATCH 1102/1387] MANIFEST.in: exclude .coafile (coala) --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index bbadcbce..2ec72dc2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include README.rst AUTHORS LICENSE CHANGES.rst MANIFEST.in -exclude .coveragerc .gitattributes .gitignore .travis.yml Vagrantfile +exclude .coafile .coveragerc .gitattributes .gitignore .travis.yml Vagrantfile prune .travis prune .github graft src From 944ae4afc375aac0550e4d7c82271d0db46d2dc6 Mon Sep 17 00:00:00 2001 From: Narendra Vardi Date: Sun, 18 Jun 2017 20:18:26 +0530 Subject: [PATCH 1103/1387] Don't perform full Travis build on docs-only changes #2531 --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index bfebcf67..88c71471 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,6 +42,13 @@ matrix: osx_image: xcode6.4 env: TOXENV=py36 +before_install: +- | + git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(AUTHORS|README\.rst|^(docs)/)' || { + echo "Only docs were updated, stopping build process." + exit + } + install: - git fetch --unshallow --tags - ./.travis/install.sh From 379378fbba8720adecd900e75e8712bf5c274880 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 18 Jun 2017 18:07:45 +0200 Subject: [PATCH 1104/1387] vagrant: add Debian 9 "stretch" 64bit --- Vagrantfile | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Vagrantfile b/Vagrantfile index 0c670430..ef96e0ad 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -429,6 +429,17 @@ Vagrant.configure(2) do |config| b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("trusty64") end + config.vm.define "stretch64" do |b| + b.vm.box = "debian/stretch64" + b.vm.provider :virtualbox do |v| + v.memory = 1024 + $wmem + end + b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid + b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("stretch64") + b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) + b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("stretch64") + end + config.vm.define "jessie64" do |b| b.vm.box = "debian/jessie64" b.vm.provider :virtualbox do |v| From 6e17ca7c3a6f13d37d2df6fe047e9555a3a678dd Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 19 Jun 2017 09:50:22 +0200 Subject: [PATCH 1105/1387] docs: fix typo --- docs/internals/data-structures.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 27089d6f..d0f1814b 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -17,7 +17,7 @@ Repository .. Some parts of this description were taken from the Repository docstring -|project_name| stores its data in a `Repository`, which is a filesystem-based +Borg stores its data in a `Repository`, which is a file system based transactional key-value store. Thus the repository does not know about the concept of archives or items. From a4440dac4d4bda599f2620e0fbe2c608849355a9 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 19 Jun 2017 09:55:56 +0200 Subject: [PATCH 1106/1387] travis: print commit range --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 88c71471..e12266b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,6 +44,7 @@ matrix: before_install: - | + echo Checking whether $TRAVIS_COMMIT_RANGE changed only docs git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(AUTHORS|README\.rst|^(docs)/)' || { echo "Only docs were updated, stopping build process." exit From d495b0fe75631858413579278caf3133aeb26840 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 19 Jun 2017 10:01:55 +0200 Subject: [PATCH 1107/1387] docs: move introduction sentence --- docs/internals.rst | 5 ++--- docs/internals/data-structures.rst | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/internals.rst b/docs/internals.rst index af951a8a..786125d0 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -4,9 +4,8 @@ Internals ========= -This page documents the internal data structures and storage -mechanisms of |project_name|. It is partly based on `mailing list -discussion about internals`_ and also on static code analysis. +The internals chapter describes and analyses most of the inner workings +of Borg. Borg uses a low-level, key-value store, the :ref:`repository`, and implements a more complex data structure on top of it, which is made diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index d0f1814b..09886e4a 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -6,6 +6,10 @@ Data structures and file formats ================================ +This page documents the internal data structures and storage +mechanisms of Borg. It is partly based on `mailing list +discussion about internals`_ and also on static code analysis. + .. todo:: Clarify terms, perhaps create a glossary. ID (client?) vs. key (repository?), chunks (blob of data in repo?) vs. object (blob of data in repo, referred to from another object?), From 496922f4e0f68da2a327579431c54174c0ff5ca5 Mon Sep 17 00:00:00 2001 From: enkore Date: Tue, 20 Jun 2017 01:06:40 +0200 Subject: [PATCH 1108/1387] changes: provisional 1.1.0rc1 header --- docs/changes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 07227c7d..5f95bb06 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -131,6 +131,9 @@ The best check that everything is ok is to run a dry-run extraction:: Changelog ========= +Version 1.1.0rc1 (unreleased) +----------------------------- + Version 1.1.0b6 (2017-06-18) ---------------------------- From 6290b73863b8098a4bf6f5d28e7a2cb1361277df Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Jun 2017 11:19:26 +0200 Subject: [PATCH 1109/1387] docs: more compact options formatting --- docs/borg_theme/css/borg.css | 26 +++++++++++++++++++++--- docs/conf.py | 8 ++++++++ setup.py | 38 ++++++++++++++++-------------------- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 3394f49c..abd7b7a1 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -109,7 +109,8 @@ table.docutils:not(.footnote) th { border: 1px solid #ddd; } -table.docutils:not(.footnote) tr:first-child th { +table.docutils:not(.footnote) tr:first-child th, +table.docutils:not(.footnote) tr:first-child td { border-top: 0; } @@ -118,15 +119,18 @@ table.docutils:not(.footnote) tr:last-child td { } table.docutils:not(.footnote) tr td:first-child, -table.docutils:not(.footnote) tr th:first-child { +table.docutils:not(.footnote) tr th:first-child, +table.docutils.option-list tr td { border-left: 0; } table.docutils:not(.footnote) tr td:last-child, -table.docutils:not(.footnote) tr th:last-child { +table.docutils:not(.footnote) tr th:last-child, +table.docutils.option-list tr td { border-right: 0; } +kbd, /* used in usage pages for options */ code, .rst-content tt.literal, .rst-content tt.literal, @@ -141,6 +145,22 @@ p .literal span { background: none; } +kbd { + box-shadow: none; + line-height: 23px; + word-wrap: normal; + font-size: 15px; + font-family: Consolas, monospace; +} + +kbd .option { + white-space: nowrap; +} + +table.docutils.option-list td.option-group { + min-width: 10em; +} + cite { white-space: nowrap; color: black; /* slight contrast with #404040 of regular text */ diff --git a/docs/conf.py b/docs/conf.py index 8f6b867c..283a2c7a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -105,8 +105,16 @@ html_theme_path = guzzle_sphinx_theme.html_theme_path() html_theme = 'guzzle_sphinx_theme' +def set_rst_settings(app): + app.env.settings.update({ + 'field_name_limit': 0, + 'option_limit': 0, + }) + + def setup(app): app.add_stylesheet('css/borg.css') + app.connect('builder-inited', set_rst_settings) # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/setup.py b/setup.py index 3b9b495b..2b58dcdf 100644 --- a/setup.py +++ b/setup.py @@ -285,6 +285,7 @@ class build_usage(Command): if option.option_strings: continue fp.write(' ' + option.metavar) + fp.write('\n\n') def write_options(self, parser, fp): for group in parser._action_groups: @@ -298,12 +299,13 @@ class build_usage(Command): def is_positional_group(group): return any(not o.option_strings for o in group._group_actions) - def get_help(option): - text = textwrap.dedent((option.help or '') % option.__dict__) - return '\n'.join('| ' + line for line in text.splitlines()) + indent = ' ' * base_indent - def shipout(text): - fp.write(textwrap.indent('\n'.join(text), ' ' * base_indent)) + if is_positional_group(group): + for option in group._group_actions: + fp.write(option.metavar + '\n') + fp.write(textwrap.indent(option.help or '', ' ' * base_indent) + '\n') + return if not group._group_actions: return @@ -311,28 +313,22 @@ class build_usage(Command): if with_title: fp.write('\n\n') fp.write(group.title + '\n') - text = [] - if is_positional_group(group): - for option in group._group_actions: - text.append(option.metavar) - text.append(textwrap.indent(option.help or '', ' ' * 4)) - shipout(text) - return + opts = OrderedDict() - options = [] for option in group._group_actions: if option.metavar: - option_fmt = '``%%s %s``' % option.metavar + option_fmt = '%s ' + option.metavar else: - option_fmt = '``%s``' + option_fmt = '%s' option_str = ', '.join(option_fmt % s for s in option.option_strings) - options.append((option_str, option)) - for option_str, option in options: - help = textwrap.indent(get_help(option), ' ' * 4) - text.append(option_str) - text.append(help) - shipout(text) + option_desc = textwrap.dedent((option.help or '') % option.__dict__) + opts[option_str] = textwrap.indent(option_desc, ' ' * 4) + + padding = len(max(opts)) + 1 + + for option, desc in opts.items(): + fp.write(indent + option.ljust(padding) + desc + '\n') class build_man(Command): From e5012b11cafc8ce7f3d2b3e552094672cba458e6 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Jun 2017 11:49:26 +0200 Subject: [PATCH 1110/1387] ran build_usage --- docs/usage/benchmark_crud.rst.inc | 10 +-- docs/usage/break-lock.rst.inc | 6 +- docs/usage/change-passphrase.rst.inc | 4 +- docs/usage/check.rst.inc | 38 ++++------ docs/usage/common-options.rst.inc | 54 +++++--------- docs/usage/create.rst.inc | 86 +++++++++-------------- docs/usage/delete.rst.inc | 35 ++++----- docs/usage/diff.rst.inc | 46 +++++------- docs/usage/export-tar.rst.inc | 36 ++++------ docs/usage/extract.rst.inc | 41 +++++------ docs/usage/info.rst.inc | 26 +++---- docs/usage/init.rst.inc | 16 ++--- docs/usage/key_change-passphrase.rst.inc | 4 +- docs/usage/key_export.rst.inc | 15 ++-- docs/usage/key_import.rst.inc | 12 ++-- docs/usage/key_migrate-to-repokey.rst.inc | 4 +- docs/usage/list.rst.inc | 63 +++++++---------- docs/usage/mount.rst.inc | 33 ++++----- docs/usage/prune.rst.inc | 53 ++++++-------- docs/usage/recreate.rst.inc | 67 +++++++----------- docs/usage/rename.rst.inc | 10 +-- docs/usage/serve.rst.inc | 15 ++-- docs/usage/umount.rst.inc | 6 +- docs/usage/upgrade.rst.inc | 24 +++---- docs/usage/with-lock.rst.inc | 14 ++-- 25 files changed, 292 insertions(+), 426 deletions(-) diff --git a/docs/usage/benchmark_crud.rst.inc b/docs/usage/benchmark_crud.rst.inc index b692cae1..c0274dbe 100644 --- a/docs/usage/benchmark_crud.rst.inc +++ b/docs/usage/benchmark_crud.rst.inc @@ -8,11 +8,11 @@ borg benchmark crud borg [common options] benchmark crud [options] REPO PATH -positional arguments - REPO - repo to use for benchmark (must exist) - PATH - path were to create benchmark input data +REPO + repo to use for benchmark (must exist) +PATH + path were to create benchmark input data + :ref:`common_options` | diff --git a/docs/usage/break-lock.rst.inc b/docs/usage/break-lock.rst.inc index fe5dcc84..341fa8a0 100644 --- a/docs/usage/break-lock.rst.inc +++ b/docs/usage/break-lock.rst.inc @@ -8,9 +8,9 @@ borg break-lock borg [common options] break-lock [options] REPOSITORY -positional arguments - REPOSITORY - repository for which to break the locks +REPOSITORY + repository for which to break the locks + :ref:`common_options` | diff --git a/docs/usage/change-passphrase.rst.inc b/docs/usage/change-passphrase.rst.inc index 29384d47..c9cbee36 100644 --- a/docs/usage/change-passphrase.rst.inc +++ b/docs/usage/change-passphrase.rst.inc @@ -8,8 +8,8 @@ borg change-passphrase borg [common options] change-passphrase [options] REPOSITORY -positional arguments - REPOSITORY +REPOSITORY + :ref:`common_options` diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index 047da6b2..ad31b6a2 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -8,36 +8,28 @@ borg check borg [common options] check [options] REPOSITORY_OR_ARCHIVE -positional arguments - REPOSITORY_OR_ARCHIVE - repository or archive to check consistency of +REPOSITORY_OR_ARCHIVE + repository or archive to check consistency of + optional arguments - ``--repository-only`` - | only perform repository checks - ``--archives-only`` - | only perform archives checks - ``--verify-data`` - | perform cryptographic archive data integrity verification (conflicts with ``--repository-only``) - ``--repair`` - | attempt to repair any inconsistencies found - ``--save-space`` - | work slower, but using less space + --repository-only only perform repository checks + --archives-only only perform archives checks + --verify-data perform cryptographic archive data integrity verification (conflicts with ``--repository-only``) + --repair attempt to repair any inconsistencies found + --save-space work slower, but using less space + :ref:`common_options` | filters - ``-P``, ``--prefix`` - | only consider archive names starting with this prefix. - ``-a``, ``--glob-archives`` - | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. - ``--sort-by`` - | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp - ``--first N`` - | consider first N archives after other filters were applied - ``--last N`` - | consider last N archives after other filters were applied + -P, --prefix only consider archive names starting with this prefix. + -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + --first N consider first N archives after other filters were applied + --last N consider last N archives after other filters were applied + Description ~~~~~~~~~~~ diff --git a/docs/usage/common-options.rst.inc b/docs/usage/common-options.rst.inc index 6bc18eba..a0d0bd0e 100644 --- a/docs/usage/common-options.rst.inc +++ b/docs/usage/common-options.rst.inc @@ -1,36 +1,18 @@ -``-h``, ``--help`` - | show this help message and exit -``--critical`` - | work on log level CRITICAL -``--error`` - | work on log level ERROR -``--warning`` - | work on log level WARNING (default) -``--info``, ``-v``, ``--verbose`` - | work on log level INFO -``--debug`` - | enable debug output, work on log level DEBUG -``--debug-topic TOPIC`` - | enable TOPIC debugging (can be specified multiple times). The logger path is borg.debug. if TOPIC is not fully qualified. -``-p``, ``--progress`` - | show progress information -``--log-json`` - | Output one JSON object per log line instead of formatted text. -``--lock-wait N`` - | wait for the lock, but max. N seconds (default: 1). -``--show-version`` - | show/log the borg version -``--show-rc`` - | show/log the return code (rc) -``--no-files-cache`` - | do not load/update the file metadata cache used to detect unchanged files -``--umask M`` - | set umask to M (local and remote, default: 0077) -``--remote-path PATH`` - | use PATH as borg executable on the remote (default: "borg") -``--remote-ratelimit rate`` - | set remote network upload rate limit in kiByte/s (default: 0=unlimited) -``--consider-part-files`` - | treat part files like normal files (e.g. to list/extract them) -``--debug-profile FILE`` - | Write execution profile in Borg format into FILE. For local use a Python-compatible file can be generated by suffixing FILE with ".pyprof". \ No newline at end of file +-h, --help show this help message and exit +--critical work on log level CRITICAL +--error work on log level ERROR +--warning work on log level WARNING (default) +--info, -v, --verbose work on log level INFO +--debug enable debug output, work on log level DEBUG +--debug-topic TOPIC enable TOPIC debugging (can be specified multiple times). The logger path is borg.debug. if TOPIC is not fully qualified. +-p, --progress show progress information +--log-json Output one JSON object per log line instead of formatted text. +--lock-wait N wait for the lock, but max. N seconds (default: 1). +--show-version show/log the borg version +--show-rc show/log the return code (rc) +--no-files-cache do not load/update the file metadata cache used to detect unchanged files +--umask M set umask to M (local and remote, default: 0077) +--remote-path PATH use PATH as borg executable on the remote (default: "borg") +--remote-ratelimit rate set remote network upload rate limit in kiByte/s (default: 0=unlimited) +--consider-part-files treat part files like normal files (e.g. to list/extract them) +--debug-profile FILE Write execution profile in Borg format into FILE. For local use a Python-compatible file can be generated by suffixing FILE with ".pyprof". diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 2a76c0dd..0480befb 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -8,70 +8,50 @@ borg create borg [common options] create [options] ARCHIVE PATH -positional arguments - ARCHIVE - name of archive to create (must be also a valid directory name) - PATH - paths to archive +ARCHIVE + name of archive to create (must be also a valid directory name) +PATH + paths to archive + optional arguments - ``-n``, ``--dry-run`` - | do not create a backup archive - ``-s``, ``--stats`` - | print statistics for the created archive - ``--list`` - | output verbose list of items (files, dirs, ...) - ``--filter STATUSCHARS`` - | only display items with the given status characters - ``--json`` - | output stats as JSON (implies --stats) - ``--no-cache-sync`` - | experimental: do not synchronize the cache. Implies --no-files-cache. + -n, --dry-run do not create a backup archive + -s, --stats print statistics for the created archive + --list output verbose list of items (files, dirs, ...) + --filter STATUSCHARS only display items with the given status characters + --json output stats as JSON (implies --stats) + --no-cache-sync experimental: do not synchronize the cache. Implies --no-files-cache. + :ref:`common_options` | Exclusion options - ``-e PATTERN``, ``--exclude PATTERN`` - | exclude paths matching PATTERN - ``--exclude-from EXCLUDEFILE`` - | read exclude patterns from EXCLUDEFILE, one per line - ``--exclude-caches`` - | exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) - ``--exclude-if-present NAME`` - | exclude directories that are tagged by containing a filesystem object with the given NAME - ``--keep-exclude-tags``, ``--keep-tag-files`` - | if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive - ``--pattern PATTERN`` - | experimental: include/exclude paths matching PATTERN - ``--patterns-from PATTERNFILE`` - | experimental: read include/exclude patterns from PATTERNFILE, one per line + -e PATTERN, --exclude PATTERN exclude paths matching PATTERN + --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line + --exclude-caches exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) + --exclude-if-present NAME exclude directories that are tagged by containing a filesystem object with the given NAME + --keep-exclude-tags, --keep-tag-files if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive + --pattern PATTERN experimental: include/exclude paths matching PATTERN + --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line + Filesystem options - ``-x``, ``--one-file-system`` - | stay in the same file system and do not store mount points of other file systems - ``--numeric-owner`` - | only store numeric user and group identifiers - ``--noatime`` - | do not store atime into archive - ``--noctime`` - | do not store ctime into archive - ``--ignore-inode`` - | ignore inode data in the file metadata cache used to detect unchanged files. - ``--read-special`` - | open and read block and char device files as well as FIFOs as if they were regular files. Also follows symlinks pointing to these kinds of files. + -x, --one-file-system stay in the same file system and do not store mount points of other file systems + --numeric-owner only store numeric user and group identifiers + --noatime do not store atime into archive + --noctime do not store ctime into archive + --ignore-inode ignore inode data in the file metadata cache used to detect unchanged files. + --read-special open and read block and char device files as well as FIFOs as if they were regular files. Also follows symlinks pointing to these kinds of files. + Archive options - ``--comment COMMENT`` - | add a comment text to the archive - ``--timestamp TIMESTAMP`` - | manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). alternatively, give a reference file/directory. - ``-c SECONDS``, ``--checkpoint-interval SECONDS`` - | write checkpoint every SECONDS seconds (Default: 1800) - ``--chunker-params PARAMS`` - | specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: 19,23,21,4095 - ``-C COMPRESSION``, ``--compression COMPRESSION`` - | select compression algorithm, see the output of the "borg help compression" command for details. + --comment COMMENT add a comment text to the archive + --timestamp TIMESTAMP manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). alternatively, give a reference file/directory. + -c SECONDS, --checkpoint-interval SECONDS write checkpoint every SECONDS seconds (Default: 1800) + --chunker-params PARAMS specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: 19,23,21,4095 + -C COMPRESSION, --compression COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. + Description ~~~~~~~~~~~ diff --git a/docs/usage/delete.rst.inc b/docs/usage/delete.rst.inc index 18e9d539..294900fe 100644 --- a/docs/usage/delete.rst.inc +++ b/docs/usage/delete.rst.inc @@ -8,34 +8,27 @@ borg delete borg [common options] delete [options] TARGET -positional arguments - TARGET - archive or repository to delete +TARGET + archive or repository to delete + optional arguments - ``-s``, ``--stats`` - | print statistics for the deleted archive - ``-c``, ``--cache-only`` - | delete only the local cache for the given repository - ``--force`` - | force deletion of corrupted archives, use --force --force in case --force does not work. - ``--save-space`` - | work slower, but using less space + -s, --stats print statistics for the deleted archive + -c, --cache-only delete only the local cache for the given repository + --force force deletion of corrupted archives, use --force --force in case --force does not work. + --save-space work slower, but using less space + :ref:`common_options` | filters - ``-P``, ``--prefix`` - | only consider archive names starting with this prefix. - ``-a``, ``--glob-archives`` - | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. - ``--sort-by`` - | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp - ``--first N`` - | consider first N archives after other filters were applied - ``--last N`` - | consider last N archives after other filters were applied + -P, --prefix only consider archive names starting with this prefix. + -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + --first N consider first N archives after other filters were applied + --last N consider last N archives after other filters were applied + Description ~~~~~~~~~~~ diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index 0de6e909..1c3d7fb1 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -8,40 +8,32 @@ borg diff borg [common options] diff [options] REPO_ARCHIVE1 ARCHIVE2 PATH -positional arguments - REPO_ARCHIVE1 - repository location and ARCHIVE1 name - ARCHIVE2 - ARCHIVE2 name (no repository location allowed) - PATH - paths of items inside the archives to compare; patterns are supported +REPO_ARCHIVE1 + repository location and ARCHIVE1 name +ARCHIVE2 + ARCHIVE2 name (no repository location allowed) +PATH + paths of items inside the archives to compare; patterns are supported + optional arguments - ``--numeric-owner`` - | only consider numeric user and group identifiers - ``--same-chunker-params`` - | Override check of chunker parameters. - ``--sort`` - | Sort the output lines by file path. + --numeric-owner only consider numeric user and group identifiers + --same-chunker-params Override check of chunker parameters. + --sort Sort the output lines by file path. + :ref:`common_options` | Exclusion options - ``-e PATTERN``, ``--exclude PATTERN`` - | exclude paths matching PATTERN - ``--exclude-from EXCLUDEFILE`` - | read exclude patterns from EXCLUDEFILE, one per line - ``--exclude-caches`` - | exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) - ``--exclude-if-present NAME`` - | exclude directories that are tagged by containing a filesystem object with the given NAME - ``--keep-exclude-tags``, ``--keep-tag-files`` - | if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive - ``--pattern PATTERN`` - | experimental: include/exclude paths matching PATTERN - ``--patterns-from PATTERNFILE`` - | experimental: read include/exclude patterns from PATTERNFILE, one per line + -e PATTERN, --exclude PATTERN exclude paths matching PATTERN + --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line + --exclude-caches exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) + --exclude-if-present NAME exclude directories that are tagged by containing a filesystem object with the given NAME + --keep-exclude-tags, --keep-tag-files if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive + --pattern PATTERN experimental: include/exclude paths matching PATTERN + --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line + Description ~~~~~~~~~~~ diff --git a/docs/usage/export-tar.rst.inc b/docs/usage/export-tar.rst.inc index 327a0874..d28bf999 100644 --- a/docs/usage/export-tar.rst.inc +++ b/docs/usage/export-tar.rst.inc @@ -8,29 +8,23 @@ borg export-tar borg [common options] export-tar [options] ARCHIVE FILE PATH -positional arguments - ARCHIVE - archive to export - FILE - output tar file. "-" to write to stdout instead. - PATH - paths to extract; patterns are supported +ARCHIVE + archive to export +FILE + output tar file. "-" to write to stdout instead. +PATH + paths to extract; patterns are supported + optional arguments - ``--tar-filter`` - | filter program to pipe data through - ``--list`` - | output verbose list of items (files, dirs, ...) - ``-e PATTERN``, ``--exclude PATTERN`` - | exclude paths matching PATTERN - ``--exclude-from EXCLUDEFILE`` - | read exclude patterns from EXCLUDEFILE, one per line - ``--pattern PATTERN`` - | experimental: include/exclude paths matching PATTERN - ``--patterns-from PATTERNFILE`` - | experimental: read include/exclude patterns from PATTERNFILE, one per line - ``--strip-components NUMBER`` - | Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. + --tar-filter filter program to pipe data through + --list output verbose list of items (files, dirs, ...) + -e PATTERN, --exclude PATTERN exclude paths matching PATTERN + --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line + --pattern PATTERN experimental: include/exclude paths matching PATTERN + --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line + --strip-components NUMBER Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. + :ref:`common_options` | diff --git a/docs/usage/extract.rst.inc b/docs/usage/extract.rst.inc index e9913d65..e23eecd0 100644 --- a/docs/usage/extract.rst.inc +++ b/docs/usage/extract.rst.inc @@ -8,33 +8,24 @@ borg extract borg [common options] extract [options] ARCHIVE PATH -positional arguments - ARCHIVE - archive to extract - PATH - paths to extract; patterns are supported +ARCHIVE + archive to extract +PATH + paths to extract; patterns are supported + optional arguments - ``--list`` - | output verbose list of items (files, dirs, ...) - ``-n``, ``--dry-run`` - | do not actually change any files - ``-e PATTERN``, ``--exclude PATTERN`` - | exclude paths matching PATTERN - ``--exclude-from EXCLUDEFILE`` - | read exclude patterns from EXCLUDEFILE, one per line - ``--pattern PATTERN`` - | experimental: include/exclude paths matching PATTERN - ``--patterns-from PATTERNFILE`` - | experimental: read include/exclude patterns from PATTERNFILE, one per line - ``--numeric-owner`` - | only obey numeric user and group identifiers - ``--strip-components NUMBER`` - | Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. - ``--stdout`` - | write all extracted data to stdout - ``--sparse`` - | create holes in output sparse file from all-zero chunks + --list output verbose list of items (files, dirs, ...) + -n, --dry-run do not actually change any files + -e PATTERN, --exclude PATTERN exclude paths matching PATTERN + --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line + --pattern PATTERN experimental: include/exclude paths matching PATTERN + --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line + --numeric-owner only obey numeric user and group identifiers + --strip-components NUMBER Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. + --stdout write all extracted data to stdout + --sparse create holes in output sparse file from all-zero chunks + :ref:`common_options` | diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index 879dee14..d23664d6 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -8,28 +8,24 @@ borg info borg [common options] info [options] REPOSITORY_OR_ARCHIVE -positional arguments - REPOSITORY_OR_ARCHIVE - archive or repository to display information about +REPOSITORY_OR_ARCHIVE + archive or repository to display information about + optional arguments - ``--json`` - | format output as JSON + --json format output as JSON + :ref:`common_options` | filters - ``-P``, ``--prefix`` - | only consider archive names starting with this prefix. - ``-a``, ``--glob-archives`` - | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. - ``--sort-by`` - | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp - ``--first N`` - | consider first N archives after other filters were applied - ``--last N`` - | consider last N archives after other filters were applied + -P, --prefix only consider archive names starting with this prefix. + -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + --first N consider first N archives after other filters were applied + --last N consider last N archives after other filters were applied + Description ~~~~~~~~~~~ diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index 88fe11c7..a959ead2 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -8,17 +8,15 @@ borg init borg [common options] init [options] REPOSITORY -positional arguments - REPOSITORY - repository to create +REPOSITORY + repository to create + optional arguments - ``-e``, ``--encryption`` - | select encryption key mode **(required)** - ``--append-only`` - | create an append-only mode repository - ``--storage-quota`` - | Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota. + -e, --encryption select encryption key mode **(required)** + --append-only create an append-only mode repository + --storage-quota Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota. + :ref:`common_options` | diff --git a/docs/usage/key_change-passphrase.rst.inc b/docs/usage/key_change-passphrase.rst.inc index 91323a04..b40ab806 100644 --- a/docs/usage/key_change-passphrase.rst.inc +++ b/docs/usage/key_change-passphrase.rst.inc @@ -8,8 +8,8 @@ borg key change-passphrase borg [common options] key change-passphrase [options] REPOSITORY -positional arguments - REPOSITORY +REPOSITORY + :ref:`common_options` diff --git a/docs/usage/key_export.rst.inc b/docs/usage/key_export.rst.inc index a0d7cf13..271d0eeb 100644 --- a/docs/usage/key_export.rst.inc +++ b/docs/usage/key_export.rst.inc @@ -8,17 +8,16 @@ borg key export borg [common options] key export [options] REPOSITORY PATH -positional arguments - REPOSITORY +REPOSITORY + +PATH + where to store the backup - PATH - where to store the backup optional arguments - ``--paper`` - | Create an export suitable for printing and later type-in - ``--qr-html`` - | Create an html file suitable for printing and later type-in or qr scan + --paper Create an export suitable for printing and later type-in + --qr-html Create an html file suitable for printing and later type-in or qr scan + :ref:`common_options` | diff --git a/docs/usage/key_import.rst.inc b/docs/usage/key_import.rst.inc index c6b9b5ea..e08ef912 100644 --- a/docs/usage/key_import.rst.inc +++ b/docs/usage/key_import.rst.inc @@ -8,15 +8,15 @@ borg key import borg [common options] key import [options] REPOSITORY PATH -positional arguments - REPOSITORY +REPOSITORY + +PATH + path to the backup - PATH - path to the backup optional arguments - ``--paper`` - | interactively import from a backup done with ``--paper`` + --paper interactively import from a backup done with ``--paper`` + :ref:`common_options` | diff --git a/docs/usage/key_migrate-to-repokey.rst.inc b/docs/usage/key_migrate-to-repokey.rst.inc index 9acf215d..948d7b8c 100644 --- a/docs/usage/key_migrate-to-repokey.rst.inc +++ b/docs/usage/key_migrate-to-repokey.rst.inc @@ -8,8 +8,8 @@ borg key migrate-to-repokey borg [common options] key migrate-to-repokey [options] REPOSITORY -positional arguments - REPOSITORY +REPOSITORY + :ref:`common_options` diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index a1fedea9..cb8e6d57 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -8,53 +8,40 @@ borg list borg [common options] list [options] REPOSITORY_OR_ARCHIVE PATH -positional arguments - REPOSITORY_OR_ARCHIVE - repository/archive to list contents of - PATH - paths to list; patterns are supported +REPOSITORY_OR_ARCHIVE + repository/archive to list contents of +PATH + paths to list; patterns are supported + optional arguments - ``--short`` - | only print file/directory names, nothing else - ``--format``, ``--list-format`` - | specify format for file listing - | (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") - ``--json`` - | Only valid for listing repository contents. Format output as JSON. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "barchive" key is therefore not available. - ``--json-lines`` - | Only valid for listing archive contents. Format output as JSON Lines. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. + --short only print file/directory names, nothing else + --format, --list-format specify format for file listing + (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") + --json Only valid for listing repository contents. Format output as JSON. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "barchive" key is therefore not available. + --json-lines Only valid for listing archive contents. Format output as JSON Lines. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. + :ref:`common_options` | filters - ``-P``, ``--prefix`` - | only consider archive names starting with this prefix. - ``-a``, ``--glob-archives`` - | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. - ``--sort-by`` - | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp - ``--first N`` - | consider first N archives after other filters were applied - ``--last N`` - | consider last N archives after other filters were applied + -P, --prefix only consider archive names starting with this prefix. + -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + --first N consider first N archives after other filters were applied + --last N consider last N archives after other filters were applied + Exclusion options - ``-e PATTERN``, ``--exclude PATTERN`` - | exclude paths matching PATTERN - ``--exclude-from EXCLUDEFILE`` - | read exclude patterns from EXCLUDEFILE, one per line - ``--exclude-caches`` - | exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) - ``--exclude-if-present NAME`` - | exclude directories that are tagged by containing a filesystem object with the given NAME - ``--keep-exclude-tags``, ``--keep-tag-files`` - | if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive - ``--pattern PATTERN`` - | experimental: include/exclude paths matching PATTERN - ``--patterns-from PATTERNFILE`` - | experimental: read include/exclude patterns from PATTERNFILE, one per line + -e PATTERN, --exclude PATTERN exclude paths matching PATTERN + --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line + --exclude-caches exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) + --exclude-if-present NAME exclude directories that are tagged by containing a filesystem object with the given NAME + --keep-exclude-tags, --keep-tag-files if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive + --pattern PATTERN experimental: include/exclude paths matching PATTERN + --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line + Description ~~~~~~~~~~~ diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index dd013838..40bbe562 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -8,32 +8,27 @@ borg mount borg [common options] mount [options] REPOSITORY_OR_ARCHIVE MOUNTPOINT -positional arguments - REPOSITORY_OR_ARCHIVE - repository/archive to mount - MOUNTPOINT - where to mount filesystem +REPOSITORY_OR_ARCHIVE + repository/archive to mount +MOUNTPOINT + where to mount filesystem + optional arguments - ``-f``, ``--foreground`` - | stay in foreground, do not daemonize - ``-o`` - | Extra mount options + -f, --foreground stay in foreground, do not daemonize + -o Extra mount options + :ref:`common_options` | filters - ``-P``, ``--prefix`` - | only consider archive names starting with this prefix. - ``-a``, ``--glob-archives`` - | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. - ``--sort-by`` - | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp - ``--first N`` - | consider first N archives after other filters were applied - ``--last N`` - | consider last N archives after other filters were applied + -P, --prefix only consider archive names starting with this prefix. + -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + --first N consider first N archives after other filters were applied + --last N consider last N archives after other filters were applied + Description ~~~~~~~~~~~ diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index e9c86051..6ded22c1 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -8,46 +8,33 @@ borg prune borg [common options] prune [options] REPOSITORY -positional arguments - REPOSITORY - repository to prune +REPOSITORY + repository to prune + optional arguments - ``-n``, ``--dry-run`` - | do not change repository - ``--force`` - | force pruning of corrupted archives - ``-s``, ``--stats`` - | print statistics for the deleted archive - ``--list`` - | output verbose list of archives it keeps/prunes - ``--keep-within WITHIN`` - | keep all archives within this time interval - ``--keep-last``, ``--keep-secondly`` - | number of secondly archives to keep - ``--keep-minutely`` - | number of minutely archives to keep - ``-H``, ``--keep-hourly`` - | number of hourly archives to keep - ``-d``, ``--keep-daily`` - | number of daily archives to keep - ``-w``, ``--keep-weekly`` - | number of weekly archives to keep - ``-m``, ``--keep-monthly`` - | number of monthly archives to keep - ``-y``, ``--keep-yearly`` - | number of yearly archives to keep - ``--save-space`` - | work slower, but using less space + -n, --dry-run do not change repository + --force force pruning of corrupted archives + -s, --stats print statistics for the deleted archive + --list output verbose list of archives it keeps/prunes + --keep-within WITHIN keep all archives within this time interval + --keep-last, --keep-secondly number of secondly archives to keep + --keep-minutely number of minutely archives to keep + -H, --keep-hourly number of hourly archives to keep + -d, --keep-daily number of daily archives to keep + -w, --keep-weekly number of weekly archives to keep + -m, --keep-monthly number of monthly archives to keep + -y, --keep-yearly number of yearly archives to keep + --save-space work slower, but using less space + :ref:`common_options` | filters - ``-P``, ``--prefix`` - | only consider archive names starting with this prefix. - ``-a``, ``--glob-archives`` - | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + -P, --prefix only consider archive names starting with this prefix. + -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + Description ~~~~~~~~~~~ diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index 26bbdb38..df43fd4b 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -8,56 +8,41 @@ borg recreate borg [common options] recreate [options] REPOSITORY_OR_ARCHIVE PATH -positional arguments - REPOSITORY_OR_ARCHIVE - repository/archive to recreate - PATH - paths to recreate; patterns are supported +REPOSITORY_OR_ARCHIVE + repository/archive to recreate +PATH + paths to recreate; patterns are supported + optional arguments - ``--list`` - | output verbose list of items (files, dirs, ...) - ``--filter STATUSCHARS`` - | only display items with the given status characters - ``-n``, ``--dry-run`` - | do not change anything - ``-s``, ``--stats`` - | print statistics at end + --list output verbose list of items (files, dirs, ...) + --filter STATUSCHARS only display items with the given status characters + -n, --dry-run do not change anything + -s, --stats print statistics at end + :ref:`common_options` | Exclusion options - ``-e PATTERN``, ``--exclude PATTERN`` - | exclude paths matching PATTERN - ``--exclude-from EXCLUDEFILE`` - | read exclude patterns from EXCLUDEFILE, one per line - ``--exclude-caches`` - | exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) - ``--exclude-if-present NAME`` - | exclude directories that are tagged by containing a filesystem object with the given NAME - ``--keep-exclude-tags``, ``--keep-tag-files`` - | if tag objects are specified with ``--exclude-if-present``, don't omit the tag objects themselves from the backup archive - ``--pattern PATTERN`` - | experimental: include/exclude paths matching PATTERN - ``--patterns-from PATTERNFILE`` - | experimental: read include/exclude patterns from PATTERNFILE, one per line + -e PATTERN, --exclude PATTERN exclude paths matching PATTERN + --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line + --exclude-caches exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) + --exclude-if-present NAME exclude directories that are tagged by containing a filesystem object with the given NAME + --keep-exclude-tags, --keep-tag-files if tag objects are specified with ``--exclude-if-present``, don't omit the tag objects themselves from the backup archive + --pattern PATTERN experimental: include/exclude paths matching PATTERN + --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line + Archive options - ``--target TARGET`` - | create a new archive with the name ARCHIVE, do not replace existing archive (only applies for a single archive) - ``-c SECONDS``, ``--checkpoint-interval SECONDS`` - | write checkpoint every SECONDS seconds (Default: 1800) - ``--comment COMMENT`` - | add a comment text to the archive - ``--timestamp TIMESTAMP`` - | manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). alternatively, give a reference file/directory. - ``-C COMPRESSION``, ``--compression COMPRESSION`` - | select compression algorithm, see the output of the "borg help compression" command for details. - ``--recompress`` - | recompress data chunks according to ``--compression`` if `if-different`. When `always`, chunks that are already compressed that way are not skipped, but compressed again. Only the algorithm is considered for `if-different`, not the compression level (if any). - ``--chunker-params PARAMS`` - | specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the current defaults. default: 19,23,21,4095 + --target TARGET create a new archive with the name ARCHIVE, do not replace existing archive (only applies for a single archive) + -c SECONDS, --checkpoint-interval SECONDS write checkpoint every SECONDS seconds (Default: 1800) + --comment COMMENT add a comment text to the archive + --timestamp TIMESTAMP manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). alternatively, give a reference file/directory. + -C COMPRESSION, --compression COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. + --recompress recompress data chunks according to ``--compression`` if `if-different`. When `always`, chunks that are already compressed that way are not skipped, but compressed again. Only the algorithm is considered for `if-different`, not the compression level (if any). + --chunker-params PARAMS specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the current defaults. default: 19,23,21,4095 + Description ~~~~~~~~~~~ diff --git a/docs/usage/rename.rst.inc b/docs/usage/rename.rst.inc index 10d4d32a..03ca4f2b 100644 --- a/docs/usage/rename.rst.inc +++ b/docs/usage/rename.rst.inc @@ -8,11 +8,11 @@ borg rename borg [common options] rename [options] ARCHIVE NEWNAME -positional arguments - ARCHIVE - archive to rename - NEWNAME - the new archive name to use +ARCHIVE + archive to rename +NEWNAME + the new archive name to use + :ref:`common_options` | diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index 2bd87e10..006045d5 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -8,15 +8,14 @@ borg serve borg [common options] serve [options] + + optional arguments - ``--restrict-to-path PATH`` - | restrict repository access to PATH. Can be specified multiple times to allow the client access to several directories. Access to all sub-directories is granted implicitly; PATH doesn't need to directly point to a repository. - ``--restrict-to-repository PATH`` - | restrict repository access. Only the repository located at PATH (no sub-directories are considered) is accessible. Can be specified multiple times to allow the client access to several repositories. Unlike --restrict-to-path sub-directories are not accessible; PATH needs to directly point at a repository location. PATH may be an empty directory or the last element of PATH may not exist, in which case the client may initialize a repository there. - ``--append-only`` - | only allow appending to repository segment files - ``--storage-quota`` - | Override storage quota of the repository (e.g. 5G, 1.5T). When a new repository is initialized, sets the storage quota on the new repository as well. Default: no quota. + --restrict-to-path PATH restrict repository access to PATH. Can be specified multiple times to allow the client access to several directories. Access to all sub-directories is granted implicitly; PATH doesn't need to directly point to a repository. + --restrict-to-repository PATH restrict repository access. Only the repository located at PATH (no sub-directories are considered) is accessible. Can be specified multiple times to allow the client access to several repositories. Unlike --restrict-to-path sub-directories are not accessible; PATH needs to directly point at a repository location. PATH may be an empty directory or the last element of PATH may not exist, in which case the client may initialize a repository there. + --append-only only allow appending to repository segment files + --storage-quota Override storage quota of the repository (e.g. 5G, 1.5T). When a new repository is initialized, sets the storage quota on the new repository as well. Default: no quota. + :ref:`common_options` | diff --git a/docs/usage/umount.rst.inc b/docs/usage/umount.rst.inc index 45d2cb97..deace69f 100644 --- a/docs/usage/umount.rst.inc +++ b/docs/usage/umount.rst.inc @@ -8,9 +8,9 @@ borg umount borg [common options] umount [options] MOUNTPOINT -positional arguments - MOUNTPOINT - mountpoint of the filesystem to umount +MOUNTPOINT + mountpoint of the filesystem to umount + :ref:`common_options` | diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index cc40e8b2..ad5ddcf3 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -8,22 +8,18 @@ borg upgrade borg [common options] upgrade [options] REPOSITORY -positional arguments - REPOSITORY - path to the repository to be upgraded +REPOSITORY + path to the repository to be upgraded + optional arguments - ``-n``, ``--dry-run`` - | do not change repository - ``--inplace`` - | rewrite repository in place, with no chance of going back to older - | versions of the repository. - ``--force`` - | Force upgrade - ``--tam`` - | Enable manifest authentication (in key and cache) (Borg 1.0.9 and later) - ``--disable-tam`` - | Disable manifest authentication (in key and cache) + -n, --dry-run do not change repository + --inplace rewrite repository in place, with no chance of going back to older + versions of the repository. + --force Force upgrade + --tam Enable manifest authentication (in key and cache) (Borg 1.0.9 and later) + --disable-tam Disable manifest authentication (in key and cache) + :ref:`common_options` | diff --git a/docs/usage/with-lock.rst.inc b/docs/usage/with-lock.rst.inc index db5523b9..3e387611 100644 --- a/docs/usage/with-lock.rst.inc +++ b/docs/usage/with-lock.rst.inc @@ -8,13 +8,13 @@ borg with-lock borg [common options] with-lock [options] REPOSITORY COMMAND ARGS -positional arguments - REPOSITORY - repository to lock - COMMAND - command to run - ARGS - command arguments +REPOSITORY + repository to lock +COMMAND + command to run +ARGS + command arguments + :ref:`common_options` | From a1a92bf00f33bde2b3af4d0e8be859215aff8d74 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Jun 2017 11:53:36 +0200 Subject: [PATCH 1111/1387] docs: less space waste following "Common options" --- docs/borg_theme/css/borg.css | 4 ++++ docs/usage/benchmark_crud.rst.inc | 4 +++- docs/usage/break-lock.rst.inc | 4 +++- docs/usage/change-passphrase.rst.inc | 4 +++- docs/usage/check.rst.inc | 4 +++- docs/usage/create.rst.inc | 4 +++- docs/usage/delete.rst.inc | 4 +++- docs/usage/diff.rst.inc | 4 +++- docs/usage/export-tar.rst.inc | 4 +++- docs/usage/extract.rst.inc | 4 +++- docs/usage/info.rst.inc | 4 +++- docs/usage/init.rst.inc | 4 +++- docs/usage/key.rst | 9 ++++----- docs/usage/key_change-passphrase.rst.inc | 4 +++- docs/usage/key_export.rst.inc | 4 +++- docs/usage/key_import.rst.inc | 4 +++- docs/usage/key_migrate-to-repokey.rst.inc | 4 +++- docs/usage/list.rst.inc | 4 +++- docs/usage/mount.rst.inc | 4 +++- docs/usage/prune.rst.inc | 4 +++- docs/usage/recreate.rst.inc | 4 +++- docs/usage/rename.rst.inc | 4 +++- docs/usage/serve.rst.inc | 4 +++- docs/usage/umount.rst.inc | 4 +++- docs/usage/upgrade.rst.inc | 4 +++- docs/usage/with-lock.rst.inc | 4 +++- setup.py | 3 +-- 27 files changed, 81 insertions(+), 31 deletions(-) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index abd7b7a1..20b3b95c 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -169,3 +169,7 @@ cite { font-style: normal; text-decoration: underline; } + +.borg-common-opt-ref { + font-weight: bold; +} diff --git a/docs/usage/benchmark_crud.rst.inc b/docs/usage/benchmark_crud.rst.inc index c0274dbe..6eb48ef6 100644 --- a/docs/usage/benchmark_crud.rst.inc +++ b/docs/usage/benchmark_crud.rst.inc @@ -14,8 +14,10 @@ PATH path were to create benchmark input data +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/break-lock.rst.inc b/docs/usage/break-lock.rst.inc index 341fa8a0..130e573e 100644 --- a/docs/usage/break-lock.rst.inc +++ b/docs/usage/break-lock.rst.inc @@ -12,8 +12,10 @@ REPOSITORY repository for which to break the locks +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/change-passphrase.rst.inc b/docs/usage/change-passphrase.rst.inc index c9cbee36..0df569a7 100644 --- a/docs/usage/change-passphrase.rst.inc +++ b/docs/usage/change-passphrase.rst.inc @@ -12,8 +12,10 @@ REPOSITORY +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index ad31b6a2..ae185a58 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -20,8 +20,10 @@ optional arguments --save-space work slower, but using less space +.. class:: borg-common-opt-ref + :ref:`common_options` - | + filters -P, --prefix only consider archive names starting with this prefix. diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 0480befb..51559157 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -23,8 +23,10 @@ optional arguments --no-cache-sync experimental: do not synchronize the cache. Implies --no-files-cache. +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Exclusion options -e PATTERN, --exclude PATTERN exclude paths matching PATTERN diff --git a/docs/usage/delete.rst.inc b/docs/usage/delete.rst.inc index 294900fe..9665be40 100644 --- a/docs/usage/delete.rst.inc +++ b/docs/usage/delete.rst.inc @@ -19,8 +19,10 @@ optional arguments --save-space work slower, but using less space +.. class:: borg-common-opt-ref + :ref:`common_options` - | + filters -P, --prefix only consider archive names starting with this prefix. diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index 1c3d7fb1..f70731cc 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -22,8 +22,10 @@ optional arguments --sort Sort the output lines by file path. +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Exclusion options -e PATTERN, --exclude PATTERN exclude paths matching PATTERN diff --git a/docs/usage/export-tar.rst.inc b/docs/usage/export-tar.rst.inc index d28bf999..f31d1255 100644 --- a/docs/usage/export-tar.rst.inc +++ b/docs/usage/export-tar.rst.inc @@ -26,8 +26,10 @@ optional arguments --strip-components NUMBER Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/extract.rst.inc b/docs/usage/extract.rst.inc index e23eecd0..3ce7c064 100644 --- a/docs/usage/extract.rst.inc +++ b/docs/usage/extract.rst.inc @@ -27,8 +27,10 @@ optional arguments --sparse create holes in output sparse file from all-zero chunks +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index d23664d6..989d380d 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -16,8 +16,10 @@ optional arguments --json format output as JSON +.. class:: borg-common-opt-ref + :ref:`common_options` - | + filters -P, --prefix only consider archive names starting with this prefix. diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index a959ead2..31e1750a 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -18,8 +18,10 @@ optional arguments --storage-quota Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota. +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/key.rst b/docs/usage/key.rst index d22db044..ac6ba86b 100644 --- a/docs/usage/key.rst +++ b/docs/usage/key.rst @@ -1,8 +1,3 @@ -.. include:: key_export.rst.inc - - -.. include:: key_import.rst.inc - .. _borg-change-passphrase: .. include:: key_change-passphrase.rst.inc @@ -40,3 +35,7 @@ Fully automated using environment variables: $ BORG_PASSPHRASE=old BORG_NEW_PASSPHRASE=new borg key change-passphrase repo # now "new" is the current passphrase. + +.. include:: key_export.rst.inc + +.. include:: key_import.rst.inc diff --git a/docs/usage/key_change-passphrase.rst.inc b/docs/usage/key_change-passphrase.rst.inc index b40ab806..b9e99df3 100644 --- a/docs/usage/key_change-passphrase.rst.inc +++ b/docs/usage/key_change-passphrase.rst.inc @@ -12,8 +12,10 @@ REPOSITORY +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/key_export.rst.inc b/docs/usage/key_export.rst.inc index 271d0eeb..e3bae821 100644 --- a/docs/usage/key_export.rst.inc +++ b/docs/usage/key_export.rst.inc @@ -19,8 +19,10 @@ optional arguments --qr-html Create an html file suitable for printing and later type-in or qr scan +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/key_import.rst.inc b/docs/usage/key_import.rst.inc index e08ef912..d08286b4 100644 --- a/docs/usage/key_import.rst.inc +++ b/docs/usage/key_import.rst.inc @@ -18,8 +18,10 @@ optional arguments --paper interactively import from a backup done with ``--paper`` +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/key_migrate-to-repokey.rst.inc b/docs/usage/key_migrate-to-repokey.rst.inc index 948d7b8c..f63aba77 100644 --- a/docs/usage/key_migrate-to-repokey.rst.inc +++ b/docs/usage/key_migrate-to-repokey.rst.inc @@ -12,8 +12,10 @@ REPOSITORY +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index cb8e6d57..802a316a 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -22,8 +22,10 @@ optional arguments --json-lines Only valid for listing archive contents. Format output as JSON Lines. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. +.. class:: borg-common-opt-ref + :ref:`common_options` - | + filters -P, --prefix only consider archive names starting with this prefix. diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index 40bbe562..91799b1a 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -19,8 +19,10 @@ optional arguments -o Extra mount options +.. class:: borg-common-opt-ref + :ref:`common_options` - | + filters -P, --prefix only consider archive names starting with this prefix. diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index 6ded22c1..4fec9a20 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -28,8 +28,10 @@ optional arguments --save-space work slower, but using less space +.. class:: borg-common-opt-ref + :ref:`common_options` - | + filters -P, --prefix only consider archive names starting with this prefix. diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index df43fd4b..8425feb0 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -21,8 +21,10 @@ optional arguments -s, --stats print statistics at end +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Exclusion options -e PATTERN, --exclude PATTERN exclude paths matching PATTERN diff --git a/docs/usage/rename.rst.inc b/docs/usage/rename.rst.inc index 03ca4f2b..7c81e2ee 100644 --- a/docs/usage/rename.rst.inc +++ b/docs/usage/rename.rst.inc @@ -14,8 +14,10 @@ NEWNAME the new archive name to use +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index 006045d5..f7ccb032 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -17,8 +17,10 @@ optional arguments --storage-quota Override storage quota of the repository (e.g. 5G, 1.5T). When a new repository is initialized, sets the storage quota on the new repository as well. Default: no quota. +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/umount.rst.inc b/docs/usage/umount.rst.inc index deace69f..f4f6da71 100644 --- a/docs/usage/umount.rst.inc +++ b/docs/usage/umount.rst.inc @@ -12,8 +12,10 @@ MOUNTPOINT mountpoint of the filesystem to umount +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index ad5ddcf3..7d13e6d8 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -21,8 +21,10 @@ optional arguments --disable-tam Disable manifest authentication (in key and cache) +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/docs/usage/with-lock.rst.inc b/docs/usage/with-lock.rst.inc index 3e387611..411b47d1 100644 --- a/docs/usage/with-lock.rst.inc +++ b/docs/usage/with-lock.rst.inc @@ -16,8 +16,10 @@ ARGS command arguments +.. class:: borg-common-opt-ref + :ref:`common_options` - | + Description ~~~~~~~~~~~ diff --git a/setup.py b/setup.py index 2b58dcdf..383e6e4b 100644 --- a/setup.py +++ b/setup.py @@ -290,8 +290,7 @@ class build_usage(Command): def write_options(self, parser, fp): for group in parser._action_groups: if group.title == 'Common options': - fp.write('\n\n:ref:`common_options`\n') - fp.write(' |') + fp.write('\n\n.. class:: borg-common-opt-ref\n\n:ref:`common_options`\n') else: self.write_options_group(group, fp) From e76fae05451d0ecb90469af8c2e9c948b60851c0 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Jun 2017 14:50:11 +0200 Subject: [PATCH 1112/1387] docs: uniform tables for listing options (HTML only) --- docs/borg_theme/css/borg.css | 29 +++++++--- setup.py | 101 ++++++++++++++++++++++++++++++++++- 2 files changed, 122 insertions(+), 8 deletions(-) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 20b3b95c..28a280ef 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -119,17 +119,34 @@ table.docutils:not(.footnote) tr:last-child td { } table.docutils:not(.footnote) tr td:first-child, -table.docutils:not(.footnote) tr th:first-child, -table.docutils.option-list tr td { +table.docutils:not(.footnote) tr th:first-child { border-left: 0; } table.docutils:not(.footnote) tr td:last-child, table.docutils:not(.footnote) tr th:last-child, -table.docutils.option-list tr td { +table.docutils.borg-options-table tr td { border-right: 0; } +table.docutils.borg-options-table tr td { + border-left: 0; + border-right: 0; +} + +table.docutils.borg-options-table tr td:first-child:not([colspan="3"]) { + border-top: 0; + border-bottom: 0; +} + +.borg-options-table td[colspan="3"] p { + margin: 0; +} + +.borg-options-table { + width: 100%; +} + kbd, /* used in usage pages for options */ code, .rst-content tt.literal, @@ -153,12 +170,12 @@ kbd { font-family: Consolas, monospace; } -kbd .option { +.borg-options-table tr td:nth-child(2) .pre { white-space: nowrap; } -table.docutils.option-list td.option-group { - min-width: 10em; +.borg-options-table tr td:first-child { + width: 2em; } cite { diff --git a/setup.py b/setup.py index 383e6e4b..f46e35e1 100644 --- a/setup.py +++ b/setup.py @@ -288,11 +288,108 @@ class build_usage(Command): fp.write('\n\n') def write_options(self, parser, fp): + def is_positional_group(group): + return any(not o.option_strings for o in group._group_actions) + + # HTML output: + # A table using some column-spans + + def html_write(s): + for line in s.splitlines(): + fp.write(' ' + line + '\n') + + rows = [] for group in parser._action_groups: if group.title == 'Common options': - fp.write('\n\n.. class:: borg-common-opt-ref\n\n:ref:`common_options`\n') + # (no of columns used, columns, ...) + rows.append((1, '.. class:: borg-common-opt-ref\n\n:ref:`common_options`')) else: - self.write_options_group(group, fp) + rows.append((1, '**%s**' % group.title)) + if is_positional_group(group): + for option in group._group_actions: + rows.append((3, '', '``%s``' % option.metavar, option.help or '')) + else: + for option in group._group_actions: + if option.metavar: + option_fmt = '``%s ' + option.metavar + '``' + else: + option_fmt = '``%s``' + option_str = ', '.join(option_fmt % s for s in option.option_strings) + option_desc = textwrap.dedent((option.help or '') % option.__dict__) + rows.append((3, '', option_str, option_desc)) + + fp.write('.. only:: html\n\n') + table = io.StringIO() + table.write('.. class:: borg-options-table\n\n') + self.rows_to_table(rows, table.write) + fp.write(textwrap.indent(table.getvalue(), ' ' * 4)) + + # LaTeX output: + # Regular rST option lists (irregular column widths) + latex_options = io.StringIO() + for group in parser._action_groups: + if group.title == 'Common options': + latex_options.write('\n\n:ref:`common_options`\n') + latex_options.write(' |') + else: + self.write_options_group(group, latex_options) + fp.write('\n.. only:: latex\n\n') + fp.write(textwrap.indent(latex_options.getvalue(), ' ' * 4)) + + def rows_to_table(self, rows, write): + def write_row_separator(): + write('+') + for column_width in column_widths: + write('-' * (column_width + 1)) + write('+') + write('\n') + + # Find column count and width + column_count = max(columns for columns, *_ in rows) + column_widths = [0] * column_count + for columns, *cells in rows: + for i in range(columns): + # "+ 1" because we want a space between the cell contents and the delimiting "|" in the output + column_widths[i] = max(column_widths[i], len(cells[i]) + 1) + + for columns, *cells in rows: + write_row_separator() + # If a cell contains newlines, then the row must be split up in individual rows + # where each cell contains no newline. + rowspanning_cells = [] + original_cells = list(cells) + while any('\n' in cell for cell in original_cells): + cell_bloc = [] + for i, cell in enumerate(original_cells): + pre, _, original_cells[i] = cell.partition('\n') + cell_bloc.append(pre) + rowspanning_cells.append(cell_bloc) + rowspanning_cells.append(original_cells) + for cells in rowspanning_cells: + for i, column_width in enumerate(column_widths): + if i < columns: + write('| ') + write(cells[i].ljust(column_width)) + else: + write(' ') + write(''.ljust(column_width)) + write('|\n') + + write_row_separator() + # This bit of JavaScript kills the that is invariably inserted by docutils, + # but does absolutely no good here. It sets bogus column widths which cannot be overridden + # with CSS alone. + # Since this is HTML-only output, it would be possible to just generate a directly, + # but then we'd lose rST formatting. + write(textwrap.dedent(""" + .. raw:: html + + + """)) def write_options_group(self, group, fp, with_title=True, base_indent=4): def is_positional_group(group): From b1747873d92eaa0412398eab1259cc0a5ccfae12 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Jun 2017 15:05:59 +0200 Subject: [PATCH 1113/1387] docs: various formatting fixes --- src/borg/archiver.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 346bb8a4..5dd1b5b1 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1881,7 +1881,7 @@ class Archiver: separator, a '\*' is appended before matching is attempted. Shell-style patterns, selector `sh:` - This is the default style for --pattern and --patterns-from. + This is the default style for ``--pattern`` and ``--patterns-from``. Like fnmatch patterns these are similar to shell patterns. The difference is that the pattern may include `**/` for matching zero or more directory levels, `*` for matching zero or more arbitrary characters with the @@ -2368,7 +2368,7 @@ class Archiver: metavar='PATH', help='restrict repository access. Only the repository located at PATH (no sub-directories are considered) ' 'is accessible. ' 'Can be specified multiple times to allow the client access to several repositories. ' - 'Unlike --restrict-to-path sub-directories are not accessible; ' + 'Unlike ``--restrict-to-path`` sub-directories are not accessible; ' 'PATH needs to directly point at a repository location. ' 'PATH may be an empty directory or the last element of PATH may not exist, in which case ' 'the client may initialize a repository there.') @@ -2803,9 +2803,9 @@ class Archiver: subparser.add_argument('--filter', dest='output_filter', metavar='STATUSCHARS', help='only display items with the given status characters') subparser.add_argument('--json', action='store_true', - help='output stats as JSON (implies --stats)') + help='output stats as JSON. Implies ``--stats``.') subparser.add_argument('--no-cache-sync', dest='no_cache_sync', action='store_true', - help='experimental: do not synchronize the cache. Implies --no-files-cache.') + help='experimental: do not synchronize the cache. Implies ``--no-files-cache``.') exclude_group = subparser.add_argument_group('Exclusion options') exclude_group.add_argument('-e', '--exclude', dest='patterns', @@ -2823,8 +2823,8 @@ class Archiver: 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', action='store_true', default=False, - help='if tag objects are specified with --exclude-if-present, don\'t omit the tag ' - 'objects themselves from the backup archive') + help='if tag objects are specified with ``--exclude-if-present``, ' + 'don\'t omit the tag objects themselves from the backup archive') exclude_group.add_argument('--pattern', action=ArgparsePatternAction, metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') @@ -3057,8 +3057,8 @@ class Archiver: 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', action='store_true', default=False, - help='if tag objects are specified with --exclude-if-present, don\'t omit the tag ' - 'objects themselves from the backup archive') + help='if tag objects are specified with ``--exclude-if-present``, ' + 'don\'t omit the tag objects themselves from the backup archive') exclude_group.add_argument('--pattern', action=ArgparsePatternAction, metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') @@ -3103,7 +3103,7 @@ class Archiver: subparser.add_argument('--force', dest='forced', action='count', default=0, help='force deletion of corrupted archives, ' - 'use --force --force in case --force does not work.') + 'use ``--force --force`` in case ``--force`` does not work.') subparser.add_argument('--save-space', dest='save_space', action='store_true', default=False, help='work slower, but using less space') @@ -3141,8 +3141,8 @@ class Archiver: action='store_true', default=False, help='only print file/directory names, nothing else') subparser.add_argument('--format', '--list-format', dest='format', type=str, - help="""specify format for file listing - (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")""") + help='specify format for file listing ' + '(default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")') subparser.add_argument('--json', action='store_true', help='Only valid for listing repository contents. Format output as JSON. ' 'The form of ``--format`` is ignored, ' @@ -3178,7 +3178,7 @@ class Archiver: 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', action='store_true', default=False, - help='if tag objects are specified with --exclude-if-present, don\'t omit the tag ' + help='if tag objects are specified with ``--exclude-if-present``, don\'t omit the tag ' 'objects themselves from the backup archive') exclude_group.add_argument('--pattern', action=ArgparsePatternAction, From e869e7dc2f88becd181bb8f0602a33341d6b1312 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Jun 2017 15:22:24 +0200 Subject: [PATCH 1114/1387] ran build_usage --- docs/usage/benchmark_crud.rst.inc | 42 ++++-- docs/usage/break-lock.rst.inc | 36 ++++- docs/usage/change-passphrase.rst.inc | 34 ++++- docs/usage/check.rst.inc | 82 +++++++++--- docs/usage/create.rst.inc | 152 +++++++++++++++++----- docs/usage/delete.rst.inc | 78 ++++++++--- docs/usage/diff.rst.inc | 94 +++++++++---- docs/usage/export-tar.rst.inc | 78 ++++++++--- docs/usage/extract.rst.inc | 84 +++++++++--- docs/usage/help.rst.inc | 2 +- docs/usage/info.rst.inc | 66 ++++++++-- docs/usage/init.rst.inc | 50 +++++-- docs/usage/key_change-passphrase.rst.inc | 34 ++++- docs/usage/key_export.rst.inc | 50 +++++-- docs/usage/key_import.rst.inc | 46 +++++-- docs/usage/key_migrate-to-repokey.rst.inc | 34 ++++- docs/usage/list.rst.inc | 117 +++++++++++++---- docs/usage/mount.rst.inc | 76 ++++++++--- docs/usage/prune.rst.inc | 102 +++++++++++---- docs/usage/recreate.rst.inc | 124 +++++++++++++----- docs/usage/rename.rst.inc | 42 ++++-- docs/usage/serve.rst.inc | 48 +++++-- docs/usage/umount.rst.inc | 36 ++++- docs/usage/upgrade.rst.inc | 61 +++++++-- docs/usage/with-lock.rst.inc | 48 +++++-- 25 files changed, 1270 insertions(+), 346 deletions(-) diff --git a/docs/usage/benchmark_crud.rst.inc b/docs/usage/benchmark_crud.rst.inc index 6eb48ef6..91a9ff42 100644 --- a/docs/usage/benchmark_crud.rst.inc +++ b/docs/usage/benchmark_crud.rst.inc @@ -8,16 +8,42 @@ borg benchmark crud borg [common options] benchmark crud [options] REPO PATH -REPO - repo to use for benchmark (must exist) -PATH - path were to create benchmark input data +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+----------+------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+----------+------------------------------------------+ + | | ``REPO`` | repo to use for benchmark (must exist) | + +-------------------------------------------------------+----------+------------------------------------------+ + | | ``PATH`` | path were to create benchmark input data | + +-------------------------------------------------------+----------+------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+----------+------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+----------+------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + REPO + repo to use for benchmark (must exist) + PATH + path were to create benchmark input data -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/break-lock.rst.inc b/docs/usage/break-lock.rst.inc index 130e573e..4ca4aa5b 100644 --- a/docs/usage/break-lock.rst.inc +++ b/docs/usage/break-lock.rst.inc @@ -8,14 +8,38 @@ borg break-lock borg [common options] break-lock [options] REPOSITORY -REPOSITORY - repository for which to break the locks +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+----------------+-----------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+----------------+-----------------------------------------+ + | | ``REPOSITORY`` | repository for which to break the locks | + +-------------------------------------------------------+----------------+-----------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+----------------+-----------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+----------------+-----------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY + repository for which to break the locks -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/change-passphrase.rst.inc b/docs/usage/change-passphrase.rst.inc index 0df569a7..5b2fd281 100644 --- a/docs/usage/change-passphrase.rst.inc +++ b/docs/usage/change-passphrase.rst.inc @@ -8,14 +8,38 @@ borg change-passphrase borg [common options] change-passphrase [options] REPOSITORY -REPOSITORY +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+----------------+--+ + | **positional arguments** | + +-------------------------------------------------------+----------------+--+ + | | ``REPOSITORY`` | | + +-------------------------------------------------------+----------------+--+ + | **optional arguments** | + +-------------------------------------------------------+----------------+--+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+----------------+--+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index ae185a58..918a3af7 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -8,29 +8,75 @@ borg check borg [common options] check [options] REPOSITORY_OR_ARCHIVE -REPOSITORY_OR_ARCHIVE - repository or archive to check consistency of +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``REPOSITORY_OR_ARCHIVE`` | repository or archive to check consistency of | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--repository-only`` | only perform repository checks | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--archives-only`` | only perform archives checks | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--verify-data`` | perform cryptographic archive data integrity verification (conflicts with ``--repository-only``) | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--repair`` | attempt to repair any inconsistencies found | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--save-space`` | work slower, but using less space | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **filters** | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-P``, ``--prefix`` | only consider archive names starting with this prefix. | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-a``, ``--glob-archives`` | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--sort-by`` | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--first N`` | consider first N archives after other filters were applied | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--last N`` | consider last N archives after other filters were applied | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY_OR_ARCHIVE + repository or archive to check consistency of -optional arguments - --repository-only only perform repository checks - --archives-only only perform archives checks - --verify-data perform cryptographic archive data integrity verification (conflicts with ``--repository-only``) - --repair attempt to repair any inconsistencies found - --save-space work slower, but using less space + optional arguments + --repository-only only perform repository checks + --archives-only only perform archives checks + --verify-data perform cryptographic archive data integrity verification (conflicts with ``--repository-only``) + --repair attempt to repair any inconsistencies found + --save-space work slower, but using less space -.. class:: borg-common-opt-ref + :ref:`common_options` + | -:ref:`common_options` - - -filters - -P, --prefix only consider archive names starting with this prefix. - -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. - --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp - --first N consider first N archives after other filters were applied - --last N consider last N archives after other filters were applied + filters + -P, --prefix only consider archive names starting with this prefix. + -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + --first N consider first N archives after other filters were applied + --last N consider last N archives after other filters were applied Description diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 51559157..00a3dfcc 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -8,51 +8,131 @@ borg create borg [common options] create [options] ARCHIVE PATH -ARCHIVE - name of archive to create (must be also a valid directory name) -PATH - paths to archive +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``ARCHIVE`` | name of archive to create (must be also a valid directory name) | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``PATH`` | paths to archive | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-n``, ``--dry-run`` | do not create a backup archive | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-s``, ``--stats`` | print statistics for the created archive | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--list`` | output verbose list of items (files, dirs, ...) | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--filter STATUSCHARS`` | only display items with the given status characters | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--json`` | output stats as JSON. Implies ``--stats``. | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--no-cache-sync`` | experimental: do not synchronize the cache. Implies ``--no-files-cache``. | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | **Exclusion options** | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-e PATTERN``, ``--exclude PATTERN`` | exclude paths matching PATTERN | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-from EXCLUDEFILE`` | read exclude patterns from EXCLUDEFILE, one per line | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-caches`` | exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-if-present NAME`` | exclude directories that are tagged by containing a filesystem object with the given NAME | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--keep-exclude-tags``, ``--keep-tag-files`` | if tag objects are specified with ``--exclude-if-present``, don't omit the tag objects themselves from the backup archive | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--pattern PATTERN`` | experimental: include/exclude paths matching PATTERN | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--patterns-from PATTERNFILE`` | experimental: read include/exclude patterns from PATTERNFILE, one per line | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | **Filesystem options** | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-x``, ``--one-file-system`` | stay in the same file system and do not store mount points of other file systems | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--numeric-owner`` | only store numeric user and group identifiers | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--noatime`` | do not store atime into archive | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--noctime`` | do not store ctime into archive | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--ignore-inode`` | ignore inode data in the file metadata cache used to detect unchanged files. | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--read-special`` | open and read block and char device files as well as FIFOs as if they were regular files. Also follows symlinks pointing to these kinds of files. | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | **Archive options** | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--comment COMMENT`` | add a comment text to the archive | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--timestamp TIMESTAMP`` | manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). alternatively, give a reference file/directory. | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-c SECONDS``, ``--checkpoint-interval SECONDS`` | write checkpoint every SECONDS seconds (Default: 1800) | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--chunker-params PARAMS`` | specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: 19,23,21,4095 | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the "borg help compression" command for details. | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + ARCHIVE + name of archive to create (must be also a valid directory name) + PATH + paths to archive -optional arguments - -n, --dry-run do not create a backup archive - -s, --stats print statistics for the created archive - --list output verbose list of items (files, dirs, ...) - --filter STATUSCHARS only display items with the given status characters - --json output stats as JSON (implies --stats) - --no-cache-sync experimental: do not synchronize the cache. Implies --no-files-cache. + optional arguments + -n, --dry-run do not create a backup archive + -s, --stats print statistics for the created archive + --list output verbose list of items (files, dirs, ...) + --filter STATUSCHARS only display items with the given status characters + --json output stats as JSON. Implies ``--stats``. + --no-cache-sync experimental: do not synchronize the cache. Implies ``--no-files-cache``. -.. class:: borg-common-opt-ref + :ref:`common_options` + | -:ref:`common_options` + Exclusion options + -e PATTERN, --exclude PATTERN exclude paths matching PATTERN + --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line + --exclude-caches exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) + --exclude-if-present NAME exclude directories that are tagged by containing a filesystem object with the given NAME + --keep-exclude-tags, --keep-tag-files if tag objects are specified with ``--exclude-if-present``, don't omit the tag objects themselves from the backup archive + --pattern PATTERN experimental: include/exclude paths matching PATTERN + --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line -Exclusion options - -e PATTERN, --exclude PATTERN exclude paths matching PATTERN - --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line - --exclude-caches exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) - --exclude-if-present NAME exclude directories that are tagged by containing a filesystem object with the given NAME - --keep-exclude-tags, --keep-tag-files if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive - --pattern PATTERN experimental: include/exclude paths matching PATTERN - --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line + Filesystem options + -x, --one-file-system stay in the same file system and do not store mount points of other file systems + --numeric-owner only store numeric user and group identifiers + --noatime do not store atime into archive + --noctime do not store ctime into archive + --ignore-inode ignore inode data in the file metadata cache used to detect unchanged files. + --read-special open and read block and char device files as well as FIFOs as if they were regular files. Also follows symlinks pointing to these kinds of files. -Filesystem options - -x, --one-file-system stay in the same file system and do not store mount points of other file systems - --numeric-owner only store numeric user and group identifiers - --noatime do not store atime into archive - --noctime do not store ctime into archive - --ignore-inode ignore inode data in the file metadata cache used to detect unchanged files. - --read-special open and read block and char device files as well as FIFOs as if they were regular files. Also follows symlinks pointing to these kinds of files. - - -Archive options - --comment COMMENT add a comment text to the archive - --timestamp TIMESTAMP manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). alternatively, give a reference file/directory. - -c SECONDS, --checkpoint-interval SECONDS write checkpoint every SECONDS seconds (Default: 1800) - --chunker-params PARAMS specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: 19,23,21,4095 - -C COMPRESSION, --compression COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. + Archive options + --comment COMMENT add a comment text to the archive + --timestamp TIMESTAMP manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). alternatively, give a reference file/directory. + -c SECONDS, --checkpoint-interval SECONDS write checkpoint every SECONDS seconds (Default: 1800) + --chunker-params PARAMS specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: 19,23,21,4095 + -C COMPRESSION, --compression COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. Description diff --git a/docs/usage/delete.rst.inc b/docs/usage/delete.rst.inc index 9665be40..96d09796 100644 --- a/docs/usage/delete.rst.inc +++ b/docs/usage/delete.rst.inc @@ -8,28 +8,72 @@ borg delete borg [common options] delete [options] TARGET -TARGET - archive or repository to delete +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``TARGET`` | archive or repository to delete | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-s``, ``--stats`` | print statistics for the deleted archive | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-c``, ``--cache-only`` | delete only the local cache for the given repository | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--force`` | force deletion of corrupted archives, use ``--force --force`` in case ``--force`` does not work. | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--save-space`` | work slower, but using less space | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **filters** | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-P``, ``--prefix`` | only consider archive names starting with this prefix. | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-a``, ``--glob-archives`` | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--sort-by`` | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--first N`` | consider first N archives after other filters were applied | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--last N`` | consider last N archives after other filters were applied | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + TARGET + archive or repository to delete -optional arguments - -s, --stats print statistics for the deleted archive - -c, --cache-only delete only the local cache for the given repository - --force force deletion of corrupted archives, use --force --force in case --force does not work. - --save-space work slower, but using less space + optional arguments + -s, --stats print statistics for the deleted archive + -c, --cache-only delete only the local cache for the given repository + --force force deletion of corrupted archives, use ``--force --force`` in case ``--force`` does not work. + --save-space work slower, but using less space -.. class:: borg-common-opt-ref + :ref:`common_options` + | -:ref:`common_options` - - -filters - -P, --prefix only consider archive names starting with this prefix. - -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. - --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp - --first N consider first N archives after other filters were applied - --last N consider last N archives after other filters were applied + filters + -P, --prefix only consider archive names starting with this prefix. + -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + --first N consider first N archives after other filters were applied + --last N consider last N archives after other filters were applied Description diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index f70731cc..f1d4aef6 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -8,33 +8,83 @@ borg diff borg [common options] diff [options] REPO_ARCHIVE1 ARCHIVE2 PATH -REPO_ARCHIVE1 - repository location and ARCHIVE1 name -ARCHIVE2 - ARCHIVE2 name (no repository location allowed) -PATH - paths of items inside the archives to compare; patterns are supported +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | | ``REPO_ARCHIVE1`` | repository location and ARCHIVE1 name | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | | ``ARCHIVE2`` | ARCHIVE2 name (no repository location allowed) | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | | ``PATH`` | paths of items inside the archives to compare; patterns are supported | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | | ``--numeric-owner`` | only consider numeric user and group identifiers | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | | ``--same-chunker-params`` | Override check of chunker parameters. | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | | ``--sort`` | Sort the output lines by file path. | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | **Exclusion options** | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | | ``-e PATTERN``, ``--exclude PATTERN`` | exclude paths matching PATTERN | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-from EXCLUDEFILE`` | read exclude patterns from EXCLUDEFILE, one per line | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-caches`` | exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-if-present NAME`` | exclude directories that are tagged by containing a filesystem object with the given NAME | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | | ``--keep-exclude-tags``, ``--keep-tag-files`` | if tag objects are specified with ``--exclude-if-present``, don't omit the tag objects themselves from the backup archive | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | | ``--pattern PATTERN`` | experimental: include/exclude paths matching PATTERN | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | | ``--patterns-from PATTERNFILE`` | experimental: read include/exclude patterns from PATTERNFILE, one per line | + +-------------------------------------------------------+-----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + REPO_ARCHIVE1 + repository location and ARCHIVE1 name + ARCHIVE2 + ARCHIVE2 name (no repository location allowed) + PATH + paths of items inside the archives to compare; patterns are supported -optional arguments - --numeric-owner only consider numeric user and group identifiers - --same-chunker-params Override check of chunker parameters. - --sort Sort the output lines by file path. + optional arguments + --numeric-owner only consider numeric user and group identifiers + --same-chunker-params Override check of chunker parameters. + --sort Sort the output lines by file path. -.. class:: borg-common-opt-ref + :ref:`common_options` + | -:ref:`common_options` - - -Exclusion options - -e PATTERN, --exclude PATTERN exclude paths matching PATTERN - --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line - --exclude-caches exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) - --exclude-if-present NAME exclude directories that are tagged by containing a filesystem object with the given NAME - --keep-exclude-tags, --keep-tag-files if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive - --pattern PATTERN experimental: include/exclude paths matching PATTERN - --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line + Exclusion options + -e PATTERN, --exclude PATTERN exclude paths matching PATTERN + --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line + --exclude-caches exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) + --exclude-if-present NAME exclude directories that are tagged by containing a filesystem object with the given NAME + --keep-exclude-tags, --keep-tag-files if tag objects are specified with ``--exclude-if-present``, don't omit the tag objects themselves from the backup archive + --pattern PATTERN experimental: include/exclude paths matching PATTERN + --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line Description diff --git a/docs/usage/export-tar.rst.inc b/docs/usage/export-tar.rst.inc index f31d1255..0144e43f 100644 --- a/docs/usage/export-tar.rst.inc +++ b/docs/usage/export-tar.rst.inc @@ -8,28 +8,70 @@ borg export-tar borg [common options] export-tar [options] ARCHIVE FILE PATH -ARCHIVE - archive to export -FILE - output tar file. "-" to write to stdout instead. -PATH - paths to extract; patterns are supported +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``ARCHIVE`` | archive to export | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``FILE`` | output tar file. "-" to write to stdout instead. | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``PATH`` | paths to extract; patterns are supported | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--tar-filter`` | filter program to pipe data through | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--list`` | output verbose list of items (files, dirs, ...) | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``-e PATTERN``, ``--exclude PATTERN`` | exclude paths matching PATTERN | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-from EXCLUDEFILE`` | read exclude patterns from EXCLUDEFILE, one per line | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--pattern PATTERN`` | experimental: include/exclude paths matching PATTERN | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--patterns-from PATTERNFILE`` | experimental: read include/exclude patterns from PATTERNFILE, one per line | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--strip-components NUMBER`` | Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + ARCHIVE + archive to export + FILE + output tar file. "-" to write to stdout instead. + PATH + paths to extract; patterns are supported -optional arguments - --tar-filter filter program to pipe data through - --list output verbose list of items (files, dirs, ...) - -e PATTERN, --exclude PATTERN exclude paths matching PATTERN - --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line - --pattern PATTERN experimental: include/exclude paths matching PATTERN - --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line - --strip-components NUMBER Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. + optional arguments + --tar-filter filter program to pipe data through + --list output verbose list of items (files, dirs, ...) + -e PATTERN, --exclude PATTERN exclude paths matching PATTERN + --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line + --pattern PATTERN experimental: include/exclude paths matching PATTERN + --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line + --strip-components NUMBER Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/extract.rst.inc b/docs/usage/extract.rst.inc index 3ce7c064..2cffeb4f 100644 --- a/docs/usage/extract.rst.inc +++ b/docs/usage/extract.rst.inc @@ -8,29 +8,75 @@ borg extract borg [common options] extract [options] ARCHIVE PATH -ARCHIVE - archive to extract -PATH - paths to extract; patterns are supported +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``ARCHIVE`` | archive to extract | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``PATH`` | paths to extract; patterns are supported | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--list`` | output verbose list of items (files, dirs, ...) | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``-n``, ``--dry-run`` | do not actually change any files | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``-e PATTERN``, ``--exclude PATTERN`` | exclude paths matching PATTERN | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-from EXCLUDEFILE`` | read exclude patterns from EXCLUDEFILE, one per line | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--pattern PATTERN`` | experimental: include/exclude paths matching PATTERN | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--patterns-from PATTERNFILE`` | experimental: read include/exclude patterns from PATTERNFILE, one per line | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--numeric-owner`` | only obey numeric user and group identifiers | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--strip-components NUMBER`` | Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--stdout`` | write all extracted data to stdout | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | | ``--sparse`` | create holes in output sparse file from all-zero chunks | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+---------------------------------------+---------------------------------------------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + ARCHIVE + archive to extract + PATH + paths to extract; patterns are supported -optional arguments - --list output verbose list of items (files, dirs, ...) - -n, --dry-run do not actually change any files - -e PATTERN, --exclude PATTERN exclude paths matching PATTERN - --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line - --pattern PATTERN experimental: include/exclude paths matching PATTERN - --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line - --numeric-owner only obey numeric user and group identifiers - --strip-components NUMBER Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. - --stdout write all extracted data to stdout - --sparse create holes in output sparse file from all-zero chunks + optional arguments + --list output verbose list of items (files, dirs, ...) + -n, --dry-run do not actually change any files + -e PATTERN, --exclude PATTERN exclude paths matching PATTERN + --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line + --pattern PATTERN experimental: include/exclude paths matching PATTERN + --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line + --numeric-owner only obey numeric user and group identifiers + --strip-components NUMBER Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped. + --stdout write all extracted data to stdout + --sparse create holes in output sparse file from all-zero chunks -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/help.rst.inc b/docs/usage/help.rst.inc index 87fbed52..152e01b5 100644 --- a/docs/usage/help.rst.inc +++ b/docs/usage/help.rst.inc @@ -32,7 +32,7 @@ two alphanumeric characters followed by a colon (i.e. `aa:something/*`). separator, a '\*' is appended before matching is attempted. Shell-style patterns, selector `sh:` - This is the default style for --pattern and --patterns-from. + This is the default style for ``--pattern`` and ``--patterns-from``. Like fnmatch patterns these are similar to shell patterns. The difference is that the pattern may include `**/` for matching zero or more directory levels, `*` for matching zero or more arbitrary characters with the diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index 989d380d..cd1bfb9a 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -8,25 +8,63 @@ borg info borg [common options] info [options] REPOSITORY_OR_ARCHIVE -REPOSITORY_OR_ARCHIVE - archive or repository to display information about +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``REPOSITORY_OR_ARCHIVE`` | archive or repository to display information about | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--json`` | format output as JSON | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **filters** | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-P``, ``--prefix`` | only consider archive names starting with this prefix. | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-a``, ``--glob-archives`` | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--sort-by`` | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--first N`` | consider first N archives after other filters were applied | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--last N`` | consider last N archives after other filters were applied | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY_OR_ARCHIVE + archive or repository to display information about -optional arguments - --json format output as JSON + optional arguments + --json format output as JSON -.. class:: borg-common-opt-ref + :ref:`common_options` + | -:ref:`common_options` - - -filters - -P, --prefix only consider archive names starting with this prefix. - -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. - --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp - --first N consider first N archives after other filters were applied - --last N consider last N archives after other filters were applied + filters + -P, --prefix only consider archive names starting with this prefix. + -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + --first N consider first N archives after other filters were applied + --last N consider last N archives after other filters were applied Description diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index 31e1750a..efadf949 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -8,20 +8,50 @@ borg init borg [common options] init [options] REPOSITORY -REPOSITORY - repository to create +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+--------------------------+-----------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+--------------------------+-----------------------------------------------------------------------------+ + | | ``REPOSITORY`` | repository to create | + +-------------------------------------------------------+--------------------------+-----------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+--------------------------+-----------------------------------------------------------------------------+ + | | ``-e``, ``--encryption`` | select encryption key mode **(required)** | + +-------------------------------------------------------+--------------------------+-----------------------------------------------------------------------------+ + | | ``--append-only`` | create an append-only mode repository | + +-------------------------------------------------------+--------------------------+-----------------------------------------------------------------------------+ + | | ``--storage-quota`` | Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota. | + +-------------------------------------------------------+--------------------------+-----------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+--------------------------+-----------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY + repository to create -optional arguments - -e, --encryption select encryption key mode **(required)** - --append-only create an append-only mode repository - --storage-quota Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota. + optional arguments + -e, --encryption select encryption key mode **(required)** + --append-only create an append-only mode repository + --storage-quota Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota. -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/key_change-passphrase.rst.inc b/docs/usage/key_change-passphrase.rst.inc index b9e99df3..eef22560 100644 --- a/docs/usage/key_change-passphrase.rst.inc +++ b/docs/usage/key_change-passphrase.rst.inc @@ -8,14 +8,38 @@ borg key change-passphrase borg [common options] key change-passphrase [options] REPOSITORY -REPOSITORY +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+----------------+--+ + | **positional arguments** | + +-------------------------------------------------------+----------------+--+ + | | ``REPOSITORY`` | | + +-------------------------------------------------------+----------------+--+ + | **optional arguments** | + +-------------------------------------------------------+----------------+--+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+----------------+--+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/key_export.rst.inc b/docs/usage/key_export.rst.inc index e3bae821..ab1a78b1 100644 --- a/docs/usage/key_export.rst.inc +++ b/docs/usage/key_export.rst.inc @@ -8,21 +8,51 @@ borg key export borg [common options] key export [options] REPOSITORY PATH -REPOSITORY +.. only:: html -PATH - where to store the backup + .. class:: borg-options-table + + +-------------------------------------------------------+----------------+------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+----------------+------------------------------------------------------------------------+ + | | ``REPOSITORY`` | | + +-------------------------------------------------------+----------------+------------------------------------------------------------------------+ + | | ``PATH`` | where to store the backup | + +-------------------------------------------------------+----------------+------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+----------------+------------------------------------------------------------------------+ + | | ``--paper`` | Create an export suitable for printing and later type-in | + +-------------------------------------------------------+----------------+------------------------------------------------------------------------+ + | | ``--qr-html`` | Create an html file suitable for printing and later type-in or qr scan | + +-------------------------------------------------------+----------------+------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+----------------+------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY + + PATH + where to store the backup -optional arguments - --paper Create an export suitable for printing and later type-in - --qr-html Create an html file suitable for printing and later type-in or qr scan + optional arguments + --paper Create an export suitable for printing and later type-in + --qr-html Create an html file suitable for printing and later type-in or qr scan -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/key_import.rst.inc b/docs/usage/key_import.rst.inc index d08286b4..88ee1516 100644 --- a/docs/usage/key_import.rst.inc +++ b/docs/usage/key_import.rst.inc @@ -8,20 +8,48 @@ borg key import borg [common options] key import [options] REPOSITORY PATH -REPOSITORY +.. only:: html -PATH - path to the backup + .. class:: borg-options-table + + +-------------------------------------------------------+----------------+----------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+----------------+----------------------------------------------------------+ + | | ``REPOSITORY`` | | + +-------------------------------------------------------+----------------+----------------------------------------------------------+ + | | ``PATH`` | path to the backup | + +-------------------------------------------------------+----------------+----------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+----------------+----------------------------------------------------------+ + | | ``--paper`` | interactively import from a backup done with ``--paper`` | + +-------------------------------------------------------+----------------+----------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+----------------+----------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY + + PATH + path to the backup -optional arguments - --paper interactively import from a backup done with ``--paper`` + optional arguments + --paper interactively import from a backup done with ``--paper`` -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/key_migrate-to-repokey.rst.inc b/docs/usage/key_migrate-to-repokey.rst.inc index f63aba77..38cf78cf 100644 --- a/docs/usage/key_migrate-to-repokey.rst.inc +++ b/docs/usage/key_migrate-to-repokey.rst.inc @@ -8,14 +8,38 @@ borg key migrate-to-repokey borg [common options] key migrate-to-repokey [options] REPOSITORY -REPOSITORY +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+----------------+--+ + | **positional arguments** | + +-------------------------------------------------------+----------------+--+ + | | ``REPOSITORY`` | | + +-------------------------------------------------------+----------------+--+ + | **optional arguments** | + +-------------------------------------------------------+----------------+--+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+----------------+--+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index 802a316a..c6fe3e4a 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -8,41 +8,102 @@ borg list borg [common options] list [options] REPOSITORY_OR_ARCHIVE PATH -REPOSITORY_OR_ARCHIVE - repository/archive to list contents of -PATH - paths to list; patterns are supported +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``REPOSITORY_OR_ARCHIVE`` | repository/archive to list contents of | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``PATH`` | paths to list; patterns are supported | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--short`` | only print file/directory names, nothing else | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--format``, ``--list-format`` | specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--json`` | Only valid for listing repository contents. Format output as JSON. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "barchive" key is therefore not available. | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--json-lines`` | Only valid for listing archive contents. Format output as JSON Lines. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **filters** | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-P``, ``--prefix`` | only consider archive names starting with this prefix. | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-a``, ``--glob-archives`` | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--sort-by`` | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--first N`` | consider first N archives after other filters were applied | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--last N`` | consider last N archives after other filters were applied | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **Exclusion options** | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-e PATTERN``, ``--exclude PATTERN`` | exclude paths matching PATTERN | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-from EXCLUDEFILE`` | read exclude patterns from EXCLUDEFILE, one per line | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-caches`` | exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-if-present NAME`` | exclude directories that are tagged by containing a filesystem object with the given NAME | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--keep-exclude-tags``, ``--keep-tag-files`` | if tag objects are specified with ``--exclude-if-present``, don't omit the tag objects themselves from the backup archive | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--pattern PATTERN`` | experimental: include/exclude paths matching PATTERN | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--patterns-from PATTERNFILE`` | experimental: read include/exclude patterns from PATTERNFILE, one per line | + +-------------------------------------------------------+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY_OR_ARCHIVE + repository/archive to list contents of + PATH + paths to list; patterns are supported -optional arguments - --short only print file/directory names, nothing else - --format, --list-format specify format for file listing - (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") - --json Only valid for listing repository contents. Format output as JSON. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "barchive" key is therefore not available. - --json-lines Only valid for listing archive contents. Format output as JSON Lines. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. + optional arguments + --short only print file/directory names, nothing else + --format, --list-format specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}") + --json Only valid for listing repository contents. Format output as JSON. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "barchive" key is therefore not available. + --json-lines Only valid for listing archive contents. Format output as JSON Lines. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. A "bpath" key is therefore not available. -.. class:: borg-common-opt-ref + :ref:`common_options` + | -:ref:`common_options` + filters + -P, --prefix only consider archive names starting with this prefix. + -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + --first N consider first N archives after other filters were applied + --last N consider last N archives after other filters were applied -filters - -P, --prefix only consider archive names starting with this prefix. - -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. - --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp - --first N consider first N archives after other filters were applied - --last N consider last N archives after other filters were applied - - -Exclusion options - -e PATTERN, --exclude PATTERN exclude paths matching PATTERN - --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line - --exclude-caches exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) - --exclude-if-present NAME exclude directories that are tagged by containing a filesystem object with the given NAME - --keep-exclude-tags, --keep-tag-files if tag objects are specified with --exclude-if-present, don't omit the tag objects themselves from the backup archive - --pattern PATTERN experimental: include/exclude paths matching PATTERN - --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line + Exclusion options + -e PATTERN, --exclude PATTERN exclude paths matching PATTERN + --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line + --exclude-caches exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) + --exclude-if-present NAME exclude directories that are tagged by containing a filesystem object with the given NAME + --keep-exclude-tags, --keep-tag-files if tag objects are specified with ``--exclude-if-present``, don't omit the tag objects themselves from the backup archive + --pattern PATTERN experimental: include/exclude paths matching PATTERN + --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line Description diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index 91799b1a..388f445d 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -8,28 +8,70 @@ borg mount borg [common options] mount [options] REPOSITORY_OR_ARCHIVE MOUNTPOINT -REPOSITORY_OR_ARCHIVE - repository/archive to mount -MOUNTPOINT - where to mount filesystem +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``REPOSITORY_OR_ARCHIVE`` | repository/archive to mount | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``MOUNTPOINT`` | where to mount filesystem | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-f``, ``--foreground`` | stay in foreground, do not daemonize | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-o`` | Extra mount options | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **filters** | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-P``, ``--prefix`` | only consider archive names starting with this prefix. | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-a``, ``--glob-archives`` | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--sort-by`` | Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--first N`` | consider first N archives after other filters were applied | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--last N`` | consider last N archives after other filters were applied | + +-------------------------------------------------------+-----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY_OR_ARCHIVE + repository/archive to mount + MOUNTPOINT + where to mount filesystem -optional arguments - -f, --foreground stay in foreground, do not daemonize - -o Extra mount options + optional arguments + -f, --foreground stay in foreground, do not daemonize + -o Extra mount options -.. class:: borg-common-opt-ref + :ref:`common_options` + | -:ref:`common_options` - - -filters - -P, --prefix only consider archive names starting with this prefix. - -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. - --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp - --first N consider first N archives after other filters were applied - --last N consider last N archives after other filters were applied + filters + -P, --prefix only consider archive names starting with this prefix. + -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + --sort-by Comma-separated list of sorting keys; valid keys are: timestamp, name, id; default is: timestamp + --first N consider first N archives after other filters were applied + --last N consider last N archives after other filters were applied Description diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index 4fec9a20..bcd0b22a 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -8,34 +8,90 @@ borg prune borg [common options] prune [options] REPOSITORY -REPOSITORY - repository to prune +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``REPOSITORY`` | repository to prune | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-n``, ``--dry-run`` | do not change repository | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--force`` | force pruning of corrupted archives | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-s``, ``--stats`` | print statistics for the deleted archive | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--list`` | output verbose list of archives it keeps/prunes | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--keep-within WITHIN`` | keep all archives within this time interval | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--keep-last``, ``--keep-secondly`` | number of secondly archives to keep | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--keep-minutely`` | number of minutely archives to keep | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-H``, ``--keep-hourly`` | number of hourly archives to keep | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-d``, ``--keep-daily`` | number of daily archives to keep | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-w``, ``--keep-weekly`` | number of weekly archives to keep | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-m``, ``--keep-monthly`` | number of monthly archives to keep | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-y``, ``--keep-yearly`` | number of yearly archives to keep | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--save-space`` | work slower, but using less space | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **filters** | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-P``, ``--prefix`` | only consider archive names starting with this prefix. | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-a``, ``--glob-archives`` | only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. | + +-------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY + repository to prune -optional arguments - -n, --dry-run do not change repository - --force force pruning of corrupted archives - -s, --stats print statistics for the deleted archive - --list output verbose list of archives it keeps/prunes - --keep-within WITHIN keep all archives within this time interval - --keep-last, --keep-secondly number of secondly archives to keep - --keep-minutely number of minutely archives to keep - -H, --keep-hourly number of hourly archives to keep - -d, --keep-daily number of daily archives to keep - -w, --keep-weekly number of weekly archives to keep - -m, --keep-monthly number of monthly archives to keep - -y, --keep-yearly number of yearly archives to keep - --save-space work slower, but using less space + optional arguments + -n, --dry-run do not change repository + --force force pruning of corrupted archives + -s, --stats print statistics for the deleted archive + --list output verbose list of archives it keeps/prunes + --keep-within WITHIN keep all archives within this time interval + --keep-last, --keep-secondly number of secondly archives to keep + --keep-minutely number of minutely archives to keep + -H, --keep-hourly number of hourly archives to keep + -d, --keep-daily number of daily archives to keep + -w, --keep-weekly number of weekly archives to keep + -m, --keep-monthly number of monthly archives to keep + -y, --keep-yearly number of yearly archives to keep + --save-space work slower, but using less space -.. class:: borg-common-opt-ref + :ref:`common_options` + | -:ref:`common_options` - - -filters - -P, --prefix only consider archive names starting with this prefix. - -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. + filters + -P, --prefix only consider archive names starting with this prefix. + -a, --glob-archives only consider archive names matching the glob. sh: rules apply, see "borg help patterns". ``--prefix`` and ``--glob-archives`` are mutually exclusive. Description diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index 8425feb0..cfbb6df2 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -8,42 +8,108 @@ borg recreate borg [common options] recreate [options] REPOSITORY_OR_ARCHIVE PATH -REPOSITORY_OR_ARCHIVE - repository/archive to recreate -PATH - paths to recreate; patterns are supported +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``REPOSITORY_OR_ARCHIVE`` | repository/archive to recreate | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``PATH`` | paths to recreate; patterns are supported | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--list`` | output verbose list of items (files, dirs, ...) | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--filter STATUSCHARS`` | only display items with the given status characters | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-n``, ``--dry-run`` | do not change anything | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-s``, ``--stats`` | print statistics at end | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **Exclusion options** | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-e PATTERN``, ``--exclude PATTERN`` | exclude paths matching PATTERN | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-from EXCLUDEFILE`` | read exclude patterns from EXCLUDEFILE, one per line | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-caches`` | exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--exclude-if-present NAME`` | exclude directories that are tagged by containing a filesystem object with the given NAME | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--keep-exclude-tags``, ``--keep-tag-files`` | if tag objects are specified with ``--exclude-if-present``, don't omit the tag objects themselves from the backup archive | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--pattern PATTERN`` | experimental: include/exclude paths matching PATTERN | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--patterns-from PATTERNFILE`` | experimental: read include/exclude patterns from PATTERNFILE, one per line | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **Archive options** | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--target TARGET`` | create a new archive with the name ARCHIVE, do not replace existing archive (only applies for a single archive) | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-c SECONDS``, ``--checkpoint-interval SECONDS`` | write checkpoint every SECONDS seconds (Default: 1800) | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--comment COMMENT`` | add a comment text to the archive | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--timestamp TIMESTAMP`` | manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). alternatively, give a reference file/directory. | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the "borg help compression" command for details. | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--recompress`` | recompress data chunks according to ``--compression`` if `if-different`. When `always`, chunks that are already compressed that way are not skipped, but compressed again. Only the algorithm is considered for `if-different`, not the compression level (if any). | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--chunker-params PARAMS`` | specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the current defaults. default: 19,23,21,4095 | + +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY_OR_ARCHIVE + repository/archive to recreate + PATH + paths to recreate; patterns are supported -optional arguments - --list output verbose list of items (files, dirs, ...) - --filter STATUSCHARS only display items with the given status characters - -n, --dry-run do not change anything - -s, --stats print statistics at end + optional arguments + --list output verbose list of items (files, dirs, ...) + --filter STATUSCHARS only display items with the given status characters + -n, --dry-run do not change anything + -s, --stats print statistics at end -.. class:: borg-common-opt-ref + :ref:`common_options` + | -:ref:`common_options` + Exclusion options + -e PATTERN, --exclude PATTERN exclude paths matching PATTERN + --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line + --exclude-caches exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) + --exclude-if-present NAME exclude directories that are tagged by containing a filesystem object with the given NAME + --keep-exclude-tags, --keep-tag-files if tag objects are specified with ``--exclude-if-present``, don't omit the tag objects themselves from the backup archive + --pattern PATTERN experimental: include/exclude paths matching PATTERN + --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line -Exclusion options - -e PATTERN, --exclude PATTERN exclude paths matching PATTERN - --exclude-from EXCLUDEFILE read exclude patterns from EXCLUDEFILE, one per line - --exclude-caches exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html) - --exclude-if-present NAME exclude directories that are tagged by containing a filesystem object with the given NAME - --keep-exclude-tags, --keep-tag-files if tag objects are specified with ``--exclude-if-present``, don't omit the tag objects themselves from the backup archive - --pattern PATTERN experimental: include/exclude paths matching PATTERN - --patterns-from PATTERNFILE experimental: read include/exclude patterns from PATTERNFILE, one per line - - -Archive options - --target TARGET create a new archive with the name ARCHIVE, do not replace existing archive (only applies for a single archive) - -c SECONDS, --checkpoint-interval SECONDS write checkpoint every SECONDS seconds (Default: 1800) - --comment COMMENT add a comment text to the archive - --timestamp TIMESTAMP manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). alternatively, give a reference file/directory. - -C COMPRESSION, --compression COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. - --recompress recompress data chunks according to ``--compression`` if `if-different`. When `always`, chunks that are already compressed that way are not skipped, but compressed again. Only the algorithm is considered for `if-different`, not the compression level (if any). - --chunker-params PARAMS specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the current defaults. default: 19,23,21,4095 + Archive options + --target TARGET create a new archive with the name ARCHIVE, do not replace existing archive (only applies for a single archive) + -c SECONDS, --checkpoint-interval SECONDS write checkpoint every SECONDS seconds (Default: 1800) + --comment COMMENT add a comment text to the archive + --timestamp TIMESTAMP manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). alternatively, give a reference file/directory. + -C COMPRESSION, --compression COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. + --recompress recompress data chunks according to ``--compression`` if `if-different`. When `always`, chunks that are already compressed that way are not skipped, but compressed again. Only the algorithm is considered for `if-different`, not the compression level (if any). + --chunker-params PARAMS specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the current defaults. default: 19,23,21,4095 Description diff --git a/docs/usage/rename.rst.inc b/docs/usage/rename.rst.inc index 7c81e2ee..307b35d1 100644 --- a/docs/usage/rename.rst.inc +++ b/docs/usage/rename.rst.inc @@ -8,16 +8,42 @@ borg rename borg [common options] rename [options] ARCHIVE NEWNAME -ARCHIVE - archive to rename -NEWNAME - the new archive name to use +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+-------------+-----------------------------+ + | **positional arguments** | + +-------------------------------------------------------+-------------+-----------------------------+ + | | ``ARCHIVE`` | archive to rename | + +-------------------------------------------------------+-------------+-----------------------------+ + | | ``NEWNAME`` | the new archive name to use | + +-------------------------------------------------------+-------------+-----------------------------+ + | **optional arguments** | + +-------------------------------------------------------+-------------+-----------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+-------------+-----------------------------+ + + .. raw:: html + + + +.. only:: latex + + ARCHIVE + archive to rename + NEWNAME + the new archive name to use -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index f7ccb032..a4e22ac3 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -8,19 +8,49 @@ borg serve borg [common options] serve [options] +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--restrict-to-path PATH`` | restrict repository access to PATH. Can be specified multiple times to allow the client access to several directories. Access to all sub-directories is granted implicitly; PATH doesn't need to directly point to a repository. | + +-------------------------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--restrict-to-repository PATH`` | restrict repository access. Only the repository located at PATH (no sub-directories are considered) is accessible. Can be specified multiple times to allow the client access to several repositories. Unlike ``--restrict-to-path`` sub-directories are not accessible; PATH needs to directly point at a repository location. PATH may be an empty directory or the last element of PATH may not exist, in which case the client may initialize a repository there. | + +-------------------------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--append-only`` | only allow appending to repository segment files | + +-------------------------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--storage-quota`` | Override storage quota of the repository (e.g. 5G, 1.5T). When a new repository is initialized, sets the storage quota on the new repository as well. Default: no quota. | + +-------------------------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex -optional arguments - --restrict-to-path PATH restrict repository access to PATH. Can be specified multiple times to allow the client access to several directories. Access to all sub-directories is granted implicitly; PATH doesn't need to directly point to a repository. - --restrict-to-repository PATH restrict repository access. Only the repository located at PATH (no sub-directories are considered) is accessible. Can be specified multiple times to allow the client access to several repositories. Unlike --restrict-to-path sub-directories are not accessible; PATH needs to directly point at a repository location. PATH may be an empty directory or the last element of PATH may not exist, in which case the client may initialize a repository there. - --append-only only allow appending to repository segment files - --storage-quota Override storage quota of the repository (e.g. 5G, 1.5T). When a new repository is initialized, sets the storage quota on the new repository as well. Default: no quota. + + optional arguments + --restrict-to-path PATH restrict repository access to PATH. Can be specified multiple times to allow the client access to several directories. Access to all sub-directories is granted implicitly; PATH doesn't need to directly point to a repository. + --restrict-to-repository PATH restrict repository access. Only the repository located at PATH (no sub-directories are considered) is accessible. Can be specified multiple times to allow the client access to several repositories. Unlike ``--restrict-to-path`` sub-directories are not accessible; PATH needs to directly point at a repository location. PATH may be an empty directory or the last element of PATH may not exist, in which case the client may initialize a repository there. + --append-only only allow appending to repository segment files + --storage-quota Override storage quota of the repository (e.g. 5G, 1.5T). When a new repository is initialized, sets the storage quota on the new repository as well. Default: no quota. -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/umount.rst.inc b/docs/usage/umount.rst.inc index f4f6da71..1ec6eb85 100644 --- a/docs/usage/umount.rst.inc +++ b/docs/usage/umount.rst.inc @@ -8,14 +8,38 @@ borg umount borg [common options] umount [options] MOUNTPOINT -MOUNTPOINT - mountpoint of the filesystem to umount +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+----------------+----------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+----------------+----------------------------------------+ + | | ``MOUNTPOINT`` | mountpoint of the filesystem to umount | + +-------------------------------------------------------+----------------+----------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+----------------+----------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+----------------+----------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + MOUNTPOINT + mountpoint of the filesystem to umount -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index 7d13e6d8..43772e65 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -8,23 +8,58 @@ borg upgrade borg [common options] upgrade [options] REPOSITORY -REPOSITORY - path to the repository to be upgraded +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+-----------------------+-------------------------------------------------------------------------------------------------------------------------------+ + | **positional arguments** | + +-------------------------------------------------------+-----------------------+-------------------------------------------------------------------------------------------------------------------------------+ + | | ``REPOSITORY`` | path to the repository to be upgraded | + +-------------------------------------------------------+-----------------------+-------------------------------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+-----------------------+-------------------------------------------------------------------------------------------------------------------------------+ + | | ``-n``, ``--dry-run`` | do not change repository | + +-------------------------------------------------------+-----------------------+-------------------------------------------------------------------------------------------------------------------------------+ + | | ``--inplace`` | rewrite repository in place, with no chance of going back to older | + | | | versions of the repository. | + +-------------------------------------------------------+-----------------------+-------------------------------------------------------------------------------------------------------------------------------+ + | | ``--force`` | Force upgrade | + +-------------------------------------------------------+-----------------------+-------------------------------------------------------------------------------------------------------------------------------+ + | | ``--tam`` | Enable manifest authentication (in key and cache) (Borg 1.0.9 and later) | + +-------------------------------------------------------+-----------------------+-------------------------------------------------------------------------------------------------------------------------------+ + | | ``--disable-tam`` | Disable manifest authentication (in key and cache) | + +-------------------------------------------------------+-----------------------+-------------------------------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+-----------------------+-------------------------------------------------------------------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY + path to the repository to be upgraded -optional arguments - -n, --dry-run do not change repository - --inplace rewrite repository in place, with no chance of going back to older - versions of the repository. - --force Force upgrade - --tam Enable manifest authentication (in key and cache) (Borg 1.0.9 and later) - --disable-tam Disable manifest authentication (in key and cache) + optional arguments + -n, --dry-run do not change repository + --inplace rewrite repository in place, with no chance of going back to older + versions of the repository. + --force Force upgrade + --tam Enable manifest authentication (in key and cache) (Borg 1.0.9 and later) + --disable-tam Disable manifest authentication (in key and cache) -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ diff --git a/docs/usage/with-lock.rst.inc b/docs/usage/with-lock.rst.inc index 411b47d1..dc74f6a3 100644 --- a/docs/usage/with-lock.rst.inc +++ b/docs/usage/with-lock.rst.inc @@ -8,18 +8,46 @@ borg with-lock borg [common options] with-lock [options] REPOSITORY COMMAND ARGS -REPOSITORY - repository to lock -COMMAND - command to run -ARGS - command arguments +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+----------------+--------------------+ + | **positional arguments** | + +-------------------------------------------------------+----------------+--------------------+ + | | ``REPOSITORY`` | repository to lock | + +-------------------------------------------------------+----------------+--------------------+ + | | ``COMMAND`` | command to run | + +-------------------------------------------------------+----------------+--------------------+ + | | ``ARGS`` | command arguments | + +-------------------------------------------------------+----------------+--------------------+ + | **optional arguments** | + +-------------------------------------------------------+----------------+--------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+----------------+--------------------+ + + .. raw:: html + + + +.. only:: latex + + REPOSITORY + repository to lock + COMMAND + command to run + ARGS + command arguments -.. class:: borg-common-opt-ref - -:ref:`common_options` - + :ref:`common_options` + | Description ~~~~~~~~~~~ From 9d33ff1720b4587e0719325615bdd01be5da18f6 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Jun 2017 15:30:20 +0200 Subject: [PATCH 1115/1387] docs: skip empty option groups --- docs/usage/benchmark_crud.rst.inc | 2 -- docs/usage/break-lock.rst.inc | 2 -- docs/usage/change-passphrase.rst.inc | 2 -- docs/usage/key_change-passphrase.rst.inc | 2 -- docs/usage/key_migrate-to-repokey.rst.inc | 2 -- docs/usage/rename.rst.inc | 2 -- docs/usage/serve.rst.inc | 2 -- docs/usage/umount.rst.inc | 2 -- docs/usage/with-lock.rst.inc | 2 -- setup.py | 2 ++ 10 files changed, 2 insertions(+), 18 deletions(-) diff --git a/docs/usage/benchmark_crud.rst.inc b/docs/usage/benchmark_crud.rst.inc index 91a9ff42..a1f30a46 100644 --- a/docs/usage/benchmark_crud.rst.inc +++ b/docs/usage/benchmark_crud.rst.inc @@ -19,8 +19,6 @@ borg benchmark crud +-------------------------------------------------------+----------+------------------------------------------+ | | ``PATH`` | path were to create benchmark input data | +-------------------------------------------------------+----------+------------------------------------------+ - | **optional arguments** | - +-------------------------------------------------------+----------+------------------------------------------+ | .. class:: borg-common-opt-ref | | | | :ref:`common_options` | diff --git a/docs/usage/break-lock.rst.inc b/docs/usage/break-lock.rst.inc index 4ca4aa5b..eb17d95f 100644 --- a/docs/usage/break-lock.rst.inc +++ b/docs/usage/break-lock.rst.inc @@ -17,8 +17,6 @@ borg break-lock +-------------------------------------------------------+----------------+-----------------------------------------+ | | ``REPOSITORY`` | repository for which to break the locks | +-------------------------------------------------------+----------------+-----------------------------------------+ - | **optional arguments** | - +-------------------------------------------------------+----------------+-----------------------------------------+ | .. class:: borg-common-opt-ref | | | | :ref:`common_options` | diff --git a/docs/usage/change-passphrase.rst.inc b/docs/usage/change-passphrase.rst.inc index 5b2fd281..1b7eb469 100644 --- a/docs/usage/change-passphrase.rst.inc +++ b/docs/usage/change-passphrase.rst.inc @@ -17,8 +17,6 @@ borg change-passphrase +-------------------------------------------------------+----------------+--+ | | ``REPOSITORY`` | | +-------------------------------------------------------+----------------+--+ - | **optional arguments** | - +-------------------------------------------------------+----------------+--+ | .. class:: borg-common-opt-ref | | | | :ref:`common_options` | diff --git a/docs/usage/key_change-passphrase.rst.inc b/docs/usage/key_change-passphrase.rst.inc index eef22560..1a420dd3 100644 --- a/docs/usage/key_change-passphrase.rst.inc +++ b/docs/usage/key_change-passphrase.rst.inc @@ -17,8 +17,6 @@ borg key change-passphrase +-------------------------------------------------------+----------------+--+ | | ``REPOSITORY`` | | +-------------------------------------------------------+----------------+--+ - | **optional arguments** | - +-------------------------------------------------------+----------------+--+ | .. class:: borg-common-opt-ref | | | | :ref:`common_options` | diff --git a/docs/usage/key_migrate-to-repokey.rst.inc b/docs/usage/key_migrate-to-repokey.rst.inc index 38cf78cf..33d2d2c5 100644 --- a/docs/usage/key_migrate-to-repokey.rst.inc +++ b/docs/usage/key_migrate-to-repokey.rst.inc @@ -17,8 +17,6 @@ borg key migrate-to-repokey +-------------------------------------------------------+----------------+--+ | | ``REPOSITORY`` | | +-------------------------------------------------------+----------------+--+ - | **optional arguments** | - +-------------------------------------------------------+----------------+--+ | .. class:: borg-common-opt-ref | | | | :ref:`common_options` | diff --git a/docs/usage/rename.rst.inc b/docs/usage/rename.rst.inc index 307b35d1..4f31c870 100644 --- a/docs/usage/rename.rst.inc +++ b/docs/usage/rename.rst.inc @@ -19,8 +19,6 @@ borg rename +-------------------------------------------------------+-------------+-----------------------------+ | | ``NEWNAME`` | the new archive name to use | +-------------------------------------------------------+-------------+-----------------------------+ - | **optional arguments** | - +-------------------------------------------------------+-------------+-----------------------------+ | .. class:: borg-common-opt-ref | | | | :ref:`common_options` | diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index a4e22ac3..02b215cc 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -12,8 +12,6 @@ borg serve .. class:: borg-options-table - +-------------------------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | **positional arguments** | +-------------------------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | **optional arguments** | +-------------------------------------------------------+-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/docs/usage/umount.rst.inc b/docs/usage/umount.rst.inc index 1ec6eb85..0d3a7e84 100644 --- a/docs/usage/umount.rst.inc +++ b/docs/usage/umount.rst.inc @@ -17,8 +17,6 @@ borg umount +-------------------------------------------------------+----------------+----------------------------------------+ | | ``MOUNTPOINT`` | mountpoint of the filesystem to umount | +-------------------------------------------------------+----------------+----------------------------------------+ - | **optional arguments** | - +-------------------------------------------------------+----------------+----------------------------------------+ | .. class:: borg-common-opt-ref | | | | :ref:`common_options` | diff --git a/docs/usage/with-lock.rst.inc b/docs/usage/with-lock.rst.inc index dc74f6a3..5feaf712 100644 --- a/docs/usage/with-lock.rst.inc +++ b/docs/usage/with-lock.rst.inc @@ -21,8 +21,6 @@ borg with-lock +-------------------------------------------------------+----------------+--------------------+ | | ``ARGS`` | command arguments | +-------------------------------------------------------+----------------+--------------------+ - | **optional arguments** | - +-------------------------------------------------------+----------------+--------------------+ | .. class:: borg-common-opt-ref | | | | :ref:`common_options` | diff --git a/setup.py b/setup.py index f46e35e1..2a866d53 100644 --- a/setup.py +++ b/setup.py @@ -304,6 +304,8 @@ class build_usage(Command): # (no of columns used, columns, ...) rows.append((1, '.. class:: borg-common-opt-ref\n\n:ref:`common_options`')) else: + if not group._group_actions: + continue rows.append((1, '**%s**' % group.title)) if is_positional_group(group): for option in group._group_actions: From 23ee9432d9f9f5d648e59c0db8731c78ec1c0759 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Jun 2017 15:32:39 +0200 Subject: [PATCH 1116/1387] docs: retain rST option list formatting (for Common Options) --- docs/borg_theme/css/borg.css | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/borg_theme/css/borg.css b/docs/borg_theme/css/borg.css index 28a280ef..698e2c0c 100644 --- a/docs/borg_theme/css/borg.css +++ b/docs/borg_theme/css/borg.css @@ -129,6 +129,7 @@ table.docutils.borg-options-table tr td { border-right: 0; } +table.docutils.option-list tr td, table.docutils.borg-options-table tr td { border-left: 0; border-right: 0; From a9059a64bdcb9c75920f612b2d8f8f723eb50c67 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Jun 2017 15:48:30 +0200 Subject: [PATCH 1117/1387] docs: use DOM ready event, not document loaded event --- docs/usage/benchmark_crud.rst.inc | 2 +- docs/usage/break-lock.rst.inc | 2 +- docs/usage/change-passphrase.rst.inc | 2 +- docs/usage/check.rst.inc | 2 +- docs/usage/create.rst.inc | 2 +- docs/usage/delete.rst.inc | 2 +- docs/usage/diff.rst.inc | 2 +- docs/usage/export-tar.rst.inc | 2 +- docs/usage/extract.rst.inc | 2 +- docs/usage/info.rst.inc | 2 +- docs/usage/init.rst.inc | 2 +- docs/usage/key_change-passphrase.rst.inc | 2 +- docs/usage/key_export.rst.inc | 2 +- docs/usage/key_import.rst.inc | 2 +- docs/usage/key_migrate-to-repokey.rst.inc | 2 +- docs/usage/list.rst.inc | 2 +- docs/usage/mount.rst.inc | 2 +- docs/usage/prune.rst.inc | 2 +- docs/usage/recreate.rst.inc | 2 +- docs/usage/rename.rst.inc | 2 +- docs/usage/serve.rst.inc | 2 +- docs/usage/umount.rst.inc | 2 +- docs/usage/upgrade.rst.inc | 2 +- docs/usage/with-lock.rst.inc | 2 +- setup.py | 2 +- 25 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/usage/benchmark_crud.rst.inc b/docs/usage/benchmark_crud.rst.inc index a1f30a46..b76c091d 100644 --- a/docs/usage/benchmark_crud.rst.inc +++ b/docs/usage/benchmark_crud.rst.inc @@ -27,7 +27,7 @@ borg benchmark crud .. raw:: html diff --git a/docs/usage/break-lock.rst.inc b/docs/usage/break-lock.rst.inc index eb17d95f..6a7f777a 100644 --- a/docs/usage/break-lock.rst.inc +++ b/docs/usage/break-lock.rst.inc @@ -25,7 +25,7 @@ borg break-lock .. raw:: html diff --git a/docs/usage/change-passphrase.rst.inc b/docs/usage/change-passphrase.rst.inc index 1b7eb469..8ff5487a 100644 --- a/docs/usage/change-passphrase.rst.inc +++ b/docs/usage/change-passphrase.rst.inc @@ -25,7 +25,7 @@ borg change-passphrase .. raw:: html diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index 918a3af7..ad9b22cf 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -49,7 +49,7 @@ borg check .. raw:: html diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index 00a3dfcc..d52ab9be 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -83,7 +83,7 @@ borg create .. raw:: html diff --git a/docs/usage/delete.rst.inc b/docs/usage/delete.rst.inc index 96d09796..734d54fa 100644 --- a/docs/usage/delete.rst.inc +++ b/docs/usage/delete.rst.inc @@ -47,7 +47,7 @@ borg delete .. raw:: html diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index f1d4aef6..9f93e4c7 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -53,7 +53,7 @@ borg diff .. raw:: html diff --git a/docs/usage/export-tar.rst.inc b/docs/usage/export-tar.rst.inc index 0144e43f..684d0757 100644 --- a/docs/usage/export-tar.rst.inc +++ b/docs/usage/export-tar.rst.inc @@ -45,7 +45,7 @@ borg export-tar .. raw:: html diff --git a/docs/usage/extract.rst.inc b/docs/usage/extract.rst.inc index 2cffeb4f..61ab3480 100644 --- a/docs/usage/extract.rst.inc +++ b/docs/usage/extract.rst.inc @@ -49,7 +49,7 @@ borg extract .. raw:: html diff --git a/docs/usage/info.rst.inc b/docs/usage/info.rst.inc index cd1bfb9a..24a7a8bf 100644 --- a/docs/usage/info.rst.inc +++ b/docs/usage/info.rst.inc @@ -41,7 +41,7 @@ borg info .. raw:: html diff --git a/docs/usage/init.rst.inc b/docs/usage/init.rst.inc index efadf949..8bfb7679 100644 --- a/docs/usage/init.rst.inc +++ b/docs/usage/init.rst.inc @@ -33,7 +33,7 @@ borg init .. raw:: html diff --git a/docs/usage/key_change-passphrase.rst.inc b/docs/usage/key_change-passphrase.rst.inc index 1a420dd3..7a20a6d1 100644 --- a/docs/usage/key_change-passphrase.rst.inc +++ b/docs/usage/key_change-passphrase.rst.inc @@ -25,7 +25,7 @@ borg key change-passphrase .. raw:: html diff --git a/docs/usage/key_export.rst.inc b/docs/usage/key_export.rst.inc index ab1a78b1..558a865d 100644 --- a/docs/usage/key_export.rst.inc +++ b/docs/usage/key_export.rst.inc @@ -33,7 +33,7 @@ borg key export .. raw:: html diff --git a/docs/usage/key_import.rst.inc b/docs/usage/key_import.rst.inc index 88ee1516..0e77946e 100644 --- a/docs/usage/key_import.rst.inc +++ b/docs/usage/key_import.rst.inc @@ -31,7 +31,7 @@ borg key import .. raw:: html diff --git a/docs/usage/key_migrate-to-repokey.rst.inc b/docs/usage/key_migrate-to-repokey.rst.inc index 33d2d2c5..3b44c4fe 100644 --- a/docs/usage/key_migrate-to-repokey.rst.inc +++ b/docs/usage/key_migrate-to-repokey.rst.inc @@ -25,7 +25,7 @@ borg key migrate-to-repokey .. raw:: html diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index c6fe3e4a..2e945f48 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -65,7 +65,7 @@ borg list .. raw:: html diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index 388f445d..f5180ee2 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -45,7 +45,7 @@ borg mount .. raw:: html diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index bcd0b22a..f8b66a61 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -59,7 +59,7 @@ borg prune .. raw:: html diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index cfbb6df2..f67148ad 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -69,7 +69,7 @@ borg recreate .. raw:: html diff --git a/docs/usage/rename.rst.inc b/docs/usage/rename.rst.inc index 4f31c870..e68c7f78 100644 --- a/docs/usage/rename.rst.inc +++ b/docs/usage/rename.rst.inc @@ -27,7 +27,7 @@ borg rename .. raw:: html diff --git a/docs/usage/serve.rst.inc b/docs/usage/serve.rst.inc index 02b215cc..3987cdd5 100644 --- a/docs/usage/serve.rst.inc +++ b/docs/usage/serve.rst.inc @@ -31,7 +31,7 @@ borg serve .. raw:: html diff --git a/docs/usage/umount.rst.inc b/docs/usage/umount.rst.inc index 0d3a7e84..151d76a8 100644 --- a/docs/usage/umount.rst.inc +++ b/docs/usage/umount.rst.inc @@ -25,7 +25,7 @@ borg umount .. raw:: html diff --git a/docs/usage/upgrade.rst.inc b/docs/usage/upgrade.rst.inc index 43772e65..22426299 100644 --- a/docs/usage/upgrade.rst.inc +++ b/docs/usage/upgrade.rst.inc @@ -38,7 +38,7 @@ borg upgrade .. raw:: html diff --git a/docs/usage/with-lock.rst.inc b/docs/usage/with-lock.rst.inc index 5feaf712..4aa80369 100644 --- a/docs/usage/with-lock.rst.inc +++ b/docs/usage/with-lock.rst.inc @@ -29,7 +29,7 @@ borg with-lock .. raw:: html diff --git a/setup.py b/setup.py index 2a866d53..1461c02f 100644 --- a/setup.py +++ b/setup.py @@ -387,7 +387,7 @@ class build_usage(Command): .. raw:: html From 82575dbfe5ea5884a48811bf930c1205c0c694c5 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Jun 2017 18:09:00 +0200 Subject: [PATCH 1118/1387] argparse cleanup - action='store_true' implies default=False - missing metavars added - minor code formatting --- src/borg/archiver.py | 71 ++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 5dd1b5b1..638a7137 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2314,9 +2314,9 @@ class Archiver: help='Output one JSON object per log line instead of formatted text.') add_common_option('--lock-wait', dest='lock_wait', type=int, metavar='N', default=1, help='wait for the lock, but max. N seconds (default: %(default)d).') - add_common_option('--show-version', dest='show_version', action='store_true', default=False, + add_common_option('--show-version', dest='show_version', action='store_true', help='show/log the borg version') - add_common_option('--show-rc', dest='show_rc', action='store_true', default=False, + add_common_option('--show-rc', dest='show_rc', action='store_true', help='show/log the return code (rc)') add_common_option('--no-files-cache', dest='cache_files', action='store_false', help='do not load/update the file metadata cache used to detect unchanged files') @@ -2327,7 +2327,7 @@ class Archiver: add_common_option('--remote-ratelimit', dest='remote_ratelimit', type=int, metavar='rate', help='set remote network upload rate limit in kiByte/s (default: 0=unlimited)') add_common_option('--consider-part-files', dest='consider_part_files', - action='store_true', default=False, + action='store_true', help='treat part files like normal files (e.g. to list/extract them)') add_common_option('--debug-profile', dest='debug_profile', default=None, metavar='FILE', help='Write execution profile in Borg format into FILE. For local use a Python-' @@ -3137,10 +3137,9 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='list archive or repository contents') subparser.set_defaults(func=self.do_list) - subparser.add_argument('--short', dest='short', - action='store_true', default=False, + subparser.add_argument('--short', dest='short', action='store_true', help='only print file/directory names, nothing else') - subparser.add_argument('--format', '--list-format', dest='format', type=str, + subparser.add_argument('--format', '--list-format', dest='format', type=str, metavar='FORMAT', help='specify format for file listing ' '(default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")') subparser.add_argument('--json', action='store_true', @@ -3168,8 +3167,7 @@ class Archiver: metavar="PATTERN", help='exclude paths matching PATTERN') exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction, metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') - exclude_group.add_argument('--exclude-caches', dest='exclude_caches', - action='store_true', default=False, + exclude_group.add_argument('--exclude-caches', dest='exclude_caches', action='store_true', help='exclude directories that contain a CACHEDIR.TAG file (' 'http://www.brynosaurus.com/cachedir/spec.html)') exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', @@ -3177,7 +3175,7 @@ class Archiver: help='exclude directories that are tagged by containing a filesystem object with ' 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', - action='store_true', default=False, + action='store_true', help='if tag objects are specified with ``--exclude-if-present``, don\'t omit the tag ' 'objects themselves from the backup archive') exclude_group.add_argument('--pattern', @@ -3231,7 +3229,7 @@ class Archiver: subparser.add_argument('mountpoint', metavar='MOUNTPOINT', type=str, help='where to mount filesystem') subparser.add_argument('-f', '--foreground', dest='foreground', - action='store_true', default=False, + action='store_true', help='stay in foreground, do not daemonize') subparser.add_argument('-o', dest='options', type=str, help='Extra mount options') @@ -3338,17 +3336,13 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='prune archives') subparser.set_defaults(func=self.do_prune) - subparser.add_argument('-n', '--dry-run', dest='dry_run', - default=False, action='store_true', + subparser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true', help='do not change repository') - subparser.add_argument('--force', dest='forced', - action='store_true', default=False, + subparser.add_argument('--force', dest='forced', action='store_true', help='force pruning of corrupted archives') - subparser.add_argument('-s', '--stats', dest='stats', - action='store_true', default=False, + subparser.add_argument('-s', '--stats', dest='stats', action='store_true', help='print statistics for the deleted archive') - subparser.add_argument('--list', dest='output_list', - action='store_true', default=False, + subparser.add_argument('--list', dest='output_list', action='store_true', help='output verbose list of archives it keeps/prunes') subparser.add_argument('--keep-within', dest='within', type=str, metavar='WITHIN', help='keep all archives within this time interval') @@ -3368,7 +3362,6 @@ class Archiver: help='number of yearly archives to keep') self.add_archives_filters_args(subparser, sort_by=False, first_last=False) subparser.add_argument('--save-space', dest='save_space', action='store_true', - default=False, help='work slower, but using less space') subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False), @@ -3462,19 +3455,17 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='upgrade repository format') subparser.set_defaults(func=self.do_upgrade) - subparser.add_argument('-n', '--dry-run', dest='dry_run', - default=False, action='store_true', + subparser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true', help='do not change repository') - subparser.add_argument('--inplace', dest='inplace', - default=False, action='store_true', - help="""rewrite repository in place, with no chance of going back to older - versions of the repository.""") + subparser.add_argument('--inplace', dest='inplace', action='store_true', + help='rewrite repository in place, with no chance of going back ' + 'to older versions of the repository.') subparser.add_argument('--force', dest='force', action='store_true', - help="""Force upgrade""") + help='Force upgrade') subparser.add_argument('--tam', dest='tam', action='store_true', - help="""Enable manifest authentication (in key and cache) (Borg 1.0.9 and later)""") + help='Enable manifest authentication (in key and cache) (Borg 1.0.9 and later).') subparser.add_argument('--disable-tam', dest='disable_tam', action='store_true', - help="""Disable manifest authentication (in key and cache)""") + help='Disable manifest authentication (in key and cache).') subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False), help='path to the repository to be upgraded') @@ -3523,16 +3514,13 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help=self.do_recreate.__doc__) subparser.set_defaults(func=self.do_recreate) - subparser.add_argument('--list', dest='output_list', - action='store_true', default=False, + subparser.add_argument('--list', dest='output_list', action='store_true', help='output verbose list of items (files, dirs, ...)') subparser.add_argument('--filter', dest='output_filter', metavar='STATUSCHARS', help='only display items with the given status characters') - subparser.add_argument('-n', '--dry-run', dest='dry_run', - action='store_true', default=False, + subparser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true', help='do not change anything') - subparser.add_argument('-s', '--stats', dest='stats', - action='store_true', default=False, + subparser.add_argument('-s', '--stats', dest='stats', action='store_true', help='print statistics at end') exclude_group = subparser.add_argument_group('Exclusion options') @@ -3542,7 +3530,7 @@ class Archiver: exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction, metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') exclude_group.add_argument('--exclude-caches', dest='exclude_caches', - action='store_true', default=False, + action='store_true', help='exclude directories that contain a CACHEDIR.TAG file (' 'http://www.brynosaurus.com/cachedir/spec.html)') exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', @@ -3550,7 +3538,7 @@ class Archiver: help='exclude directories that are tagged by containing a filesystem object with ' 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', - action='store_true', default=False, + action='store_true', help='if tag objects are specified with ``--exclude-if-present``, don\'t omit the tag ' 'objects themselves from the backup archive') exclude_group.add_argument('--pattern', @@ -3625,10 +3613,8 @@ class Archiver: subparser = subparsers.add_parser('help', parents=[common_parser], add_help=False, description='Extra help') - subparser.add_argument('--epilog-only', dest='epilog_only', - action='store_true', default=False) - subparser.add_argument('--usage-only', dest='usage_only', - action='store_true', default=False) + subparser.add_argument('--epilog-only', dest='epilog_only', action='store_true') + subparser.add_argument('--usage-only', dest='usage_only', action='store_true') subparser.set_defaults(func=functools.partial(self.do_help, parser, subparsers.choices)) subparser.add_argument('topic', metavar='TOPIC', type=str, nargs='?', help='additional help on TOPIC') @@ -3863,9 +3849,9 @@ class Archiver: def add_archives_filters_args(subparser, sort_by=True, first_last=True): filters_group = subparser.add_argument_group('filters', 'Archive filters can be applied to repository targets.') group = filters_group.add_mutually_exclusive_group() - group.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, default='', + group.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, default='', metavar='PREFIX', help='only consider archive names starting with this prefix.') - group.add_argument('-a', '--glob-archives', dest='glob_archives', default=None, + group.add_argument('-a', '--glob-archives', dest='glob_archives', default=None, metavar='GLOB', help='only consider archive names matching the glob. ' 'sh: rules apply, see "borg help patterns". ' '``--prefix`` and ``--glob-archives`` are mutually exclusive.') @@ -3873,6 +3859,7 @@ class Archiver: if sort_by: sort_by_default = 'timestamp' filters_group.add_argument('--sort-by', dest='sort_by', type=SortBySpec, default=sort_by_default, + metavar='KEYS', help='Comma-separated list of sorting keys; valid keys are: {}; default is: {}' .format(', '.join(HUMAN_SORT_KEYS), sort_by_default)) From 6e5ae6dc04f2f2fbf5ce2b550d04400d8479fc1a Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 20 Jun 2017 18:13:10 +0200 Subject: [PATCH 1119/1387] delete: remove short option for --cache-only --- src/borg/archiver.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 5dd1b5b1..03020a6c 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -3094,11 +3094,9 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='delete archive') subparser.set_defaults(func=self.do_delete) - subparser.add_argument('-s', '--stats', dest='stats', - action='store_true', default=False, + subparser.add_argument('-s', '--stats', dest='stats', action='store_true', help='print statistics for the deleted archive') - subparser.add_argument('-c', '--cache-only', dest='cache_only', - action='store_true', default=False, + subparser.add_argument('--cache-only', dest='cache_only', action='store_true', help='delete only the local cache for the given repository') subparser.add_argument('--force', dest='forced', action='count', default=0, From 88ae1ebf33ffb9ae1db0d1c52422a6cf70cce85d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 21 Jun 2017 00:02:57 +0200 Subject: [PATCH 1120/1387] docs: format metavars more accurately --- setup.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 1461c02f..f2b4d977 100644 --- a/setup.py +++ b/setup.py @@ -203,6 +203,17 @@ with open('README.rst', 'r') as fd: long_description = re.compile(r'^\.\. highlight:: \w+$', re.M).sub('', long_description) +def format_metavar(option): + if option.nargs in ('*', '...'): + return '[%s...]' % option.metavar + elif option.nargs == '?': + return '[%s]' % option.metavar + elif option.nargs is None: + return option.metavar + else: + raise ValueError('Can\'t format metavar %s, unknown nargs %s!' % (option.metavar, option.nargs)) + + class build_usage(Command): description = "generate usage for each command" @@ -284,7 +295,7 @@ class build_usage(Command): for option in parser._actions: if option.option_strings: continue - fp.write(' ' + option.metavar) + fp.write(' ' + format_metavar(option)) fp.write('\n\n') def write_options(self, parser, fp): @@ -645,11 +656,11 @@ class build_man(Command): def write_usage(self, write, parser): if any(len(o.option_strings) for o in parser._actions): - write(' ', end='') + write(' [options] ', end='') for option in parser._actions: if option.option_strings: continue - write(option.metavar, end=' ') + write(format_metavar(option), end=' ') def write_options(self, write, parser): for group in parser._action_groups: From 09d0d566a5a86ac2f032b441852747bbfacca764 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 21 Jun 2017 00:16:06 +0200 Subject: [PATCH 1121/1387] docs: with-lock: convert to proper admonition --- src/borg/archiver.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 449198ea..e00e344a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -3591,9 +3591,11 @@ class Archiver: for its termination, release the lock and return the user command's return code as borg's return code. - Note: if you copy a repository with the lock held, the lock will be present in - the copy, obviously. Thus, before using borg on the copy, you need to - use "borg break-lock" on it. + .. note:: + + If you copy a repository with the lock held, the lock will be present in + the copy, obviously. Thus, before using borg on the copy, you need to + use "borg break-lock" on it. """) subparser = subparsers.add_parser('with-lock', parents=[common_parser], add_help=False, description=self.do_with_lock.__doc__, From 771168a3afcb8556c35997c00ffbd4421dcd7dc2 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Wed, 21 Jun 2017 00:16:25 +0200 Subject: [PATCH 1122/1387] docs: extract: move cwd note to --help --- docs/usage/extract.rst | 6 ------ src/borg/archiver.py | 5 +++++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/usage/extract.rst b/docs/usage/extract.rst index be926896..292f84bd 100644 --- a/docs/usage/extract.rst +++ b/docs/usage/extract.rst @@ -21,9 +21,3 @@ Examples # Restore a raw device (must not be active/in use/mounted at that time) $ borg extract --stdout /path/to/repo::my-sdx | dd of=/dev/sdx bs=10M - - -.. Note:: - - Currently, extract always writes into the current working directory ("."), - so make sure you ``cd`` to the right place before calling ``borg extract``. diff --git a/src/borg/archiver.py b/src/borg/archiver.py index e00e344a..899c4c1d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2893,6 +2893,11 @@ class Archiver: ``--progress`` can be slower than no progress display, since it makes one additional pass over the archive metadata. + + .. note:: + + Currently, extract always writes into the current working directory ("."), + so make sure you ``cd`` to the right place before calling ``borg extract``. """) subparser = subparsers.add_parser('extract', parents=[common_parser], add_help=False, description=self.do_extract.__doc__, From 49411d1c6c4a0d99cfab2f2d31eec6e31a7b8dbb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 21 Jun 2017 15:59:44 +0200 Subject: [PATCH 1123/1387] remove skipping the noatime tests on GNU/Hurd, fixes #2710 I recently installed GNU/Hurd 2017 and found the atime test works now. --- src/borg/testsuite/archiver.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index b0b0a9b7..24377a1e 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -453,9 +453,6 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('extract', self.repository_location + '::test') assert os.readlink('input/link1') == 'somewhere' - # Search for O_NOATIME there: https://www.gnu.org/software/hurd/contributing.html - we just - # skip the test on Hurd, it is not critical anyway, just testing a performance optimization. - @pytest.mark.skipif(sys.platform == 'gnu0', reason="O_NOATIME is strangely broken on GNU Hurd") @pytest.mark.skipif(not is_utime_fully_supported(), reason='cannot properly setup and execute test without utime') def test_atime(self): def has_noatime(some_file): From b625a4c8c5fbbe5e9cbcb28b02843f1e6df07069 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 22 Jun 2017 20:13:39 +0200 Subject: [PATCH 1124/1387] travis: install fakeroot for Linux they removed it from the preinstalled sw stack. --- .travis/install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis/install.sh b/.travis/install.sh index 573cba08..708b8c81 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -39,6 +39,7 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then else pip install virtualenv sudo apt-get update + sudo apt-get install -y fakeroot sudo apt-get install -y liblz4-dev sudo apt-get install -y libacl1-dev sudo apt-get install -y libfuse-dev fuse pkg-config # optional, for FUSE support From b27b9894d1b9fa182be4e25cc054fb0644354647 Mon Sep 17 00:00:00 2001 From: rugk Date: Fri, 23 Jun 2017 13:28:49 +0200 Subject: [PATCH 1125/1387] Simplify ssh authorized_keys file Just using "restrict"; closes https://github.com/borgbackup/borg/issues/2121 --- docs/deployment/central-backup-server.rst | 11 +++-------- docs/quickstart.rst | 2 +- docs/usage/serve.rst | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/docs/deployment/central-backup-server.rst b/docs/deployment/central-backup-server.rst index 68c8fdda..a2af9c97 100644 --- a/docs/deployment/central-backup-server.rst +++ b/docs/deployment/central-backup-server.rst @@ -68,8 +68,7 @@ forced command and restrictions applied as shown below: command="cd /home/backup/repos/; borg serve --restrict-to-path /home/backup/repos/", - no-port-forwarding,no-X11-forwarding,no-pty, - no-agent-forwarding,no-user-rc + restrict .. note:: The text shown above needs to be written on a single line! @@ -147,7 +146,7 @@ package manager to install and keep borg up-to-date. - file: path="{{ pool }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory - authorized_key: user="{{ user }}" key="{{ item.key }}" - key_options='command="cd {{ pool }}/{{ item.host }};borg serve --restrict-to-path {{ pool }}/{{ item.host }}",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc' + key_options='command="cd {{ pool }}/{{ item.host }};borg serve --restrict-to-path {{ pool }}/{{ item.host }}",restrict' with_items: "{{ auth_users }}" - file: path="{{ home }}/.ssh/authorized_keys" owner="{{ user }}" group="{{ group }}" mode=0600 state=file - file: path="{{ pool }}/{{ item.host }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory @@ -198,11 +197,7 @@ Salt running on a Debian system. - source: salt://conf/ssh-pubkeys/{{host}}-backup.id_ecdsa.pub - options: - command="cd /home/backup/repos/{{host}}; borg serve --restrict-to-path /home/backup/repos/{{host}}" - - no-port-forwarding - - no-X11-forwarding - - no-pty - - no-agent-forwarding - - no-user-rc + - restrict {% endfor %} diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 43d62fc9..d816fea0 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -277,7 +277,7 @@ use of the SSH keypair by prepending a forced command to the SSH public key in the remote server's `authorized_keys` file. This example will start |project_name| in server mode and limit it to a specific filesystem path:: - command="borg serve --restrict-to-path /path/to/repo",no-pty,no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-user-rc ssh-rsa AAAAB3[...] + command="borg serve --restrict-to-path /path/to/repo",restrict ssh-rsa AAAAB3[...] If it is not possible to install |project_name| on the remote host, it is still possible to use the remote host to store a repository by diff --git a/docs/usage/serve.rst b/docs/usage/serve.rst index ebc5626e..f3a48b58 100644 --- a/docs/usage/serve.rst +++ b/docs/usage/serve.rst @@ -23,7 +23,7 @@ locations like ``/etc/environment`` or in the forced command itself (example bel # Use key options to disable unneeded and potentially dangerous SSH functionality. # This will help to secure an automated remote backup system. $ cat ~/.ssh/authorized_keys - command="borg serve --restrict-to-path /path/to/repo",no-pty,no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-user-rc ssh-rsa AAAAB3[...] + command="borg serve --restrict-to-path /path/to/repo",restrict ssh-rsa AAAAB3[...] # Set a BORG_XXX environment variable on the "borg serve" side $ cat ~/.ssh/authorized_keys From 41248bbab10b5cafe1f6a7bbcf2cdf103c260c56 Mon Sep 17 00:00:00 2001 From: rugk Date: Fri, 23 Jun 2017 14:50:00 +0200 Subject: [PATCH 1126/1387] Add legacy note & normalize order --- docs/deployment/hosting-repositories.rst | 2 +- docs/usage/serve.rst | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/deployment/hosting-repositories.rst b/docs/deployment/hosting-repositories.rst index e502d644..a44b9941 100644 --- a/docs/deployment/hosting-repositories.rst +++ b/docs/deployment/hosting-repositories.rst @@ -29,7 +29,7 @@ SSH access to safe operations only. :: - restrict,command="borg serve --restrict-to-repository /home//repository" + command="borg serve --restrict-to-repository /home//repository",restrict .. note:: The text shown above needs to be written on a **single** line! diff --git a/docs/usage/serve.rst b/docs/usage/serve.rst index f3a48b58..1753b339 100644 --- a/docs/usage/serve.rst +++ b/docs/usage/serve.rst @@ -29,3 +29,13 @@ locations like ``/etc/environment`` or in the forced command itself (example bel $ cat ~/.ssh/authorized_keys command="export BORG_XXX=value; borg serve [...]",restrict ssh-rsa [...] +.. note:: + The examples above use the ``restrict`` directive. This does automatically + block potential dangerous ssh features, even when they are added in a future + update. Thus, this option should be prefered. + + If you're using openssh-server < 7.2, however, you have to explicitly specify + the ssh features to restrict and cannot simply use the restrict option as it + has been introduced in v7.2. We recommend to use + ``,no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc`` + in this case. From 5b0b4f4b00efaa71af56c32dbdee4b2a0dde115a Mon Sep 17 00:00:00 2001 From: rugk Date: Fri, 23 Jun 2017 14:52:11 +0200 Subject: [PATCH 1127/1387] Remove unneccessary space --- docs/usage/serve.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/serve.rst b/docs/usage/serve.rst index 1753b339..ae18205a 100644 --- a/docs/usage/serve.rst +++ b/docs/usage/serve.rst @@ -37,5 +37,5 @@ locations like ``/etc/environment`` or in the forced command itself (example bel If you're using openssh-server < 7.2, however, you have to explicitly specify the ssh features to restrict and cannot simply use the restrict option as it has been introduced in v7.2. We recommend to use - ``,no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc`` + ``no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc`` in this case. From 6051969df89589b8f0a85aecb6ba07dc08853b2a Mon Sep 17 00:00:00 2001 From: rugk Date: Fri, 23 Jun 2017 16:54:02 +0200 Subject: [PATCH 1128/1387] Fix typo --- docs/usage/serve.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/serve.rst b/docs/usage/serve.rst index ae18205a..7adc8f66 100644 --- a/docs/usage/serve.rst +++ b/docs/usage/serve.rst @@ -32,7 +32,7 @@ locations like ``/etc/environment`` or in the forced command itself (example bel .. note:: The examples above use the ``restrict`` directive. This does automatically block potential dangerous ssh features, even when they are added in a future - update. Thus, this option should be prefered. + update. Thus, this option should be preferred. If you're using openssh-server < 7.2, however, you have to explicitly specify the ssh features to restrict and cannot simply use the restrict option as it From e967edad9868c14fcefc8e2306f7bd2d1a28eed9 Mon Sep 17 00:00:00 2001 From: rugk Date: Fri, 23 Jun 2017 17:03:04 +0200 Subject: [PATCH 1129/1387] Include prune example in doc --- docs/usage/prune.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/usage/prune.rst b/docs/usage/prune.rst index fb1fe660..6a3b5ab0 100644 --- a/docs/usage/prune.rst +++ b/docs/usage/prune.rst @@ -14,8 +14,6 @@ prefix "foo" if you do not also want to match "foobar". It is strongly recommended to always run ``prune -v --list --dry-run ...`` first so you will see what it would do without it actually doing anything. -There is also a visualized prune example in ``docs/misc/prune-example.txt``. - :: # Keep 7 end of day and 4 additional end of week archives. @@ -33,3 +31,8 @@ There is also a visualized prune example in ``docs/misc/prune-example.txt``. # Keep all backups in the last 10 days, 4 additional end of week archives, # and an end of month archive for every month: $ borg prune -v --list --keep-within=10d --keep-weekly=4 --keep-monthly=-1 /path/to/repo + +There is also a visualized prune example in ``docs/misc/prune-example.txt``. + +.. include:: misc/prune-example.txt + :literal: From 310a9b3486f9b29a730c548c2447110f5142b62b Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 23 Jun 2017 19:16:33 +0200 Subject: [PATCH 1130/1387] docs: prune: fix include path --- docs/usage/prune.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/usage/prune.rst b/docs/usage/prune.rst index 6a3b5ab0..028f8300 100644 --- a/docs/usage/prune.rst +++ b/docs/usage/prune.rst @@ -32,7 +32,8 @@ first so you will see what it would do without it actually doing anything. # and an end of month archive for every month: $ borg prune -v --list --keep-within=10d --keep-weekly=4 --keep-monthly=-1 /path/to/repo -There is also a visualized prune example in ``docs/misc/prune-example.txt``. +There is also a visualized prune example in ``docs/misc/prune-example.txt``: -.. include:: misc/prune-example.txt +.. highlight:: none +.. include:: ../misc/prune-example.txt :literal: From 7ebad4f8035e70b5dee64f2001c0ce9bb0b59582 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 24 Jun 2017 01:24:14 +0200 Subject: [PATCH 1131/1387] FUSE vs. fuse --- Vagrantfile | 4 ++-- docs/changes.rst | 30 +++++++++++++++--------------- docs/installation.rst | 2 +- src/borg/archiver.py | 4 ++-- src/borg/fuse.py | 4 ++-- src/borg/testsuite/__init__.py | 2 +- src/borg/testsuite/archiver.py | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index ef96e0ad..2461715a 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -149,7 +149,7 @@ def packages_netbsd ln -s /usr/pkg/lib/liblz4* /usr/local/opt/lz4/lib/ touch /etc/openssl/openssl.cnf # avoids a flood of "can't open ..." mozilla-rootcerts install - pkg_add pkg-config # avoids some "pkg-config missing" error msg, even without fuse + pkg_add pkg-config # avoids some "pkg-config missing" error msg, even without fuse pkg # pkg_add fuse # llfuse supports netbsd, but is still buggy. # https://bitbucket.org/nikratio/python-llfuse/issues/70/perfuse_open-setsockopt-no-buffer-space pkg_add python34 py34-setuptools @@ -286,7 +286,7 @@ def install_borg(fuse) EOF if fuse script += <<-EOF - # by using [fuse], setup.py can handle different fuse requirements: + # by using [fuse], setup.py can handle different FUSE requirements: pip install -e .[fuse] EOF else diff --git a/docs/changes.rst b/docs/changes.rst index 5f95bb06..78092e58 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -199,7 +199,7 @@ Fixes: error message when parsing fails. - mount: check whether llfuse is installed before asking for passphrase, #2540 - mount: do pre-mount checks before opening repository, #2541 -- fuse: +- FUSE: - fix crash if empty (None) xattr is read, #2534 - fix read(2) caching data in metadata cache @@ -243,7 +243,7 @@ Other changes: - increase DEFAULT_SEGMENTS_PER_DIR to 1000 - chunker: fix invalid use of types (function only used by tests) - chunker: don't do uint32_t >> 32 -- fuse: +- FUSE: - add instrumentation (--debug and SIGUSR1/SIGINFO) - reduced memory usage for repository mounts by lazily instantiating archives @@ -651,8 +651,8 @@ Other changes: - tests: - - fuse tests: catch ENOTSUP on freebsd - - fuse tests: test troublesome xattrs last + - FUSE tests: catch ENOTSUP on freebsd + - FUSE tests: test troublesome xattrs last - fix byte range error in test, #1740 - use monkeypatch to set env vars, but only on pytest based tests. - point XDG_*_HOME to temp dirs for tests, #1714 @@ -824,7 +824,7 @@ Other changes: - upgrade OSXfuse / FUSE for macOS to 3.5.3 - remove llfuse from tox.ini at a central place - do not try to install llfuse on centos6 - - fix fuse test for darwin, #1546 + - fix FUSE test for darwin, #1546 - add windows virtual machine with cygwin - Vagrantfile cleanup / code deduplication @@ -1033,8 +1033,8 @@ Other changes: - vagrant / tests: - no chown when rsyncing (fixes boxes w/o vagrant group) - - fix fuse permission issues on linux/freebsd, #1544 - - skip fuse test for borg binary + fakeroot + - fix FUSE permission issues on linux/freebsd, #1544 + - skip FUSE test for borg binary + fakeroot - ignore security.selinux xattrs, fixes tests on centos, #1735 @@ -1099,7 +1099,7 @@ Other changes: - upgrade OSXfuse / FUSE for macOS to 3.5.2 - update Debian Wheezy boxes, #1686 - openbsd / netbsd: use own boxes, fixes misc rsync installation and - fuse/llfuse related testing issues, #1695 #1696 #1670 #1671 #1728 + FUSE/llfuse related testing issues, #1695 #1696 #1670 #1671 #1728 - docs: - add docs for "key export" and "key import" commands, #1641 @@ -1120,10 +1120,10 @@ Other changes: - clarify FAQ regarding backup of virtual machines, #1672 - tests: - - work around fuse xattr test issue with recent fakeroot + - work around FUSE xattr test issue with recent fakeroot - simplify repo/hashindex tests - - travis: test fuse-enabled borg, use trusty to have a recent FUSE - - re-enable fuse tests for RemoteArchiver (no deadlocks any more) + - travis: test FUSE-enabled borg, use trusty to have a recent FUSE + - re-enable FUSE tests for RemoteArchiver (no deadlocks any more) - clean env for pytest based tests, #1714 - fuse_mount contextmanager: accept any options @@ -1264,7 +1264,7 @@ Other changes: - xenial64: use user "ubuntu", not "vagrant" (as usual), #1331 - tests: - - fix fuse tests on OS X, #1433 + - fix FUSE tests on OS X, #1433 - docs: - FAQ: add backup using stable filesystem names recommendation @@ -1325,7 +1325,7 @@ Other changes: - tests: - add more FUSE tests, #1284 - - deduplicate fuse (u)mount code + - deduplicate FUSE (u)mount code - fix borg binary test issues, #862 - docs: @@ -1700,7 +1700,7 @@ Other changes: - fix order in release process - updated usage docs and other minor / cosmetic fixes - verified borg examples in docs, #644 - - freebsd dependency installation and fuse configuration, #649 + - freebsd dependency installation and FUSE configuration, #649 - add example how to restore a raw device, #671 - add a hint about the dev headers needed when installing from source - add examples for delete (and handle delete after list, before prune), #656 @@ -2545,7 +2545,7 @@ Version 0.7 - Ported to FreeBSD - Improved documentation -- Experimental: Archives mountable as fuse filesystems. +- Experimental: Archives mountable as FUSE filesystems. - The "user." prefix is no longer stripped from xattrs on Linux diff --git a/docs/installation.rst b/docs/installation.rst index 962e11f8..2f1e7f50 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -321,7 +321,7 @@ FUSE for OS X, which is available as a pre-release_. FreeBSD ++++++++ Listed below are packages you will need to install |project_name|, its dependencies, -and commands to make fuse work for using the mount command. +and commands to make FUSE work for using the mount command. :: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 899c4c1d..01456b2c 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1266,7 +1266,7 @@ class Archiver: try: import borg.fuse except ImportError as e: - self.print_error('borg mount not available: loading fuse support failed [ImportError: %s]' % str(e)) + self.print_error('borg mount not available: loading FUSE support failed [ImportError: %s]' % str(e)) return self.exit_code if not os.path.isdir(args.mountpoint) or not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK): @@ -1285,7 +1285,7 @@ class Archiver: try: operations.mount(args.mountpoint, args.options, args.foreground) except RuntimeError: - # Relevant error message already printed to stderr by fuse + # Relevant error message already printed to stderr by FUSE self.exit_code = EXIT_ERROR return self.exit_code diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 8492ee3f..7d383a2a 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -201,7 +201,7 @@ class ItemCache: class FuseOperations(llfuse.Operations): - """Export archive as a fuse filesystem + """Export archive as a FUSE filesystem """ # mount options allow_damaged_files = False @@ -310,7 +310,7 @@ class FuseOperations(llfuse.Operations): return ino def process_archive(self, archive_name, prefix=[]): - """Build fuse inode hierarchy from archive metadata + """Build FUSE inode hierarchy from archive metadata """ self.file_versions = {} # for versions mode: original path -> version t0 = time.perf_counter() diff --git a/src/borg/testsuite/__init__.py b/src/borg/testsuite/__init__.py index 08e6db25..c5748caa 100644 --- a/src/borg/testsuite/__init__.py +++ b/src/borg/testsuite/__init__.py @@ -162,7 +162,7 @@ class BaseTestCase(unittest.TestCase): fuse = s1.st_dev != s2.st_dev attrs = ['st_uid', 'st_gid', 'st_rdev'] if not fuse or not os.path.isdir(path1): - # dir nlink is always 1 on our fuse filesystem + # dir nlink is always 1 on our FUSE filesystem attrs.append('st_nlink') d1 = [filename] + [getattr(s1, a) for a in attrs] d2 = [filename] + [getattr(s2, a) for a in attrs] diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 24377a1e..b047ff44 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -326,7 +326,7 @@ class ArchiverTestCaseBase(BaseTestCase): os.symlink('somewhere', os.path.join(self.input_path, 'link1')) self.create_regular_file('fusexattr', size=1) if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path): - # ironically, due to the way how fakeroot works, comparing fuse file xattrs to orig file xattrs + # ironically, due to the way how fakeroot works, comparing FUSE file xattrs to orig file xattrs # will FAIL if fakeroot supports xattrs, thus we only set the xattr if XATTR_FAKEROOT is False. # This is because fakeroot with xattr-support does not propagate xattrs of the underlying file # into "fakeroot space". Because the xattrs exposed by borgfs are these of an underlying file From 26c92256e25f2af50b863f7e2d112b5263398c5a Mon Sep 17 00:00:00 2001 From: Hartmut Goebel Date: Sat, 24 Jun 2017 09:14:41 +0200 Subject: [PATCH 1132/1387] Vagrantfile: Fix cygwin mirror URL. mirrors.kernel.org no longer offers ftp. --- Vagrantfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index ef96e0ad..abc099f5 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -193,7 +193,7 @@ def packages_cygwin(version) REM --- Install build version of CygWin in a subfolder set OURPATH=%cd% set CYGBUILD="C:\\cygwin\\CygWin" - set CYGMIRROR=ftp://mirrors.kernel.org/sourceware/cygwin/ + set CYGMIRROR=http://mirrors.kernel.org/sourceware/cygwin/ set BASEPKGS=openssh,rsync set BUILDPKGS=python3,python3-setuptools,python-devel,binutils,gcc-g++,libopenssl,openssl-devel,git,make,liblz4-devel,liblz4_1,curl %CYGSETUP% -q -B -o -n -R %CYGBUILD% -L -D -s %CYGMIRROR% -P %BASEPKGS%,%BUILDPKGS% From b09e4eff863795c8b7295754eee3df64b84cfca4 Mon Sep 17 00:00:00 2001 From: Hartmut Goebel Date: Sat, 24 Jun 2017 15:19:03 +0200 Subject: [PATCH 1133/1387] Vagrantfile: cygwin: Run setup after setting PATH Fixes #2609. --- Vagrantfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index abc099f5..8cf159a9 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -201,7 +201,6 @@ def packages_cygwin(version) regtool set /HKLM/SYSTEM/CurrentControlSet/Services/OpenSSHd/ImagePath "C:\\cygwin\\CygWin\\bin\\cygrunsrv.exe" bash -c "ssh-host-config --no" ' > /cygdrive/c/cygwin/install.bat - cd /cygdrive/c/cygwin && cmd.exe /c install.bat echo "alias mkdir='mkdir -p'" > ~/.profile echo "export CYGWIN_ROOT=/cygdrive/c/cygwin/CygWin" >> ~/.profile @@ -211,6 +210,8 @@ def packages_cygwin(version) cmd.exe /c 'setx /m PATH "%PATH%;C:\\cygwin\\CygWin\\bin"' source ~/.profile + cd /cygdrive/c/cygwin && cmd.exe /c install.bat + echo 'db_home: windows' > $CYGWIN_ROOT/etc/nsswitch.conf EOF end From 54a60c5fc8b36f69a38bcd81b1e8d98be840b474 Mon Sep 17 00:00:00 2001 From: Hartmut Goebel Date: Sat, 24 Jun 2017 17:43:24 +0200 Subject: [PATCH 1134/1387] Vagrantfile: cygwin: Fix permissions as required by sshd. --- Vagrantfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Vagrantfile b/Vagrantfile index 8cf159a9..ef1b51c3 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -200,6 +200,7 @@ def packages_cygwin(version) cd /d C:\\cygwin\\CygWin\\bin regtool set /HKLM/SYSTEM/CurrentControlSet/Services/OpenSSHd/ImagePath "C:\\cygwin\\CygWin\\bin\\cygrunsrv.exe" bash -c "ssh-host-config --no" + bash -c "chown sshd_server /cygdrive/c/cygwin/CygWin/var/empty" ' > /cygdrive/c/cygwin/install.bat echo "alias mkdir='mkdir -p'" > ~/.profile From f962659f11a995f3a812e1ab3d64b4f8afeaf438 Mon Sep 17 00:00:00 2001 From: Hartmut Goebel Date: Sat, 24 Jun 2017 19:22:39 +0200 Subject: [PATCH 1135/1387] Vagrantfile: cygwin: Prepend cygwin PATH. Otherwise crippled programs of the pre-installed MLS-OpenSSHd's path hide the cygwin programs. --- Vagrantfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index ef1b51c3..c25d89a6 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -205,11 +205,11 @@ def packages_cygwin(version) echo "alias mkdir='mkdir -p'" > ~/.profile echo "export CYGWIN_ROOT=/cygdrive/c/cygwin/CygWin" >> ~/.profile - echo 'export PATH=$PATH:$CYGWIN_ROOT/bin' >> ~/.profile + echo 'export PATH=$CYGWIN_ROOT/bin:$PATH' >> ~/.profile echo '' > ~/.bash_profile - cmd.exe /c 'setx /m PATH "%PATH%;C:\\cygwin\\CygWin\\bin"' + cmd.exe /c 'setx /m PATH "C:\\cygwin\\CygWin\\bin;%PATH%"' source ~/.profile cd /cygdrive/c/cygwin && cmd.exe /c install.bat From 045a6768259c307f86a2bda03581478b3e57ae2e Mon Sep 17 00:00:00 2001 From: Hartmut Goebel Date: Sat, 24 Jun 2017 19:39:08 +0200 Subject: [PATCH 1136/1387] Vagrantfile: cygwin: Fix installation of pip. cygwin is at python-3.6 now. Use version-independent `ensurepip` to be future-proof. --- Vagrantfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index c25d89a6..e6b65310 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -219,7 +219,7 @@ end def install_cygwin_venv return <<-EOF - easy_install-3.4 pip + python3 -m ensurepip -U --default-pip pip install virtualenv EOF end From 9c5425dda88dfe3b26766b7c36f313f63cb814b1 Mon Sep 17 00:00:00 2001 From: Ed Blackman Date: Wed, 21 Jun 2017 22:25:45 -0400 Subject: [PATCH 1137/1387] Split up interval parsing from filtering for --keep-within Fixes #2610 Parse --keep-within argument early, via new validator method interval passed to argparse type=, so that better error messages can be given. Also swallows ValueError stacktrace per the comment in the old code that including it wasn't desirable. --- src/borg/archiver.py | 4 ++-- src/borg/helpers.py | 29 +++++++++++++++++++----- src/borg/testsuite/helpers.py | 42 ++++++++++++++++++++++++++++++----- 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 01456b2c..91f2fcac 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -49,7 +49,7 @@ from .helpers import PrefixSpec, SortBySpec, HUMAN_SORT_KEYS from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter from .helpers import format_timedelta, format_file_size, parse_file_size, format_archive from .helpers import safe_encode, remove_surrogates, bin_to_hex, prepare_dump_dict -from .helpers import prune_within, prune_split +from .helpers import interval, prune_within, prune_split from .helpers import timestamp from .helpers import get_cache_dir from .helpers import Manifest @@ -3347,7 +3347,7 @@ class Archiver: help='print statistics for the deleted archive') subparser.add_argument('--list', dest='output_list', action='store_true', help='output verbose list of archives it keeps/prunes') - subparser.add_argument('--keep-within', dest='within', type=str, metavar='WITHIN', + subparser.add_argument('--keep-within', dest='within', type=interval, metavar='INTERVAL', help='keep all archives within this time interval') subparser.add_argument('--keep-last', '--keep-secondly', dest='secondly', type=int, default=0, help='number of secondly archives to keep') diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 54feffa8..92c573b2 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -364,15 +364,32 @@ class Manifest: self.repository.put(self.MANIFEST_ID, self.key.encrypt(data)) -def prune_within(archives, within): +def interval(s): + """Convert a string representing a valid interval to a number of hours.""" multiplier = {'H': 1, 'd': 24, 'w': 24 * 7, 'm': 24 * 31, 'y': 24 * 365} + + if s.endswith(tuple(multiplier.keys())): + number = s[:-1] + suffix = s[-1] + else: + # range suffixes in ascending multiplier order + ranges = [k for k, v in sorted(multiplier.items(), key=lambda t: t[1])] + raise argparse.ArgumentTypeError( + 'Unexpected interval time unit "%s": expected one of %r' % (s[-1], ranges)) + try: - hours = int(within[:-1]) * multiplier[within[-1]] - except (KeyError, ValueError): - # I don't like how this displays the original exception too: - raise argparse.ArgumentTypeError('Unable to parse --keep-within option: "%s"' % within) + hours = int(number) * multiplier[suffix] + except ValueError: + hours = -1 + if hours <= 0: - raise argparse.ArgumentTypeError('Number specified using --keep-within option must be positive') + raise argparse.ArgumentTypeError( + 'Unexpected interval number "%s": expected an integer greater than 0' % number) + + return hours + + +def prune_within(archives, hours): target = datetime.now(timezone.utc) - timedelta(seconds=hours * 3600) return [a for a in archives if a.ts > target] diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index b23e277b..14d4783c 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -1,9 +1,9 @@ -import argparse import hashlib import io import os import shutil import sys +from argparse import ArgumentTypeError from datetime import datetime, timezone, timedelta from time import mktime, strptime, sleep @@ -17,7 +17,7 @@ from ..helpers import Location from ..helpers import Buffer from ..helpers import partial_format, format_file_size, parse_file_size, format_timedelta, format_line, PlaceholderError, replace_placeholders from ..helpers import make_path_safe, clean_lines -from ..helpers import prune_within, prune_split +from ..helpers import interval, prune_within, prune_split from ..helpers import get_cache_dir, get_keys_dir, get_security_dir from ..helpers import is_slow_msgpack from ..helpers import yes, TRUISH, FALSISH, DEFAULTISH @@ -368,16 +368,48 @@ class PruneSplitTestCase(BaseTestCase): dotest(test_archives, 0, [], []) -class PruneWithinTestCase(BaseTestCase): +class IntervalTestCase(BaseTestCase): + def test_interval(self): + self.assert_equal(interval('1H'), 1) + self.assert_equal(interval('1d'), 24) + self.assert_equal(interval('1w'), 168) + self.assert_equal(interval('1m'), 744) + self.assert_equal(interval('1y'), 8760) - def test(self): + def test_interval_time_unit(self): + with pytest.raises(ArgumentTypeError) as exc: + interval('H') + self.assert_equal( + exc.value.args, + ('Unexpected interval number "": expected an integer greater than 0',)) + with pytest.raises(ArgumentTypeError) as exc: + interval('-1d') + self.assert_equal( + exc.value.args, + ('Unexpected interval number "-1": expected an integer greater than 0',)) + with pytest.raises(ArgumentTypeError) as exc: + interval('food') + self.assert_equal( + exc.value.args, + ('Unexpected interval number "foo": expected an integer greater than 0',)) + + def test_interval_number(self): + with pytest.raises(ArgumentTypeError) as exc: + interval('5') + self.assert_equal( + exc.value.args, + ("Unexpected interval time unit \"5\": expected one of ['H', 'd', 'w', 'm', 'y']",)) + + +class PruneWithinTestCase(BaseTestCase): + def test_prune_within(self): def subset(lst, indices): return {lst[i] for i in indices} def dotest(test_archives, within, indices): for ta in test_archives, reversed(test_archives): - self.assert_equal(set(prune_within(ta, within)), + self.assert_equal(set(prune_within(ta, interval(within))), subset(test_archives, indices)) # 1 minute, 1.5 hours, 2.5 hours, 3.5 hours, 25 hours, 49 hours From 6c2c51939d30eaab7fc9791f486b54f94de50659 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 23 Jun 2017 05:56:41 +0200 Subject: [PATCH 1138/1387] Manifest: use limited unpacker --- src/borg/constants.py | 5 +++++ src/borg/crypto/key.py | 7 ++++--- src/borg/helpers.py | 4 ++++ src/borg/remote.py | 12 +++++++++++- src/borg/testsuite/key.py | 2 +- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/borg/constants.py b/src/borg/constants.py index 9813a8af..aaf7d044 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -31,6 +31,11 @@ DEFAULT_MAX_SEGMENT_SIZE = 500 * 1024 * 1024 # the header, and the total size was set to 20 MiB). MAX_DATA_SIZE = 20971479 +# to use a safe, limited unpacker, we need to set a upper limit to the archive count in the manifest. +# this does not mean that you can always really reach that number, because it also needs to be less than +# MAX_DATA_SIZE or it will trigger the check for that. +MAX_ARCHIVES = 400000 + DEFAULT_SEGMENTS_PER_DIR = 1000 CHUNK_MIN_EXP = 19 # 2**19 == 512kiB diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index aba1f35b..ea0d5e6a 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -24,6 +24,7 @@ from ..helpers import get_keys_dir, get_security_dir from ..helpers import bin_to_hex from ..item import Key, EncryptedKey from ..platform import SaveFile +from .. import remote from .nonces import NonceManager from .low_level import AES, bytes_to_long, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512 @@ -216,9 +217,9 @@ class KeyBase: logger.warning('Manifest authentication DISABLED.') tam_required = False data = bytearray(data) - # Since we don't trust these bytes we use the slower Python unpacker, - # which is assumed to have a lower probability of security issues. - unpacked = msgpack.fallback.unpackb(data, object_hook=StableDict, unicode_errors='surrogateescape') + unpacker = remote.get_limited_unpacker('manifest') + unpacker.feed(data) + unpacked = unpacker.unpack() if b'tam' not in unpacked: if tam_required: raise TAMRequiredError(self.repository._location.canonical_path()) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 92c573b2..95b00185 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -351,6 +351,10 @@ class Manifest: prev_ts = self.last_timestamp incremented = (prev_ts + timedelta(microseconds=1)).isoformat() self.timestamp = max(incremented, datetime.utcnow().isoformat()) + # include checks for limits as enforced by limited unpacker (used by load()) + assert len(self.archives) <= MAX_ARCHIVES + assert all(len(name) <= 255 for name in self.archives) + assert len(self.item_keys) <= 100 manifest = ManifestItem( version=1, archives=StableDict(self.archives.get_raw_dict()), diff --git a/src/borg/remote.py b/src/borg/remote.py index 670179a4..11a2a251 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -20,6 +20,7 @@ import msgpack from . import __version__ from .compress import LZ4 +from .constants import * # NOQA from .helpers import Error, IntegrityError from .helpers import bin_to_hex from .helpers import get_home_dir @@ -28,6 +29,7 @@ from .helpers import replace_placeholders from .helpers import sysinfo from .helpers import format_file_size from .helpers import truncate_and_unlink +from .helpers import StableDict from .logger import create_logger, setup_logging from .repository import Repository, MAX_OBJECT_SIZE, LIST_SCAN_LIMIT from .version import parse_version, format_version @@ -81,8 +83,16 @@ def get_limited_unpacker(kind): args.update(dict(max_array_len=LIST_SCAN_LIMIT, # result list from repo.list() / .scan() max_map_len=100, # misc. result dicts )) + elif kind == 'manifest': + args.update(dict(use_list=True, # default value + max_array_len=100, # ITEM_KEYS ~= 22 + max_map_len=MAX_ARCHIVES, # list of archives + max_str_len=255, # archive name + object_hook=StableDict, + unicode_errors='surrogateescape', + )) else: - raise ValueError('kind must be "server" or "client"') + raise ValueError('kind must be "server", "client" or "manifest"') return msgpack.Unpacker(**args) diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index df7f81e5..6a7a6c8d 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -331,7 +331,7 @@ class TestTAM: key.unpack_and_verify_manifest(blob) blob = b'\xc1\xc1\xc1' - with pytest.raises(msgpack.UnpackException): + with pytest.raises((ValueError, msgpack.UnpackException)): key.unpack_and_verify_manifest(blob) def test_missing_when_required(self, key): From 89f3cab6cd44903e096fe0237e9b0cc1eeac1e87 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 24 Jun 2017 18:31:34 +0200 Subject: [PATCH 1139/1387] move get_limited_unpacker to helpers also: move some constants to borg.constants --- src/borg/constants.py | 11 +++++++++++ src/borg/crypto/key.py | 4 ++-- src/borg/helpers.py | 29 +++++++++++++++++++++++++++++ src/borg/remote.py | 35 ++--------------------------------- src/borg/repository.py | 7 +------ 5 files changed, 45 insertions(+), 41 deletions(-) diff --git a/src/borg/constants.py b/src/borg/constants.py index aaf7d044..bcdaa07d 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -31,11 +31,22 @@ DEFAULT_MAX_SEGMENT_SIZE = 500 * 1024 * 1024 # the header, and the total size was set to 20 MiB). MAX_DATA_SIZE = 20971479 +# MAX_OBJECT_SIZE = <20 MiB (MAX_DATA_SIZE) + 41 bytes for a Repository PUT header, which consists of +# a 1 byte tag ID, 4 byte CRC, 4 byte size and 32 bytes for the ID. +MAX_OBJECT_SIZE = MAX_DATA_SIZE + 41 # see LoggedIO.put_header_fmt.size assertion in repository module +assert MAX_OBJECT_SIZE == 20971520 == 20 * 1024 * 1024 + +# borg.remote read() buffer size +BUFSIZE = 10 * 1024 * 1024 + # to use a safe, limited unpacker, we need to set a upper limit to the archive count in the manifest. # this does not mean that you can always really reach that number, because it also needs to be less than # MAX_DATA_SIZE or it will trigger the check for that. MAX_ARCHIVES = 400000 +# repo.list() / .scan() result count limit the borg client uses +LIST_SCAN_LIMIT = 100000 + DEFAULT_SEGMENTS_PER_DIR = 1000 CHUNK_MIN_EXP = 19 # 2**19 == 512kiB diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index ea0d5e6a..256a3c40 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -21,10 +21,10 @@ from ..helpers import StableDict from ..helpers import Error, IntegrityError from ..helpers import yes from ..helpers import get_keys_dir, get_security_dir +from ..helpers import get_limited_unpacker from ..helpers import bin_to_hex from ..item import Key, EncryptedKey from ..platform import SaveFile -from .. import remote from .nonces import NonceManager from .low_level import AES, bytes_to_long, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512 @@ -217,7 +217,7 @@ class KeyBase: logger.warning('Manifest authentication DISABLED.') tam_required = False data = bytearray(data) - unpacker = remote.get_limited_unpacker('manifest') + unpacker = get_limited_unpacker('manifest') unpacker.feed(data) unpacked = unpacker.unpack() if b'tam' not in unpacked: diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 95b00185..1554eefe 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -145,6 +145,35 @@ def check_extension_modules(): raise ExtensionModuleError +def get_limited_unpacker(kind): + """return a limited Unpacker because we should not trust msgpack data received from remote""" + args = dict(use_list=False, # return tuples, not lists + max_bin_len=0, # not used + max_ext_len=0, # not used + max_buffer_size=3 * max(BUFSIZE, MAX_OBJECT_SIZE), + max_str_len=MAX_OBJECT_SIZE, # a chunk or other repo object + ) + if kind == 'server': + args.update(dict(max_array_len=100, # misc. cmd tuples + max_map_len=100, # misc. cmd dicts + )) + elif kind == 'client': + args.update(dict(max_array_len=LIST_SCAN_LIMIT, # result list from repo.list() / .scan() + max_map_len=100, # misc. result dicts + )) + elif kind == 'manifest': + args.update(dict(use_list=True, # default value + max_array_len=100, # ITEM_KEYS ~= 22 + max_map_len=MAX_ARCHIVES, # list of archives + max_str_len=255, # archive name + object_hook=StableDict, + unicode_errors='surrogateescape', + )) + else: + raise ValueError('kind must be "server", "client" or "manifest"') + return msgpack.Unpacker(**args) + + ArchiveInfo = namedtuple('ArchiveInfo', 'name id ts') diff --git a/src/borg/remote.py b/src/borg/remote.py index 11a2a251..d131a826 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -24,14 +24,14 @@ from .constants import * # NOQA from .helpers import Error, IntegrityError from .helpers import bin_to_hex from .helpers import get_home_dir +from .helpers import get_limited_unpacker from .helpers import hostname_is_unique from .helpers import replace_placeholders from .helpers import sysinfo from .helpers import format_file_size from .helpers import truncate_and_unlink -from .helpers import StableDict from .logger import create_logger, setup_logging -from .repository import Repository, MAX_OBJECT_SIZE, LIST_SCAN_LIMIT +from .repository import Repository from .version import parse_version, format_version from .algorithms.checksums import xxh64 @@ -41,8 +41,6 @@ RPC_PROTOCOL_VERSION = 2 BORG_VERSION = parse_version(__version__) MSGID, MSG, ARGS, RESULT = b'i', b'm', b'a', b'r' -BUFSIZE = 10 * 1024 * 1024 - MAX_INFLIGHT = 100 RATELIMIT_PERIOD = 0.1 @@ -67,35 +65,6 @@ def os_write(fd, data): return amount -def get_limited_unpacker(kind): - """return a limited Unpacker because we should not trust msgpack data received from remote""" - args = dict(use_list=False, # return tuples, not lists - max_bin_len=0, # not used - max_ext_len=0, # not used - max_buffer_size=3 * max(BUFSIZE, MAX_OBJECT_SIZE), - max_str_len=MAX_OBJECT_SIZE, # a chunk or other repo object - ) - if kind == 'server': - args.update(dict(max_array_len=100, # misc. cmd tuples - max_map_len=100, # misc. cmd dicts - )) - elif kind == 'client': - args.update(dict(max_array_len=LIST_SCAN_LIMIT, # result list from repo.list() / .scan() - max_map_len=100, # misc. result dicts - )) - elif kind == 'manifest': - args.update(dict(use_list=True, # default value - max_array_len=100, # ITEM_KEYS ~= 22 - max_map_len=MAX_ARCHIVES, # list of archives - max_str_len=255, # archive name - object_hook=StableDict, - unicode_errors='surrogateescape', - )) - else: - raise ValueError('kind must be "server", "client" or "manifest"') - return msgpack.Unpacker(**args) - - class ConnectionClosed(Error): """Connection closed by remote host""" diff --git a/src/borg/repository.py b/src/borg/repository.py index f73e9cf5..5a3aa735 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -34,8 +34,6 @@ TAG_PUT = 0 TAG_DELETE = 1 TAG_COMMIT = 2 -LIST_SCAN_LIMIT = 100000 # repo.list() / .scan() result count limit the borg client uses - FreeSpace = partial(defaultdict, int) @@ -1411,7 +1409,4 @@ class LoggedIO: return self.segment - 1 # close_segment() increments it -# MAX_OBJECT_SIZE = <20 MiB (MAX_DATA_SIZE) + 41 bytes for a Repository PUT header, which consists of -# a 1 byte tag ID, 4 byte CRC, 4 byte size and 32 bytes for the ID. -MAX_OBJECT_SIZE = MAX_DATA_SIZE + LoggedIO.put_header_fmt.size -assert MAX_OBJECT_SIZE == 20971520 == 20 * 1024 * 1024 +assert LoggedIO.put_header_fmt.size == 41 # see constants.MAX_OBJECT_SIZE From cbeda1d8e361994345a78af332a032c089874202 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 26 Jun 2017 21:04:39 +0200 Subject: [PATCH 1140/1387] archiver: more argparse cleanup, redundant options, missing metavars --- src/borg/archiver.py | 133 +++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 82 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 91f2fcac..c4860cae 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2491,12 +2491,12 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False), help='repository to create') - subparser.add_argument('-e', '--encryption', dest='encryption', required=True, + subparser.add_argument('-e', '--encryption', dest='encryption', required=True, metavar='MODE', choices=('none', 'keyfile', 'repokey', 'keyfile-blake2', 'repokey-blake2', 'authenticated'), help='select encryption key mode **(required)**') subparser.add_argument('--append-only', dest='append_only', action='store_true', help='create an append-only mode repository') - subparser.add_argument('--storage-quota', dest='storage_quota', default=None, + subparser.add_argument('--storage-quota', dest='storage_quota', default=None, metavar='QUOTA', type=parse_storage_quota, help='Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota.') @@ -2557,20 +2557,15 @@ class Archiver: type=location_validator(), help='repository or archive to check consistency of') subparser.add_argument('--repository-only', dest='repo_only', action='store_true', - default=False, help='only perform repository checks') subparser.add_argument('--archives-only', dest='archives_only', action='store_true', - default=False, help='only perform archives checks') subparser.add_argument('--verify-data', dest='verify_data', action='store_true', - default=False, help='perform cryptographic archive data integrity verification ' '(conflicts with ``--repository-only``)') subparser.add_argument('--repair', dest='repair', action='store_true', - default=False, help='attempt to repair any inconsistencies found') subparser.add_argument('--save-space', dest='save_space', action='store_true', - default=False, help='work slower, but using less space') self.add_archives_filters_args(subparser) @@ -2613,10 +2608,8 @@ class Archiver: subparser.add_argument('path', metavar='PATH', nargs='?', type=str, help='where to store the backup') subparser.add_argument('--paper', dest='paper', action='store_true', - default=False, help='Create an export suitable for printing and later type-in') subparser.add_argument('--qr-html', dest='qr', action='store_true', - default=False, help='Create an html file suitable for printing and later type-in or qr scan') key_import_epilog = process_epilog(""" @@ -2638,7 +2631,6 @@ class Archiver: subparser.add_argument('path', metavar='PATH', nargs='?', type=str, help='path to the backup') subparser.add_argument('--paper', dest='paper', action='store_true', - default=False, help='interactively import from a backup done with ``--paper``') change_passphrase_epilog = process_epilog(""" @@ -2790,15 +2782,11 @@ class Archiver: help='create backup') subparser.set_defaults(func=self.do_create) - subparser.add_argument('-n', '--dry-run', dest='dry_run', - action='store_true', default=False, + subparser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true', help='do not create a backup archive') - - subparser.add_argument('-s', '--stats', dest='stats', - action='store_true', default=False, + subparser.add_argument('-s', '--stats', dest='stats', action='store_true', help='print statistics for the created archive') - subparser.add_argument('--list', dest='output_list', - action='store_true', default=False, + subparser.add_argument('--list', dest='output_list', action='store_true', help='output verbose list of items (files, dirs, ...)') subparser.add_argument('--filter', dest='output_filter', metavar='STATUSCHARS', help='only display items with the given status characters') @@ -2808,47 +2796,38 @@ class Archiver: help='experimental: do not synchronize the cache. Implies ``--no-files-cache``.') exclude_group = subparser.add_argument_group('Exclusion options') - exclude_group.add_argument('-e', '--exclude', dest='patterns', - type=parse_exclude_pattern, action='append', - metavar="PATTERN", help='exclude paths matching PATTERN') - exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction, - metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') - exclude_group.add_argument('--exclude-caches', dest='exclude_caches', - action='store_true', default=False, + exclude_group.add_argument('-e', '--exclude', metavar='PATTERN', dest='patterns', + type=parse_exclude_pattern, action='append', help='exclude paths matching PATTERN') + exclude_group.add_argument('--exclude-from', metavar='EXCLUDEFILE', action=ArgparseExcludeFileAction, + help='read exclude patterns from EXCLUDEFILE, one per line') + exclude_group.add_argument('--exclude-caches', dest='exclude_caches', action='store_true', help='exclude directories that contain a CACHEDIR.TAG file (' 'http://www.brynosaurus.com/cachedir/spec.html)') - exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', - metavar='NAME', action='append', type=str, + exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', metavar='NAME', + action='append', type=str, help='exclude directories that are tagged by containing a filesystem object with ' 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', - action='store_true', default=False, + action='store_true', help='if tag objects are specified with ``--exclude-if-present``, ' 'don\'t omit the tag objects themselves from the backup archive') - exclude_group.add_argument('--pattern', - action=ArgparsePatternAction, - metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') - exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction, - metavar='PATTERNFILE', help='experimental: read include/exclude patterns from PATTERNFILE, one per line') + exclude_group.add_argument('--pattern', metavar='PATTERN', action=ArgparsePatternAction, + help='experimental: include/exclude paths matching PATTERN') + exclude_group.add_argument('--patterns-from', metavar='PATTERNFILE', action=ArgparsePatternFileAction, + help='experimental: read include/exclude patterns from PATTERNFILE, one per line') fs_group = subparser.add_argument_group('Filesystem options') - fs_group.add_argument('-x', '--one-file-system', dest='one_file_system', - action='store_true', default=False, + fs_group.add_argument('-x', '--one-file-system', dest='one_file_system', action='store_true', help='stay in the same file system and do not store mount points of other file systems') - fs_group.add_argument('--numeric-owner', dest='numeric_owner', - action='store_true', default=False, + fs_group.add_argument('--numeric-owner', dest='numeric_owner', action='store_true', help='only store numeric user and group identifiers') - fs_group.add_argument('--noatime', dest='noatime', - action='store_true', default=False, + fs_group.add_argument('--noatime', dest='noatime', action='store_true', help='do not store atime into archive') - fs_group.add_argument('--noctime', dest='noctime', - action='store_true', default=False, + fs_group.add_argument('--noctime', dest='noctime', action='store_true', help='do not store ctime into archive') - fs_group.add_argument('--ignore-inode', dest='ignore_inode', - action='store_true', default=False, + fs_group.add_argument('--ignore-inode', dest='ignore_inode', action='store_true', help='ignore inode data in the file metadata cache used to detect unchanged files.') - fs_group.add_argument('--read-special', dest='read_special', - action='store_true', default=False, + fs_group.add_argument('--read-special', dest='read_special', action='store_true', help='open and read block and char device files as well as FIFOs as if they were ' 'regular files. Also follows symlinks pointing to these kinds of files.') @@ -2860,16 +2839,15 @@ class Archiver: metavar='TIMESTAMP', help='manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). ' 'alternatively, give a reference file/directory.') - archive_group.add_argument('-c', '--checkpoint-interval', dest='checkpoint_interval', - type=int, default=1800, metavar='SECONDS', + archive_group.add_argument('-c', '--checkpoint-interval', metavar='SECONDS', dest='checkpoint_interval', + type=int, default=1800, help='write checkpoint every SECONDS seconds (Default: 1800)') - archive_group.add_argument('--chunker-params', dest='chunker_params', + archive_group.add_argument('--chunker-params', metavar='PARAMS', dest='chunker_params', type=ChunkerParams, default=CHUNKER_PARAMS, - metavar='PARAMS', help='specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, ' 'HASH_MASK_BITS, HASH_WINDOW_SIZE). default: %d,%d,%d,%d' % CHUNKER_PARAMS) - archive_group.add_argument('-C', '--compression', dest='compression', - type=CompressionSpec, default=CompressionSpec('lz4'), metavar='COMPRESSION', + archive_group.add_argument('-C', '--compression', metavar='COMPRESSION', dest='compression', + type=CompressionSpec, default=CompressionSpec('lz4'), help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') @@ -2905,32 +2883,28 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='extract archive contents') subparser.set_defaults(func=self.do_extract) - subparser.add_argument('--list', dest='output_list', - action='store_true', default=False, + subparser.add_argument('--list', dest='output_list', action='store_true', help='output verbose list of items (files, dirs, ...)') - subparser.add_argument('-n', '--dry-run', dest='dry_run', - default=False, action='store_true', + subparser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true', help='do not actually change any files') - subparser.add_argument('-e', '--exclude', dest='patterns', + subparser.add_argument('-e', '--exclude', metavar='PATTERN', dest='patterns', type=parse_exclude_pattern, action='append', - metavar="PATTERN", help='exclude paths matching PATTERN') - subparser.add_argument('--exclude-from', action=ArgparseExcludeFileAction, - metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') - subparser.add_argument('--pattern', action=ArgparsePatternAction, - metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') - subparser.add_argument('--patterns-from', action=ArgparsePatternFileAction, - metavar='PATTERNFILE', help='experimental: read include/exclude patterns from PATTERNFILE, one per line') - subparser.add_argument('--numeric-owner', dest='numeric_owner', - action='store_true', default=False, + help='exclude paths matching PATTERN') + subparser.add_argument('--exclude-from', metavar='EXCLUDEFILE', action=ArgparseExcludeFileAction, + help='read exclude patterns from EXCLUDEFILE, one per line') + subparser.add_argument('--pattern', metavar='PATTERN', action=ArgparsePatternAction, + help='experimental: include/exclude paths matching PATTERN') + subparser.add_argument('--patterns-from', metavar='PATTERNFILE', action=ArgparsePatternFileAction, + help='experimental: read include/exclude patterns from PATTERNFILE, one per line') + subparser.add_argument('--numeric-owner', dest='numeric_owner', action='store_true', help='only obey numeric user and group identifiers') - subparser.add_argument('--strip-components', dest='strip_components', - type=int, default=0, metavar='NUMBER', - help='Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped.') - subparser.add_argument('--stdout', dest='stdout', - action='store_true', default=False, + subparser.add_argument('--strip-components', metavar='NUMBER', dest='strip_components', + type=int, default=0, + help='Remove the specified number of leading path elements. Pathnames with fewer ' + 'elements will be silently skipped.') + subparser.add_argument('--stdout', dest='stdout', action='store_true', help='write all extracted data to stdout') - subparser.add_argument('--sparse', dest='sparse', - action='store_true', default=False, + subparser.add_argument('--sparse', dest='sparse', action='store_true', help='create holes in output sparse file from all-zero chunks') subparser.add_argument('location', metavar='ARCHIVE', type=location_validator(archive=True), @@ -2981,8 +2955,7 @@ class Archiver: subparser.set_defaults(func=self.do_export_tar) subparser.add_argument('--tar-filter', dest='tar_filter', default='auto', help='filter program to pipe data through') - subparser.add_argument('--list', dest='output_list', - action='store_true', default=False, + subparser.add_argument('--list', dest='output_list', action='store_true', help='output verbose list of items (files, dirs, ...)') subparser.add_argument('-e', '--exclude', dest='patterns', type=parse_exclude_pattern, action='append', @@ -3028,14 +3001,11 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='find differences in archive contents') subparser.set_defaults(func=self.do_diff) - subparser.add_argument('--numeric-owner', dest='numeric_owner', - action='store_true', default=False, + subparser.add_argument('--numeric-owner', dest='numeric_owner', action='store_true', help='only consider numeric user and group identifiers') - subparser.add_argument('--same-chunker-params', dest='same_chunker_params', - action='store_true', default=False, + subparser.add_argument('--same-chunker-params', dest='same_chunker_params', action='store_true', help='Override check of chunker parameters.') - subparser.add_argument('--sort', dest='sort', - action='store_true', default=False, + subparser.add_argument('--sort', dest='sort', action='store_true', help='Sort the output lines by file path.') subparser.add_argument('location', metavar='REPO_ARCHIVE1', type=location_validator(archive=True), @@ -3053,7 +3023,7 @@ class Archiver: exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction, metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') exclude_group.add_argument('--exclude-caches', dest='exclude_caches', - action='store_true', default=False, + action='store_true', help='exclude directories that contain a CACHEDIR.TAG file (' 'http://www.brynosaurus.com/cachedir/spec.html)') exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', @@ -3061,7 +3031,7 @@ class Archiver: help='exclude directories that are tagged by containing a filesystem object with ' 'the given NAME') exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', - action='store_true', default=False, + action='store_true', help='if tag objects are specified with ``--exclude-if-present``, ' 'don\'t omit the tag objects themselves from the backup archive') exclude_group.add_argument('--pattern', @@ -3108,7 +3078,6 @@ class Archiver: help='force deletion of corrupted archives, ' 'use ``--force --force`` in case ``--force`` does not work.') subparser.add_argument('--save-space', dest='save_space', action='store_true', - default=False, help='work slower, but using less space') subparser.add_argument('location', metavar='TARGET', nargs='?', default='', type=location_validator(), From 8aba5772b5fdc001e0a02b62f80e851415c0134e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 26 Jun 2017 22:23:02 +0200 Subject: [PATCH 1141/1387] include attic.tar.gz when installing the package --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f2b4d977..6da76df4 100644 --- a/setup.py +++ b/setup.py @@ -776,7 +776,6 @@ setup( ], packages=find_packages('src'), package_dir={'': 'src'}, - include_package_data=True, zip_safe=False, entry_points={ 'console_scripts': [ @@ -784,8 +783,10 @@ setup( 'borgfs = borg.archiver:main', ] }, + include_package_data=True, package_data={ - 'borg': ['paperkey.html'] + 'borg': ['paperkey.html'], + 'borg.testsuite': ['attic.tar.gz'], }, cmdclass=cmdclass, ext_modules=ext_modules, From 0fabefdb59206faab9d27875863b94e346ec42bb Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 26 Jun 2017 22:48:55 +0200 Subject: [PATCH 1142/1387] archiver: define_exclusion_group to avoid repetition --- src/borg/archiver.py | 154 +++++++++++-------------------------------- 1 file changed, 39 insertions(+), 115 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index c4860cae..14e6f6b8 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2333,6 +2333,39 @@ class Archiver: help='Write execution profile in Borg format into FILE. For local use a Python-' 'compatible file can be generated by suffixing FILE with ".pyprof".') + def define_exclude_and_patterns(add_option, *, tag_files=False, strip_components=False): + add_option('-e', '--exclude', metavar='PATTERN', dest='patterns', + type=parse_exclude_pattern, action='append', + help='exclude paths matching PATTERN') + add_option('--exclude-from', metavar='EXCLUDEFILE', action=ArgparseExcludeFileAction, + help='read exclude patterns from EXCLUDEFILE, one per line') + add_option('--pattern', metavar='PATTERN', action=ArgparsePatternAction, + help='experimental: include/exclude paths matching PATTERN') + add_option('--patterns-from', metavar='PATTERNFILE', action=ArgparsePatternFileAction, + help='experimental: read include/exclude patterns from PATTERNFILE, one per line') + + if tag_files: + add_option('--exclude-caches', dest='exclude_caches', action='store_true', + help='exclude directories that contain a CACHEDIR.TAG file ' + '(http://www.brynosaurus.com/cachedir/spec.html)') + add_option('--exclude-if-present', metavar='NAME', dest='exclude_if_present', + action='append', type=str, + help='exclude directories that are tagged by containing a filesystem object with ' + 'the given NAME') + add_option('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', + action='store_true', + help='if tag objects are specified with ``--exclude-if-present``, ' + 'don\'t omit the tag objects themselves from the backup archive') + + if strip_components: + add_option('--strip-components', metavar='NUMBER', dest='strip_components', type=int, default=0, + help='Remove the specified number of leading path elements. ' + 'Paths with fewer elements will be silently skipped.') + + def define_exclusion_group(subparser, **kwargs): + exclude_group = subparser.add_argument_group('Exclusion options') + define_exclude_and_patterns(exclude_group.add_argument, **kwargs) + parser = argparse.ArgumentParser(prog=self.prog, description='Borg - Deduplicated Backups', add_help=False) parser.common_options = self.CommonOptions(define_common_options, @@ -2795,26 +2828,7 @@ class Archiver: subparser.add_argument('--no-cache-sync', dest='no_cache_sync', action='store_true', help='experimental: do not synchronize the cache. Implies ``--no-files-cache``.') - exclude_group = subparser.add_argument_group('Exclusion options') - exclude_group.add_argument('-e', '--exclude', metavar='PATTERN', dest='patterns', - type=parse_exclude_pattern, action='append', help='exclude paths matching PATTERN') - exclude_group.add_argument('--exclude-from', metavar='EXCLUDEFILE', action=ArgparseExcludeFileAction, - help='read exclude patterns from EXCLUDEFILE, one per line') - exclude_group.add_argument('--exclude-caches', dest='exclude_caches', action='store_true', - help='exclude directories that contain a CACHEDIR.TAG file (' - 'http://www.brynosaurus.com/cachedir/spec.html)') - exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', metavar='NAME', - action='append', type=str, - help='exclude directories that are tagged by containing a filesystem object with ' - 'the given NAME') - exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', - action='store_true', - help='if tag objects are specified with ``--exclude-if-present``, ' - 'don\'t omit the tag objects themselves from the backup archive') - exclude_group.add_argument('--pattern', metavar='PATTERN', action=ArgparsePatternAction, - help='experimental: include/exclude paths matching PATTERN') - exclude_group.add_argument('--patterns-from', metavar='PATTERNFILE', action=ArgparsePatternFileAction, - help='experimental: read include/exclude patterns from PATTERNFILE, one per line') + define_exclusion_group(subparser, tag_files=True) fs_group = subparser.add_argument_group('Filesystem options') fs_group.add_argument('-x', '--one-file-system', dest='one_file_system', action='store_true', @@ -2887,21 +2901,8 @@ class Archiver: help='output verbose list of items (files, dirs, ...)') subparser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true', help='do not actually change any files') - subparser.add_argument('-e', '--exclude', metavar='PATTERN', dest='patterns', - type=parse_exclude_pattern, action='append', - help='exclude paths matching PATTERN') - subparser.add_argument('--exclude-from', metavar='EXCLUDEFILE', action=ArgparseExcludeFileAction, - help='read exclude patterns from EXCLUDEFILE, one per line') - subparser.add_argument('--pattern', metavar='PATTERN', action=ArgparsePatternAction, - help='experimental: include/exclude paths matching PATTERN') - subparser.add_argument('--patterns-from', metavar='PATTERNFILE', action=ArgparsePatternFileAction, - help='experimental: read include/exclude patterns from PATTERNFILE, one per line') subparser.add_argument('--numeric-owner', dest='numeric_owner', action='store_true', help='only obey numeric user and group identifiers') - subparser.add_argument('--strip-components', metavar='NUMBER', dest='strip_components', - type=int, default=0, - help='Remove the specified number of leading path elements. Pathnames with fewer ' - 'elements will be silently skipped.') subparser.add_argument('--stdout', dest='stdout', action='store_true', help='write all extracted data to stdout') subparser.add_argument('--sparse', dest='sparse', action='store_true', @@ -2911,6 +2912,7 @@ class Archiver: help='archive to extract') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to extract; patterns are supported') + define_exclusion_group(subparser, strip_components=True) export_tar_epilog = process_epilog(""" This command creates a tarball from an archive. @@ -2957,18 +2959,6 @@ class Archiver: help='filter program to pipe data through') subparser.add_argument('--list', dest='output_list', action='store_true', help='output verbose list of items (files, dirs, ...)') - subparser.add_argument('-e', '--exclude', dest='patterns', - type=parse_exclude_pattern, action='append', - metavar="PATTERN", help='exclude paths matching PATTERN') - subparser.add_argument('--exclude-from', action=ArgparseExcludeFileAction, - metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') - subparser.add_argument('--pattern', action=ArgparsePatternAction, - metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') - subparser.add_argument('--patterns-from', action=ArgparsePatternFileAction, - metavar='PATTERNFILE', help='experimental: read include/exclude patterns from PATTERNFILE, one per line') - subparser.add_argument('--strip-components', dest='strip_components', - type=int, default=0, metavar='NUMBER', - help='Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped.') subparser.add_argument('location', metavar='ARCHIVE', type=location_validator(archive=True), help='archive to export') @@ -2976,6 +2966,7 @@ class Archiver: help='output tar file. "-" to write to stdout instead.') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to extract; patterns are supported') + define_exclusion_group(subparser, strip_components=True) diff_epilog = process_epilog(""" This command finds differences (file contents, user/group/mode) between archives. @@ -3015,30 +3006,7 @@ class Archiver: help='ARCHIVE2 name (no repository location allowed)') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths of items inside the archives to compare; patterns are supported') - - exclude_group = subparser.add_argument_group('Exclusion options') - exclude_group.add_argument('-e', '--exclude', dest='patterns', - type=parse_exclude_pattern, action='append', - metavar="PATTERN", help='exclude paths matching PATTERN') - exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction, - metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') - exclude_group.add_argument('--exclude-caches', dest='exclude_caches', - action='store_true', - help='exclude directories that contain a CACHEDIR.TAG file (' - 'http://www.brynosaurus.com/cachedir/spec.html)') - exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', - metavar='NAME', action='append', type=str, - help='exclude directories that are tagged by containing a filesystem object with ' - 'the given NAME') - exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', - action='store_true', - help='if tag objects are specified with ``--exclude-if-present``, ' - 'don\'t omit the tag objects themselves from the backup archive') - exclude_group.add_argument('--pattern', - action=ArgparsePatternAction, - metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') - exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction, - metavar='PATTERNFILE', help='experimental: read include/exclude patterns from PATTERNFILE, one per line') + define_exclusion_group(subparser, tag_files=True) rename_epilog = process_epilog(""" This command renames an archive in the repository. @@ -3132,29 +3100,7 @@ class Archiver: subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to list; patterns are supported') self.add_archives_filters_args(subparser) - - exclude_group = subparser.add_argument_group('Exclusion options') - exclude_group.add_argument('-e', '--exclude', dest='patterns', - type=parse_exclude_pattern, action='append', - metavar="PATTERN", help='exclude paths matching PATTERN') - exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction, - metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') - exclude_group.add_argument('--exclude-caches', dest='exclude_caches', action='store_true', - help='exclude directories that contain a CACHEDIR.TAG file (' - 'http://www.brynosaurus.com/cachedir/spec.html)') - exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', - metavar='NAME', action='append', type=str, - help='exclude directories that are tagged by containing a filesystem object with ' - 'the given NAME') - exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', - action='store_true', - help='if tag objects are specified with ``--exclude-if-present``, don\'t omit the tag ' - 'objects themselves from the backup archive') - exclude_group.add_argument('--pattern', - action=ArgparsePatternAction, - metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') - exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction, - metavar='PATTERNFILE', help='experimental: read include/exclude patterns from PATTERNFILE, one per line') + define_exclusion_group(subparser, tag_files=True) mount_epilog = process_epilog(""" This command mounts an archive as a FUSE filesystem. This can be useful for @@ -3495,29 +3441,7 @@ class Archiver: subparser.add_argument('-s', '--stats', dest='stats', action='store_true', help='print statistics at end') - exclude_group = subparser.add_argument_group('Exclusion options') - exclude_group.add_argument('-e', '--exclude', dest='patterns', - type=parse_exclude_pattern, action='append', - metavar="PATTERN", help='exclude paths matching PATTERN') - exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction, - metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line') - exclude_group.add_argument('--exclude-caches', dest='exclude_caches', - action='store_true', - help='exclude directories that contain a CACHEDIR.TAG file (' - 'http://www.brynosaurus.com/cachedir/spec.html)') - exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present', - metavar='NAME', action='append', type=str, - help='exclude directories that are tagged by containing a filesystem object with ' - 'the given NAME') - exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags', - action='store_true', - help='if tag objects are specified with ``--exclude-if-present``, don\'t omit the tag ' - 'objects themselves from the backup archive') - exclude_group.add_argument('--pattern', - action=ArgparsePatternAction, - metavar="PATTERN", help='experimental: include/exclude paths matching PATTERN') - exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction, - metavar='PATTERNFILE', help='experimental: read include/exclude patterns from PATTERNFILE, one per line') + define_exclusion_group(subparser, tag_files=True) archive_group = subparser.add_argument_group('Archive options') archive_group.add_argument('--target', dest='target', metavar='TARGET', default=None, From 0a496c10649fc6b284c92851c3246203ca8d3d25 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 26 Jun 2017 22:50:57 +0200 Subject: [PATCH 1143/1387] archiver: define_archive_filters_group --- src/borg/archiver.py | 62 ++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 14e6f6b8..0afe89f7 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2366,6 +2366,31 @@ class Archiver: exclude_group = subparser.add_argument_group('Exclusion options') define_exclude_and_patterns(exclude_group.add_argument, **kwargs) + def define_archive_filters_group(subparser, *, sort_by=True, first_last=True): + filters_group = subparser.add_argument_group('filters', + 'Archive filters can be applied to repository targets.') + group = filters_group.add_mutually_exclusive_group() + group.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, default='', metavar='PREFIX', + help='only consider archive names starting with this prefix.') + group.add_argument('-a', '--glob-archives', dest='glob_archives', default=None, metavar='GLOB', + help='only consider archive names matching the glob. ' + 'sh: rules apply, see "borg help patterns". ' + '``--prefix`` and ``--glob-archives`` are mutually exclusive.') + + if sort_by: + sort_by_default = 'timestamp' + filters_group.add_argument('--sort-by', dest='sort_by', type=SortBySpec, default=sort_by_default, + metavar='KEYS', + help='Comma-separated list of sorting keys; valid keys are: {}; default is: {}' + .format(', '.join(HUMAN_SORT_KEYS), sort_by_default)) + + if first_last: + group = filters_group.add_mutually_exclusive_group() + group.add_argument('--first', dest='first', metavar='N', default=0, type=int, + help='consider first N archives after other filters were applied') + group.add_argument('--last', dest='last', metavar='N', default=0, type=int, + help='consider last N archives after other filters were applied') + parser = argparse.ArgumentParser(prog=self.prog, description='Borg - Deduplicated Backups', add_help=False) parser.common_options = self.CommonOptions(define_common_options, @@ -2600,7 +2625,7 @@ class Archiver: help='attempt to repair any inconsistencies found') subparser.add_argument('--save-space', dest='save_space', action='store_true', help='work slower, but using less space') - self.add_archives_filters_args(subparser) + define_archive_filters_group(subparser) subparser = subparsers.add_parser('key', parents=[mid_common_parser], add_help=False, description="Manage a keyfile or repokey of a repository", @@ -3050,7 +3075,7 @@ class Archiver: subparser.add_argument('location', metavar='TARGET', nargs='?', default='', type=location_validator(), help='archive or repository to delete') - self.add_archives_filters_args(subparser) + define_archive_filters_group(subparser) list_epilog = process_epilog(""" This command lists the contents of a repository or an archive. @@ -3099,7 +3124,7 @@ class Archiver: help='repository/archive to list contents of') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to list; patterns are supported') - self.add_archives_filters_args(subparser) + define_archive_filters_group(subparser) define_exclusion_group(subparser, tag_files=True) mount_epilog = process_epilog(""" @@ -3151,7 +3176,7 @@ class Archiver: help='stay in foreground, do not daemonize') subparser.add_argument('-o', dest='options', type=str, help='Extra mount options') - self.add_archives_filters_args(subparser) + define_archive_filters_group(subparser) umount_epilog = process_epilog(""" This command un-mounts a FUSE filesystem that was mounted with ``borg mount``. @@ -3191,7 +3216,7 @@ class Archiver: help='archive or repository to display information about') subparser.add_argument('--json', action='store_true', help='format output as JSON') - self.add_archives_filters_args(subparser) + define_archive_filters_group(subparser) break_lock_epilog = process_epilog(""" This command breaks the repository and cache locks. @@ -3278,7 +3303,7 @@ class Archiver: help='number of monthly archives to keep') subparser.add_argument('-y', '--keep-yearly', dest='yearly', type=int, default=0, help='number of yearly archives to keep') - self.add_archives_filters_args(subparser, sort_by=False, first_last=False) + define_archive_filters_group(subparser, sort_by=False, first_last=False) subparser.add_argument('--save-space', dest='save_space', action='store_true', help='work slower, but using less space') subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', @@ -3743,31 +3768,6 @@ class Archiver: return parser - @staticmethod - def add_archives_filters_args(subparser, sort_by=True, first_last=True): - filters_group = subparser.add_argument_group('filters', 'Archive filters can be applied to repository targets.') - group = filters_group.add_mutually_exclusive_group() - group.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, default='', metavar='PREFIX', - help='only consider archive names starting with this prefix.') - group.add_argument('-a', '--glob-archives', dest='glob_archives', default=None, metavar='GLOB', - help='only consider archive names matching the glob. ' - 'sh: rules apply, see "borg help patterns". ' - '``--prefix`` and ``--glob-archives`` are mutually exclusive.') - - if sort_by: - sort_by_default = 'timestamp' - filters_group.add_argument('--sort-by', dest='sort_by', type=SortBySpec, default=sort_by_default, - metavar='KEYS', - help='Comma-separated list of sorting keys; valid keys are: {}; default is: {}' - .format(', '.join(HUMAN_SORT_KEYS), sort_by_default)) - - if first_last: - group = filters_group.add_mutually_exclusive_group() - group.add_argument('--first', dest='first', metavar='N', default=0, type=int, - help='consider first N archives after other filters were applied') - group.add_argument('--last', dest='last', metavar='N', default=0, type=int, - help='consider last N archives after other filters were applied') - def get_args(self, argv, cmd): """usually, just returns argv, except if we deal with a ssh forced command for borg serve.""" result = self.parse_args(argv[1:]) From 12bcccc0d6af2e6763508e734ba243ff5f633559 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 26 Jun 2017 23:10:12 +0200 Subject: [PATCH 1144/1387] docs: html: include group description in output --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f2b4d977..43f73e4f 100644 --- a/setup.py +++ b/setup.py @@ -317,7 +317,10 @@ class build_usage(Command): else: if not group._group_actions: continue - rows.append((1, '**%s**' % group.title)) + group_header = '**%s**' % group.title + if group.description: + group_header += ' — ' + group.description + rows.append((1, group_header)) if is_positional_group(group): for option in group._group_actions: rows.append((3, '', '``%s``' % option.metavar, option.help or '')) From 97a76c296fc3bfd5469b6acf90ecc5634eda7ced Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 26 Jun 2017 23:10:43 +0200 Subject: [PATCH 1145/1387] archiver: rename "filters" argument group to "Archive filters" --- src/borg/archiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 0afe89f7..974f5885 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2367,7 +2367,7 @@ class Archiver: define_exclude_and_patterns(exclude_group.add_argument, **kwargs) def define_archive_filters_group(subparser, *, sort_by=True, first_last=True): - filters_group = subparser.add_argument_group('filters', + filters_group = subparser.add_argument_group('Archive filters', 'Archive filters can be applied to repository targets.') group = filters_group.add_mutually_exclusive_group() group.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, default='', metavar='PREFIX', From 39a09123ef334bed4a1d9a861afc1f5b60573991 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Mon, 26 Jun 2017 23:20:24 +0200 Subject: [PATCH 1146/1387] archiver: more consistent arguments formatting --- src/borg/archiver.py | 85 +++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 974f5885..25b71168 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2304,32 +2304,30 @@ class Archiver: add_common_option('--debug', dest='log_level', action='store_const', const='debug', default='warning', help='enable debug output, work on log level DEBUG') - add_common_option('--debug-topic', dest='debug_topics', - action='append', metavar='TOPIC', default=[], + add_common_option('--debug-topic', metavar='TOPIC', dest='debug_topics', action='append', default=[], help='enable TOPIC debugging (can be specified multiple times). ' 'The logger path is borg.debug. if TOPIC is not fully qualified.') add_common_option('-p', '--progress', dest='progress', action='store_true', help='show progress information') add_common_option('--log-json', dest='log_json', action='store_true', help='Output one JSON object per log line instead of formatted text.') - add_common_option('--lock-wait', dest='lock_wait', type=int, metavar='N', default=1, - help='wait for the lock, but max. N seconds (default: %(default)d).') + add_common_option('--lock-wait', metavar='SECONDS', dest='lock_wait', type=int, default=1, + help='wait at most SECONDS for acquiring a repository/cache lock (default: %(default)d).') add_common_option('--show-version', dest='show_version', action='store_true', help='show/log the borg version') add_common_option('--show-rc', dest='show_rc', action='store_true', help='show/log the return code (rc)') add_common_option('--no-files-cache', dest='cache_files', action='store_false', help='do not load/update the file metadata cache used to detect unchanged files') - add_common_option('--umask', dest='umask', type=lambda s: int(s, 8), default=UMASK_DEFAULT, metavar='M', + add_common_option('--umask', metavar='M', dest='umask', type=lambda s: int(s, 8), default=UMASK_DEFAULT, help='set umask to M (local and remote, default: %(default)04o)') - add_common_option('--remote-path', dest='remote_path', metavar='PATH', + add_common_option('--remote-path', metavar='PATH', dest='remote_path', help='use PATH as borg executable on the remote (default: "borg")') - add_common_option('--remote-ratelimit', dest='remote_ratelimit', type=int, metavar='rate', + add_common_option('--remote-ratelimit', metavar='RATE', dest='remote_ratelimit', type=int, help='set remote network upload rate limit in kiByte/s (default: 0=unlimited)') - add_common_option('--consider-part-files', dest='consider_part_files', - action='store_true', + add_common_option('--consider-part-files', dest='consider_part_files', action='store_true', help='treat part files like normal files (e.g. to list/extract them)') - add_common_option('--debug-profile', dest='debug_profile', default=None, metavar='FILE', + add_common_option('--debug-profile', metavar='FILE', dest='debug_profile', default=None, help='Write execution profile in Borg format into FILE. For local use a Python-' 'compatible file can be generated by suffixing FILE with ".pyprof".') @@ -2370,25 +2368,25 @@ class Archiver: filters_group = subparser.add_argument_group('Archive filters', 'Archive filters can be applied to repository targets.') group = filters_group.add_mutually_exclusive_group() - group.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, default='', metavar='PREFIX', + group.add_argument('-P', '--prefix', metavar='PREFIX', dest='prefix', type=PrefixSpec, default='', help='only consider archive names starting with this prefix.') - group.add_argument('-a', '--glob-archives', dest='glob_archives', default=None, metavar='GLOB', + group.add_argument('-a', '--glob-archives', metavar='GLOB', dest='glob_archives', default=None, help='only consider archive names matching the glob. ' 'sh: rules apply, see "borg help patterns". ' '``--prefix`` and ``--glob-archives`` are mutually exclusive.') if sort_by: sort_by_default = 'timestamp' - filters_group.add_argument('--sort-by', dest='sort_by', type=SortBySpec, default=sort_by_default, - metavar='KEYS', + filters_group.add_argument('--sort-by', metavar='KEYS', dest='sort_by', + type=SortBySpec, default=sort_by_default, help='Comma-separated list of sorting keys; valid keys are: {}; default is: {}' .format(', '.join(HUMAN_SORT_KEYS), sort_by_default)) if first_last: group = filters_group.add_mutually_exclusive_group() - group.add_argument('--first', dest='first', metavar='N', default=0, type=int, + group.add_argument('--first', metavar='N', dest='first', default=0, type=int, help='consider first N archives after other filters were applied') - group.add_argument('--last', dest='last', metavar='N', default=0, type=int, + group.add_argument('--last', metavar='N', dest='last', default=0, type=int, help='consider last N archives after other filters were applied') parser = argparse.ArgumentParser(prog=self.prog, description='Borg - Deduplicated Backups', @@ -2418,22 +2416,22 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='start repository server process') subparser.set_defaults(func=self.do_serve) - subparser.add_argument('--restrict-to-path', dest='restrict_to_paths', action='append', - metavar='PATH', help='restrict repository access to PATH. ' - 'Can be specified multiple times to allow the client access to several directories. ' - 'Access to all sub-directories is granted implicitly; PATH doesn\'t need to directly point to a repository.') - subparser.add_argument('--restrict-to-repository', dest='restrict_to_repositories', action='append', - metavar='PATH', help='restrict repository access. Only the repository located at PATH (no sub-directories are considered) ' - 'is accessible. ' - 'Can be specified multiple times to allow the client access to several repositories. ' - 'Unlike ``--restrict-to-path`` sub-directories are not accessible; ' - 'PATH needs to directly point at a repository location. ' - 'PATH may be an empty directory or the last element of PATH may not exist, in which case ' - 'the client may initialize a repository there.') + subparser.add_argument('--restrict-to-path', metavar='PATH', dest='restrict_to_paths', action='append', + help='restrict repository access to PATH. ' + 'Can be specified multiple times to allow the client access to several directories. ' + 'Access to all sub-directories is granted implicitly; PATH doesn\'t need to directly point to a repository.') + subparser.add_argument('--restrict-to-repository', metavar='PATH', dest='restrict_to_repositories', action='append', + help='restrict repository access. Only the repository located at PATH ' + '(no sub-directories are considered) is accessible. ' + 'Can be specified multiple times to allow the client access to several repositories. ' + 'Unlike ``--restrict-to-path`` sub-directories are not accessible; ' + 'PATH needs to directly point at a repository location. ' + 'PATH may be an empty directory or the last element of PATH may not exist, in which case ' + 'the client may initialize a repository there.') subparser.add_argument('--append-only', dest='append_only', action='store_true', help='only allow appending to repository segment files') - subparser.add_argument('--storage-quota', dest='storage_quota', default=None, - type=parse_storage_quota, + subparser.add_argument('--storage-quota', metavar='QUOTA', dest='storage_quota', + type=parse_storage_quota, default=None, help='Override storage quota of the repository (e.g. 5G, 1.5T). ' 'When a new repository is initialized, sets the storage quota on the new ' 'repository as well. Default: no quota.') @@ -2549,12 +2547,12 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False), help='repository to create') - subparser.add_argument('-e', '--encryption', dest='encryption', required=True, metavar='MODE', + subparser.add_argument('-e', '--encryption', metavar='MODE', dest='encryption', required=True, choices=('none', 'keyfile', 'repokey', 'keyfile-blake2', 'repokey-blake2', 'authenticated'), help='select encryption key mode **(required)**') subparser.add_argument('--append-only', dest='append_only', action='store_true', help='create an append-only mode repository') - subparser.add_argument('--storage-quota', dest='storage_quota', default=None, metavar='QUOTA', + subparser.add_argument('--storage-quota', metavar='QUOTA', dest='storage_quota', default=None, type=parse_storage_quota, help='Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota.') @@ -2846,8 +2844,8 @@ class Archiver: help='print statistics for the created archive') subparser.add_argument('--list', dest='output_list', action='store_true', help='output verbose list of items (files, dirs, ...)') - subparser.add_argument('--filter', dest='output_filter', metavar='STATUSCHARS', - help='only display items with the given status characters') + subparser.add_argument('--filter', metavar='STATUSCHARS', dest='output_filter', + help='only display items with the given status characters (see description)') subparser.add_argument('--json', action='store_true', help='output stats as JSON. Implies ``--stats``.') subparser.add_argument('--no-cache-sync', dest='no_cache_sync', action='store_true', @@ -2873,11 +2871,10 @@ class Archiver: archive_group = subparser.add_argument_group('Archive options') archive_group.add_argument('--comment', dest='comment', metavar='COMMENT', default='', help='add a comment text to the archive') - archive_group.add_argument('--timestamp', dest='timestamp', + archive_group.add_argument('--timestamp', metavar='TIMESTAMP', dest='timestamp', type=timestamp, default=None, - metavar='TIMESTAMP', help='manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). ' - 'alternatively, give a reference file/directory.') + 'Alternatively, give a reference file/directory.') archive_group.add_argument('-c', '--checkpoint-interval', metavar='SECONDS', dest='checkpoint_interval', type=int, default=1800, help='write checkpoint every SECONDS seconds (Default: 1800)') @@ -3104,7 +3101,7 @@ class Archiver: subparser.set_defaults(func=self.do_list) subparser.add_argument('--short', dest='short', action='store_true', help='only print file/directory names, nothing else') - subparser.add_argument('--format', '--list-format', dest='format', type=str, metavar='FORMAT', + subparser.add_argument('--format', '--list-format', metavar='FORMAT', dest='format', help='specify format for file listing ' '(default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")') subparser.add_argument('--json', action='store_true', @@ -3287,7 +3284,7 @@ class Archiver: help='print statistics for the deleted archive') subparser.add_argument('--list', dest='output_list', action='store_true', help='output verbose list of archives it keeps/prunes') - subparser.add_argument('--keep-within', dest='within', type=interval, metavar='INTERVAL', + subparser.add_argument('--keep-within', metavar='INTERVAL', dest='within', type=interval, help='keep all archives within this time interval') subparser.add_argument('--keep-last', '--keep-secondly', dest='secondly', type=int, default=0, help='number of secondly archives to keep') @@ -3478,13 +3475,12 @@ class Archiver: help='write checkpoint every SECONDS seconds (Default: 1800)') archive_group.add_argument('--comment', dest='comment', metavar='COMMENT', default=None, help='add a comment text to the archive') - archive_group.add_argument('--timestamp', dest='timestamp', + archive_group.add_argument('--timestamp', metavar='TIMESTAMP', dest='timestamp', type=timestamp, default=None, - metavar='TIMESTAMP', help='manually specify the archive creation date/time (UTC, yyyy-mm-ddThh:mm:ss format). ' 'alternatively, give a reference file/directory.') - archive_group.add_argument('-C', '--compression', dest='compression', - type=CompressionSpec, default=CompressionSpec('lz4'), metavar='COMPRESSION', + archive_group.add_argument('-C', '--compression', metavar='COMPRESSION', dest='compression', + type=CompressionSpec, default=CompressionSpec('lz4'), help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') archive_group.add_argument('--recompress', dest='recompress', nargs='?', default='never', const='if-different', @@ -3493,9 +3489,8 @@ class Archiver: 'When `always`, chunks that are already compressed that way are not skipped, ' 'but compressed again. Only the algorithm is considered for `if-different`, ' 'not the compression level (if any).') - archive_group.add_argument('--chunker-params', dest='chunker_params', + archive_group.add_argument('--chunker-params', metavar='PARAMS', dest='chunker_params', type=ChunkerParams, default=CHUNKER_PARAMS, - metavar='PARAMS', help='specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, ' 'HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the current defaults. ' 'default: %d,%d,%d,%d' % CHUNKER_PARAMS) From 7965efd5d91111cf469b9da889619818c53a00f4 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 27 Jun 2017 10:11:57 +0200 Subject: [PATCH 1147/1387] version: add missing test for format_version, fix bug --- src/borg/testsuite/version.py | 17 ++++++++++++++++- src/borg/version.py | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/borg/testsuite/version.py b/src/borg/testsuite/version.py index b5f32e6e..d17dee0e 100644 --- a/src/borg/testsuite/version.py +++ b/src/borg/testsuite/version.py @@ -1,6 +1,6 @@ import pytest -from ..version import parse_version +from ..version import parse_version, format_version @pytest.mark.parametrize("version_str, version_tuple", [ @@ -36,3 +36,18 @@ def test_parse_version_invalid(): assert parse_version('1.2') # we require x.y.z versions with pytest.raises(ValueError): assert parse_version('crap') + + +@pytest.mark.parametrize("version_str, version_tuple", [ + ('1.0.0a1', (1, 0, 0, -4, 1)), + ('1.0.0', (1, 0, 0, -1)), + ('1.0.0a2', (1, 0, 0, -4, 2)), + ('1.0.0b3', (1, 0, 0, -3, 3)), + ('1.0.0rc4', (1, 0, 0, -2, 4)), + ('0.0.0', (0, 0, 0, -1)), + ('0.0.11', (0, 0, 11, -1)), + ('0.11.0', (0, 11, 0, -1)), + ('11.0.0', (11, 0, 0, -1)), +]) +def test_format_version(version_str, version_tuple): + assert format_version(version_tuple) == version_str diff --git a/src/borg/version.py b/src/borg/version.py index 7e2e95b7..a7a997f7 100644 --- a/src/borg/version.py +++ b/src/borg/version.py @@ -40,7 +40,7 @@ def format_version(version): while True: part = next(it) if part >= 0: - f += str(part) + f.append(str(part)) elif part == -1: break else: From 78f0e5d473515b7c7dd51b4c60d4c0c91af1b8c4 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 27 Jun 2017 10:34:34 +0200 Subject: [PATCH 1148/1387] archiver: add test for paperkey import, fix bug --- src/borg/crypto/keymanager.py | 1 + src/borg/testsuite/archiver.py | 51 ++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/borg/crypto/keymanager.py b/src/borg/crypto/keymanager.py index f6564a88..5d96a162 100644 --- a/src/borg/crypto/keymanager.py +++ b/src/borg/crypto/keymanager.py @@ -164,6 +164,7 @@ class KeyManager: (id_lines, id_repoid, id_complete_checksum) = data.split('/') except ValueError: print("the id line must contain exactly three '/', try again") + continue if sha256_truncated(data.lower().encode('ascii'), 2) != checksum: print('line checksum did not match, try same line again') continue diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index b047ff44..3a539eaf 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -61,7 +61,7 @@ from . import key src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw): +def exec_cmd(*args, archiver=None, fork=False, exe=None, input=b'', **kw): if fork: try: if exe is None: @@ -70,7 +70,7 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw): borg = (exe, ) elif not isinstance(exe, tuple): raise ValueError('exe must be None, a tuple or a str') - output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT) + output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT, input=input) ret = 0 except subprocess.CalledProcessError as e: output = e.output @@ -82,7 +82,7 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw): else: stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr try: - sys.stdin = StringIO() + sys.stdin = StringIO(input.decode()) sys.stdout = sys.stderr = output = StringIO() if archiver is None: archiver = Archiver() @@ -2533,6 +2533,51 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 2: 737475 - 88 """ + def test_key_import_paperkey(self): + repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239' + self.cmd('init', self.repository_location, '--encryption', 'keyfile') + self._set_repository_id(self.repository_path, unhexlify(repo_id)) + + key_file = self.keys_path + '/' + os.listdir(self.keys_path)[0] + with open(key_file, 'w') as fd: + fd.write(KeyfileKey.FILE_ID + ' ' + repo_id + '\n') + fd.write(b2a_base64(b'abcdefghijklmnopqrstu').decode()) + + typed_input = ( + b'2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 02\n' # Forgot to type "-" + b'2 / e29442 3506da 4e1ea7 25f62a 5a3d41 - 02\n' # Forgot to type second "/" + b'2 / e29442 3506da 4e1ea7 / 25f62a 5a3d42 - 02\n' # Typo (..42 not ..41) + b'2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02\n' # Correct! Congratulations + b'616263 646566 676869 6a6b6c 6d6e6f 707172 - 6d\n' + b'\n\n' # Abort [yN] => N + b'737475 88\n' # missing "-" + b'73747i - 88\n' # typo + b'73747 - 88\n' # missing nibble + b'73 74 75 - 89\n' # line checksum mismatch + b'00a1 - 88\n' # line hash collision - overall hash mismatch, have to start over + + b'2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02\n' + b'616263 646566 676869 6a6b6c 6d6e6f 707172 - 6d\n' + b'73 74 75 - 88\n' + ) + + # In case that this has to change, here is a quick way to find a colliding line hash: + # + # from hashlib import sha256 + # hash_fn = lambda x: sha256(b'\x00\x02' + x).hexdigest()[:2] + # for i in range(1000): + # if hash_fn(i.to_bytes(2, byteorder='big')) == '88': # 88 = line hash + # print(i.to_bytes(2, 'big')) + # break + + self.cmd('key', 'import', '--paper', self.repository_location, input=typed_input) + + # Test abort paths + typed_input = b'\ny\n' + self.cmd('key', 'import', '--paper', self.repository_location, input=typed_input) + typed_input = b'2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02\n\ny\n' + self.cmd('key', 'import', '--paper', self.repository_location, input=typed_input) + def test_debug_dump_manifest(self): self.create_regular_file('file1', size=1024 * 80) self.cmd('init', '--encryption=repokey', self.repository_location) From 6c67b64ab62f862909170cf44005df355687fe23 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 27 Jun 2017 12:21:30 +0200 Subject: [PATCH 1149/1387] xattr: test split_lstring --- src/borg/testsuite/xattr.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/borg/testsuite/xattr.py b/src/borg/testsuite/xattr.py index db01a29a..709d773e 100644 --- a/src/borg/testsuite/xattr.py +++ b/src/borg/testsuite/xattr.py @@ -2,7 +2,9 @@ import os import tempfile import unittest -from ..xattr import is_enabled, getxattr, setxattr, listxattr, buffer +import pytest + +from ..xattr import is_enabled, getxattr, setxattr, listxattr, buffer, split_lstring from . import BaseTestCase @@ -58,3 +60,13 @@ class XattrTestCase(BaseTestCase): got_value = getxattr(self.tmpfile.name, 'user.big') self.assert_equal(value, got_value) self.assert_equal(len(buffer), 128) + + +@pytest.mark.parametrize('lstring, splitted', ( + (b'', []), + (b'\x00', [b'']), + (b'\x01a', [b'a']), + (b'\x01a\x02cd', [b'a', b'cd']), +)) +def test_split_lstring(lstring, splitted): + assert split_lstring(lstring) == splitted From 38d601619e0a9082999a7270e832c9003ac927ea Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 27 Jun 2017 14:33:47 +0200 Subject: [PATCH 1150/1387] archiver: add test for debug refcount-obj --- src/borg/testsuite/archiver.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 3a539eaf..93eab20e 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2607,6 +2607,20 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 assert '_meta' in result assert '_items' in result + def test_debug_refcount_obj(self): + self.cmd('init', '--encryption=repokey', self.repository_location) + output = self.cmd('debug', 'refcount-obj', self.repository_location, '0' * 64).strip() + assert output == 'object 0000000000000000000000000000000000000000000000000000000000000000 not found [info from chunks cache].' + + create_json = json.loads(self.cmd('create', '--json', self.repository_location + '::test', 'input')) + archive_id = create_json['archive']['id'] + output = self.cmd('debug', 'refcount-obj', self.repository_location, archive_id).strip() + assert output == 'object ' + archive_id + ' has 1 referrers [info from chunks cache].' + + # Invalid IDs do not abort or return an error + output = self.cmd('debug', 'refcount-obj', self.repository_location, '124', 'xyza').strip() + assert output == 'object id 124 is invalid.\nobject id xyza is invalid.' + requires_gnutar = pytest.mark.skipif(not have_gnutar(), reason='GNU tar must be installed for this test.') requires_gzip = pytest.mark.skipif(not shutil.which('gzip'), reason='gzip must be installed for this test.') From 5d60669c507cf4b592bdd225e518604793dca78c Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 27 Jun 2017 14:35:44 +0200 Subject: [PATCH 1151/1387] archiver: add test for debug info --- src/borg/testsuite/archiver.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 93eab20e..c2e7e1d1 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2621,6 +2621,11 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 output = self.cmd('debug', 'refcount-obj', self.repository_location, '124', 'xyza').strip() assert output == 'object id 124 is invalid.\nobject id xyza is invalid.' + def test_debug_info(self): + output = self.cmd('debug', 'info') + assert 'CRC implementation' in output + assert 'Python' in output + requires_gnutar = pytest.mark.skipif(not have_gnutar(), reason='GNU tar must be installed for this test.') requires_gzip = pytest.mark.skipif(not shutil.which('gzip'), reason='gzip must be installed for this test.') From 4af1142693b84f6fe4a49d5b5d3291ec5dfaaa94 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 27 Jun 2017 14:47:06 +0200 Subject: [PATCH 1152/1387] archiver: add test_benchmark_crud, fix bug benchmark crud would just crash with a TypeError due to the missing return --- src/borg/archiver.py | 26 ++++++++++++++++++-------- src/borg/testsuite/archiver.py | 5 +++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 25b71168..64abf65d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -411,14 +411,22 @@ class Archiver: yield path shutil.rmtree(path) - for msg, count, size, random in [ - ('Z-BIG', 10, 100000000, False), - ('R-BIG', 10, 100000000, True), - ('Z-MEDIUM', 1000, 1000000, False), - ('R-MEDIUM', 1000, 1000000, True), - ('Z-SMALL', 10000, 10000, False), - ('R-SMALL', 10000, 10000, True), - ]: + if '_BORG_BENCHMARK_CRUD_TEST' in os.environ: + tests = [ + ('Z-TEST', 1, 1, False), + ('R-TEST', 1, 1, True), + ] + else: + tests = [ + ('Z-BIG', 10, 100000000, False), + ('R-BIG', 10, 100000000, True), + ('Z-MEDIUM', 1000, 1000000, False), + ('R-MEDIUM', 1000, 1000000, True), + ('Z-SMALL', 10000, 10000, False), + ('R-SMALL', 10000, 10000, True), + ] + + for msg, count, size, random in tests: with test_files(args.path, count, size, random) as path: dt_create, dt_update, dt_extract, dt_delete = measurement_run(args.location.canonical_path(), path) total_size_MB = count * size / 1e06 @@ -430,6 +438,8 @@ class Archiver: print(fmt % ('U', msg, total_size_MB / dt_update, count, file_size_formatted, content, dt_update)) print(fmt % ('D', msg, total_size_MB / dt_delete, count, file_size_formatted, content, dt_delete)) + return 0 + @with_repository(fake='dry_run', exclusive=True, compatibility=(Manifest.Operation.WRITE,)) def do_create(self, args, repository, manifest=None, key=None): """Create new archive""" diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index c2e7e1d1..5b1eedee 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2626,6 +2626,11 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 assert 'CRC implementation' in output assert 'Python' in output + def test_benchmark_crud(self): + self.cmd('init', '--encryption=repokey', self.repository_location) + with environment_variable(_BORG_BENCHMARK_CRUD_TEST='YES'): + self.cmd('benchmark', 'crud', self.repository_location, self.input_path) + requires_gnutar = pytest.mark.skipif(not have_gnutar(), reason='GNU tar must be installed for this test.') requires_gzip = pytest.mark.skipif(not shutil.which('gzip'), reason='gzip must be installed for this test.') From f037e2b64f74d5d27764e8807081bbaa8d2b07ed Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 27 Jun 2017 16:06:01 +0200 Subject: [PATCH 1153/1387] archiver: add test for "create -" and "extract --stdout" --- src/borg/testsuite/archiver.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 5b1eedee..52f06959 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -19,7 +19,7 @@ from configparser import ConfigParser from datetime import datetime from datetime import timedelta from hashlib import sha256 -from io import StringIO +from io import BytesIO, StringIO from unittest.mock import patch import msgpack @@ -61,7 +61,7 @@ from . import key src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -def exec_cmd(*args, archiver=None, fork=False, exe=None, input=b'', **kw): +def exec_cmd(*args, archiver=None, fork=False, exe=None, input=b'', binary_output=False, **kw): if fork: try: if exe is None: @@ -78,12 +78,18 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, input=b'', **kw): except SystemExit as e: # possibly raised by argparse output = '' ret = e.code - return ret, os.fsdecode(output) + if binary_output: + return ret, output + else: + return ret, os.fsdecode(output) else: stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr try: sys.stdin = StringIO(input.decode()) - sys.stdout = sys.stderr = output = StringIO() + sys.stdin.buffer = BytesIO(input) + output = BytesIO() + # Always use utf-8 here, to simply .decode() below + output_text = sys.stdout = sys.stderr = io.TextIOWrapper(output, encoding='utf-8') if archiver is None: archiver = Archiver() archiver.prerun_checks = lambda *args: None @@ -95,9 +101,11 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, input=b'', **kw): # actions that abort early (eg. --help) where given. Catch this and return # the error code as-if we invoked a Borg binary. except SystemExit as e: - return e.code, output.getvalue() + output_text.flush() + return e.code, output.getvalue() if binary_output else output.getvalue().decode() ret = archiver.run(args) - return ret, output.getvalue() + output_text.flush() + return ret, output.getvalue() if binary_output else output.getvalue().decode() finally: sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr @@ -927,6 +935,18 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.mkdir('input/cache3') os.link('input/cache1/%s' % CACHE_TAG_NAME, 'input/cache3/%s' % CACHE_TAG_NAME) + def test_create_stdin(self): + self.cmd('init', '--encryption=repokey', self.repository_location) + input_data = b'\x00foo\n\nbar\n \n' + self.cmd('create', self.repository_location + '::test', '-', input=input_data) + item = json.loads(self.cmd('list', '--json-lines', self.repository_location + '::test')) + assert item['uid'] == 0 + assert item['gid'] == 0 + assert item['size'] == len(input_data) + assert item['path'] == 'stdin' + extracted_data = self.cmd('extract', '--stdout', self.repository_location + '::test', binary_output=True) + assert extracted_data == input_data + def test_create_without_root(self): """test create without a root""" self.cmd('init', '--encryption=repokey', self.repository_location) From 29646d5b5e71752c6d3b9e06f719b2152413bb63 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 27 Jun 2017 16:15:47 +0200 Subject: [PATCH 1154/1387] key import: allow reading from stdin --- src/borg/archiver.py | 4 ++-- src/borg/crypto/keymanager.py | 4 ++-- src/borg/helpers.py | 14 ++++++++++++++ src/borg/nanorst.py | 5 +++-- src/borg/testsuite/helpers.py | 6 ++++++ 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 64abf65d..9eee69f2 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -341,7 +341,7 @@ class Archiver: if not args.path: self.print_error("input file to import key from expected") return EXIT_ERROR - if not os.path.exists(args.path): + if args.path != '-' and not os.path.exists(args.path): self.print_error("input file does not exist: " + args.path) return EXIT_ERROR manager.import_keyfile(args) @@ -2695,7 +2695,7 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False)) subparser.add_argument('path', metavar='PATH', nargs='?', type=str, - help='path to the backup') + help='path to the backup (\'-\' to read from stdin)') subparser.add_argument('--paper', dest='paper', action='store_true', help='interactively import from a backup done with ``--paper``') diff --git a/src/borg/crypto/keymanager.py b/src/borg/crypto/keymanager.py index 5d96a162..5f7e5978 100644 --- a/src/borg/crypto/keymanager.py +++ b/src/borg/crypto/keymanager.py @@ -4,7 +4,7 @@ import textwrap from binascii import unhexlify, a2b_base64, b2a_base64 from hashlib import sha256 -from ..helpers import Manifest, NoManifestError, Error, yes, bin_to_hex +from ..helpers import Manifest, NoManifestError, Error, yes, bin_to_hex, open_file_or_stdin from ..repository import Repository from .key import KeyfileKey, KeyfileNotFoundError, KeyBlobStorage, identify_key @@ -130,7 +130,7 @@ class KeyManager: def import_keyfile(self, args): file_id = KeyfileKey.FILE_ID first_line = file_id + ' ' + bin_to_hex(self.repository.id) + '\n' - with open(args.path, 'r') as fd: + with open_file_or_stdin(args.path, 'r') as fd: file_first_line = fd.read(len(first_line)) if file_first_line != first_line: if not file_first_line.startswith(file_id): diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 1554eefe..dfed99e3 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -2162,3 +2162,17 @@ def popen_with_error_handling(cmd_line: str, log_prefix='', **kwargs): except PermissionError: logger.error('%spermission denied: %s', log_prefix, command[0]) return + + +def open_file_or_stdin(path, mode): + if path == '-': + if 'b' in mode: + return sys.stdin.buffer + else: + return sys.stdin + else: + return open(path, mode) + + +def is_terminal(fd=sys.stdout): + return hasattr(fd, 'isatty') and fd.isatty() and (sys.platform != 'win32' or 'ANSICON' in os.environ) diff --git a/src/borg/nanorst.py b/src/borg/nanorst.py index ba4ad2a3..33a5e541 100644 --- a/src/borg/nanorst.py +++ b/src/borg/nanorst.py @@ -1,7 +1,8 @@ import io -import os import sys +from .helpers import is_terminal + class TextPecker: def __init__(self, s): @@ -205,7 +206,7 @@ def rst_to_terminal(rst, references=None, destination=sys.stdout): If *destination* is a file-like object connected to a terminal, enrich text with suitable ANSI escapes. Otherwise return plain text. """ - if hasattr(destination, 'isatty') and destination.isatty() and (sys.platform != 'win32' or 'ANSICON' in os.environ): + if is_terminal(destination): rst_state_hook = ansi_escapes else: rst_state_hook = None diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 14d4783c..9dba4d5e 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -28,6 +28,7 @@ from ..helpers import swidth_slice from ..helpers import chunkit from ..helpers import safe_ns, safe_s, SUPPORT_32BIT_PLATFORMS from ..helpers import popen_with_error_handling +from ..helpers import open_file_or_stdin from . import BaseTestCase, FakeInputs @@ -942,3 +943,8 @@ class TestPopenWithErrorHandling: def test_shell(self): with pytest.raises(AssertionError): popen_with_error_handling('', shell=True) + + +def test_open_file_or_stdin(): + assert open_file_or_stdin('-', 'r') is sys.stdin + assert open_file_or_stdin('-', 'rb') is sys.stdin.buffer From 4009a764ed70f054d65921e043baa68bce9476b7 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Thu, 29 Jun 2017 11:48:58 +0200 Subject: [PATCH 1155/1387] docs: info: update and add examples The form of the output has changed and new output has been added. --- docs/usage/info.rst | 57 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/docs/usage/info.rst b/docs/usage/info.rst index df770ffc..542ff5ef 100644 --- a/docs/usage/info.rst +++ b/docs/usage/info.rst @@ -4,20 +4,55 @@ Examples ~~~~~~~~ :: - $ borg info /path/to/repo::root-2016-02-15 - Name: root-2016-02-15 - Fingerprint: 57c827621f21b000a8d363c1e163cc55983822b3afff3a96df595077a660be50 + $ borg info /path/to/repo::2017-06-29T11:00-srv + Archive name: 2017-06-29T11:00-srv + Archive fingerprint: b2f1beac2bd553b34e06358afa45a3c1689320d39163890c5bbbd49125f00fe5 + Comment: Hostname: myhostname Username: root - Time (start): Mon, 2016-02-15 19:36:29 - Time (end): Mon, 2016-02-15 19:39:26 - Command line: /usr/local/bin/borg create --list -C zlib,6 /path/to/repo::root-2016-02-15 / --one-file-system - Number of files: 38100 - + Time (start): Thu, 2017-06-29 11:03:07 + Time (end): Thu, 2017-06-29 11:03:13 + Duration: 5.66 seconds + Number of files: 17037 + Command line: /usr/sbin/borg create /path/to/repo::2017-06-29T11:00-srv /srv + Utilization of max. archive size: 0% + ------------------------------------------------------------------------------ Original size Compressed size Deduplicated size - This archive: 1.33 GB 613.25 MB 571.64 MB - All archives: 1.63 GB 853.66 MB 584.12 MB + This archive: 12.53 GB 12.49 GB 1.62 kB + All archives: 121.82 TB 112.41 TB 215.42 GB Unique chunks Total chunks - Chunk index: 36858 48844 + Chunk index: 1015213 626934122 + $ borg info /path/to/repo --last 1 + Archive name: 2017-06-29T11:00-srv + Archive fingerprint: b2f1beac2bd553b34e06358afa45a3c1689320d39163890c5bbbd49125f00fe5 + Comment: + Hostname: myhostname + Username: root + Time (start): Thu, 2017-06-29 11:03:07 + Time (end): Thu, 2017-06-29 11:03:13 + Duration: 5.66 seconds + Number of files: 17037 + Command line: /usr/sbin/borg create /path/to/repo::2017-06-29T11:00-srv /srv + Utilization of max. archive size: 0% + ------------------------------------------------------------------------------ + Original size Compressed size Deduplicated size + This archive: 12.53 GB 12.49 GB 1.62 kB + All archives: 121.82 TB 112.41 TB 215.42 GB + + Unique chunks Total chunks + Chunk index: 1015213 626934122 + + $ borg info /path/to/repo + Repository ID: d857ce5788c51272c61535062e89eac4e8ef5a884ffbe976e0af9d8765dedfa5 + Location: /path/to/repo + Encrypted: Yes (repokey) + Cache: /root/.cache/borg/d857ce5788c51272c61535062e89eac4e8ef5a884ffbe976e0af9d8765dedfa5 + Security dir: /root/.config/borg/security/d857ce5788c51272c61535062e89eac4e8ef5a884ffbe976e0af9d8765dedfa5 + ------------------------------------------------------------------------------ + Original size Compressed size Deduplicated size + All archives: 121.82 TB 112.41 TB 215.42 GB + + Unique chunks Total chunks + Chunk index: 1015213 626934122 From 52f76d2cd6f2b7ddb6cea90e60a0f9c63a3cd016 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 30 Jun 2017 10:53:23 +0200 Subject: [PATCH 1156/1387] docs: mount: add repository example --- docs/usage/mount.rst | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/usage/mount.rst b/docs/usage/mount.rst index f5f1b4f5..a3ab2ea3 100644 --- a/docs/usage/mount.rst +++ b/docs/usage/mount.rst @@ -5,26 +5,35 @@ Examples ~~~~~~~~ -borg mount -++++++++++ :: - $ borg mount /path/to/repo::root-2016-02-15 /tmp/mymountpoint + # Mounting the repository shows all archives. + # Archives are loaded lazily, expect some delay when navigating to an archive + # for the first time. + $ borg mount /path/to/repo /tmp/mymountpoint $ ls /tmp/mymountpoint - bin boot etc home lib lib64 lost+found media mnt opt root sbin srv tmp usr var + root-2016-02-14 root-2016-02-15 $ borg umount /tmp/mymountpoint -:: + # Mounting a specific archive is possible as well. + $ borg mount /path/to/repo::root-2016-02-15 /tmp/mymountpoint + $ ls /tmp/mymountpoint + bin boot etc home lib lib64 lost+found media mnt opt + root sbin srv tmp usr var + $ borg umount /tmp/mymountpoint + # The experimental versions view merges all archives in the repository + # and provides a versioned view on files. $ borg mount -o versions /path/to/repo /tmp/mymountpoint $ ls -l /tmp/mymountpoint/home/user/doc.txt/ total 24 -rw-rw-r-- 1 user group 12357 Aug 26 21:19 doc.txt.cda00bc9 -rw-rw-r-- 1 user group 12204 Aug 26 21:04 doc.txt.fa760f28 - $ fusermount -u /tmp/mymountpoint + $ borg umount /tmp/mymountpoint borgfs ++++++ + :: $ echo '/mnt/backup /tmp/myrepo fuse.borgfs defaults,noauto 0 0' >> /etc/fstab From f6c3d1d2cc38b53ba22f5890890a2d5db868a89d Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Fri, 30 Jun 2017 17:41:58 +0200 Subject: [PATCH 1157/1387] docs: no third level toc on command usage pages ("Description", "Examples") --- docs/_templates/globaltoc.html | 12 +++++++++++- docs/usage.rst | 2 -- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/_templates/globaltoc.html b/docs/_templates/globaltoc.html index 7841437c..d10f5f77 100644 --- a/docs/_templates/globaltoc.html +++ b/docs/_templates/globaltoc.html @@ -1,6 +1,16 @@