From 116f67036f503405b80a4d8baa429a6166490b80 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 3 May 2022 03:01:03 +0200 Subject: [PATCH 001/160] transfer: copy archives from another repo this is somehow similar to borg recreate, but with different focus and way simpler: not changing compression algo not changing chunking not excluding files inside an archive by path match only dealing with complete archives but: different src and dst repo only reading each chunk once keeping the compressed payload (no decompression/recompression effort) --dry-run can be used before and afterwards to check --- src/borg/archiver.py | 105 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index de9489ff0..49516fff6 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -338,6 +338,74 @@ class Archiver: ).serve() return EXIT_SUCCESS + @with_other_repository(manifest=True, key=True, compatibility=(Manifest.Operation.READ,)) + @with_repository(exclusive=True, manifest=True, cache=True, compatibility=(Manifest.Operation.WRITE,)) + def do_transfer(self, args, *, + repository, manifest, key, cache, + other_repository=None, other_manifest=None, other_key=None): + """archives transfer from other repository""" + dry_run = args.dry_run + + args.consider_checkpoints = True + archive_names = tuple(x.name for x in other_manifest.archives.list_considering(args)) + if not archive_names: + return EXIT_SUCCESS + + for name in archive_names: + transfer_size = 0 + present_size = 0 + if name in manifest.archives and not dry_run: + print(f"{name}: archive is already present in destination repo, skipping.") + else: + if not dry_run: + print(f"{name}: copying archive to destination repo...") + other_archive = Archive(other_repository, other_key, other_manifest, name) + archive = Archive(repository, key, manifest, name, cache=cache, create=True) if not dry_run else None + for item in other_archive.iter_items(): + if 'chunks' in item: + chunks = [] + for chunk_id, size, _ in item.chunks: + refcount = cache.seen_chunk(chunk_id, size) + if refcount == 0: # target repo does not yet have this chunk + if not dry_run: + cdata = other_repository.get(chunk_id) + # keep compressed payload same, avoid decompression / recompression + data = other_key.decrypt(chunk_id, cdata, decompress=False) + chunk_entry = cache.add_chunk(chunk_id, data, archive.stats, wait=False, + compress=False, size=size) + cache.repository.async_response(wait=False) + chunks.append(chunk_entry) + transfer_size += size + else: + if not dry_run: + chunk_entry = cache.chunk_incref(chunk_id, archive.stats) + chunks.append(chunk_entry) + present_size += size + if not dry_run: + item.chunks = chunks # overwrite! IDs and sizes are same, csizes are likely different + archive.stats.nfiles += 1 + # TODO: filter the item data, get rid of legacy crap + if not dry_run: + archive.add_item(item) + if not dry_run: + additional_metadata = {} + # keep all metadata except archive version and stats. also do not keep + # recreate_source_id, recreate_args, recreate_partial_chunks which were used only in 1.1.0b1 .. b2. + for attr in ('cmdline', 'hostname', 'username', 'time', 'time_end', 'comment', + 'chunker_params', 'recreate_cmdline'): + if hasattr(other_archive.metadata, attr): + additional_metadata[attr] = getattr(other_archive.metadata, attr) + archive.save(stats=archive.stats, additional_metadata=additional_metadata) + print(f"{name}: finished. " + f"transfer_size: {format_file_size(transfer_size)} " + f"present_size: {format_file_size(present_size)}") + else: + print(f"{name}: completed" if transfer_size == 0 else + f"{name}: incomplete, " + f"transfer_size: {format_file_size(transfer_size)} " + f"present_size: {format_file_size(present_size)}") + return EXIT_SUCCESS + @with_repository(create=True, exclusive=True, manifest=False) @with_other_repository(key=True, compatibility=(Manifest.Operation.READ, )) def do_init(self, args, repository, *, other_repository=None, other_key=None): @@ -4083,6 +4151,43 @@ class Archiver: help='archives to delete') define_archive_filters_group(subparser) + # borg transfer + transfer_epilog = process_epilog(""" + This command transfers archives from one repository to another repository. + + Suggested use: + + # initialize DST_REPO reusing key material from SRC_REPO, so that + # chunking and chunk id generation will work in the same way as before. + borg init --other-location=SRC_REPO --encryption=DST_ENC DST_REPO + + # transfer archives from SRC_REPO to DST_REPO + borg transfer --dry-run SRC_REPO DST_REPO # check what it would do + borg transfer SRC_REPO DST_REPO # do it! + borg transfer --dry-run SRC_REPO DST_REPO # check! anything left? + + The default is to transfer all archives, including checkpoint archives. + + You could use the misc. archive filter options to limit which archives it will + transfer, e.g. using the --prefix option. This is recommended for big + repositories with multiple data sets to keep the runtime per invocation lower. + """) + subparser = subparsers.add_parser('transfer', parents=[common_parser], add_help=False, + description=self.do_transfer.__doc__, + epilog=transfer_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='transfer of archives from another repository') + subparser.set_defaults(func=self.do_transfer) + subparser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true', + help='do not change repository, just check') + subparser.add_argument('other_location', metavar='SRC_REPOSITORY', + type=location_validator(archive=False, other=True), + help='source repository') + subparser.add_argument('location', metavar='DST_REPOSITORY', + type=location_validator(archive=False, other=False), + help='destination repository') + define_archive_filters_group(subparser) + # borg diff diff_epilog = process_epilog(""" This command finds differences (file contents, user/group/mode) between archives. From 98b7dc0bf5baecc8a371f78b8ea42fc5f1769c2c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 3 May 2022 16:58:57 +0200 Subject: [PATCH 002/160] transfer: clean item of attic 0.13 'acl' bug remnants also: remove attic bug support code from borg check. borg transfer removes the acl key. we do not run borg check on old repos. --- src/borg/archive.py | 3 --- src/borg/archiver.py | 9 +++++++-- src/borg/testsuite/archiver.py | 29 ----------------------------- 3 files changed, 7 insertions(+), 34 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index a9c145d49..ba95a0867 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1944,9 +1944,6 @@ class ArchiveChecker: def valid_item(obj): if not isinstance(obj, StableDict): 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) if not required_item_keys.issubset(keys): return False, 'missing required keys: ' + list_keys_safe(required_item_keys - keys) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 49516fff6..a4cddd7b0 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -344,6 +344,12 @@ class Archiver: repository, manifest, key, cache, other_repository=None, other_manifest=None, other_key=None): """archives transfer from other repository""" + + def upgrade_item(item): + """upgrade item as needed, get rid of legacy crap""" + item._dict.pop('acl', None) # remove remnants of bug in attic <= 0.13 + return item + dry_run = args.dry_run args.consider_checkpoints = True @@ -384,9 +390,8 @@ class Archiver: if not dry_run: item.chunks = chunks # overwrite! IDs and sizes are same, csizes are likely different archive.stats.nfiles += 1 - # TODO: filter the item data, get rid of legacy crap if not dry_run: - archive.add_item(item) + archive.add_item(upgrade_item(item)) if not dry_run: additional_metadata = {} # keep all metadata except archive version and stats. also do not keep diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 450ba1757..5889b12ab 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -3907,35 +3907,6 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): repository.commit(compact=False) 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). - class Attic013Item: - def as_dict(self): - 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: - 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()) - archive.save() - self.cmd('check', self.repository_location, exit_code=0) - self.cmd('list', self.repository_location + '::0.13', exit_code=0) - class ManifestAuthenticationTest(ArchiverTestCaseBase): def spoof_manifest(self, repository): From ba1dbe6111d5b1f11b2a3d3c43a0fed83b2e7392 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 3 May 2022 17:13:37 +0200 Subject: [PATCH 003/160] transfer: make sure items with chunks have precomputed size --- src/borg/archiver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index a4cddd7b0..0c5dee5f1 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -348,6 +348,7 @@ class Archiver: def upgrade_item(item): """upgrade item as needed, get rid of legacy crap""" item._dict.pop('acl', None) # remove remnants of bug in attic <= 0.13 + item.get_size(memorize=True) # if not already present: compute+remember size for items with chunks return item dry_run = args.dry_run From 01f72d15b4c4ebd5dd21e6787dacf0b44d433d5c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 3 May 2022 20:51:43 +0200 Subject: [PATCH 004/160] transfer: remove the zlib type bytes hack hack: see the docstring of ZLIB_legacy class. New clean ZLIB class that works as every other compressor. ZLIB ID 0x0500, ZLIB_legacy ID 0x.8.. --- src/borg/archiver.py | 8 +++++- src/borg/compress.pyx | 51 ++++++++++++++++++++++++++++++---- src/borg/testsuite/archiver.py | 4 +-- src/borg/testsuite/compress.py | 4 +-- 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 0c5dee5f1..098208167 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -44,7 +44,7 @@ try: from .archive import has_link from .cache import Cache, assert_secure, SecurityManager from .constants import * # NOQA - from .compress import CompressionSpec + from .compress import CompressionSpec, ZLIB, ZLIB_legacy from .crypto.key import key_creator, key_argument_names, tam_required_file, tam_required from .crypto.key import RepoKey, KeyfileKey, Blake2RepoKey, Blake2KeyfileKey, FlexiKey from .crypto.keymanager import KeyManager @@ -351,6 +351,11 @@ class Archiver: item.get_size(memorize=True) # if not already present: compute+remember size for items with chunks return item + def upgrade_compressed_chunk(chunk): + if ZLIB_legacy.detect(chunk): + chunk = ZLIB.ID + chunk # get rid of the attic legacy: prepend separate type bytes for zlib + return chunk + dry_run = args.dry_run args.consider_checkpoints = True @@ -378,6 +383,7 @@ class Archiver: cdata = other_repository.get(chunk_id) # keep compressed payload same, avoid decompression / recompression data = other_key.decrypt(chunk_id, cdata, decompress=False) + data = upgrade_compressed_chunk(data) chunk_entry = cache.add_chunk(chunk_id, data, archive.stats, wait=False, compress=False, size=size) cache.repository.async_response(wait=False) diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index 2e0eb4809..7997456c6 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -331,14 +331,52 @@ class ZSTD(DecidingCompressor): return dest[:osize] -class ZLIB(CompressorBase): +class ZLIB(DecidingCompressor): """ zlib compression / decompression (python stdlib) """ - ID = b'\x08\x00' # not used here, see detect() - # avoid all 0x.8.. IDs elsewhere! + ID = b'\x05\x00' name = 'zlib' + def __init__(self, level=6, **kwargs): + super().__init__(**kwargs) + self.level = level + + def _decide(self, data): + """ + Decides what to do with *data*. Returns (compressor, zlib_data). + + *zlib_data* is the ZLIB result if *compressor* is ZLIB as well, otherwise it is None. + """ + zlib_data = zlib.compress(data, self.level) + if len(zlib_data) < len(data): + return self, zlib_data + else: + return NONE_COMPRESSOR, None + + def decompress(self, data): + data = super().decompress(data) + try: + return zlib.decompress(data) + except zlib.error as e: + raise DecompressionError(str(e)) from None + + +class ZLIB_legacy(CompressorBase): + """ + zlib compression / decompression (python stdlib) + + Note: This is the legacy ZLIB support as used by borg < 1.3. + It still suffers from attic *only* supporting zlib and not having separate + ID bytes to differentiate between differently compressed chunks. + This just works because zlib compressed stuff always starts with 0x.8.. bytes. + Newer borg uses the ZLIB class that has separate ID bytes (as all the other + compressors) and does not need this hack. + """ + ID = b'\x08\x00' # not used here, see detect() + # avoid all 0x.8.. IDs elsewhere! + name = 'zlib_legacy' + @classmethod def detect(cls, data): # matches misc. patterns 0x.8.. used by zlib @@ -502,13 +540,14 @@ COMPRESSOR_TABLE = { CNONE.name: CNONE, LZ4.name: LZ4, ZLIB.name: ZLIB, + ZLIB_legacy.name: ZLIB_legacy, LZMA.name: LZMA, Auto.name: Auto, ZSTD.name: ZSTD, ObfuscateSize.name: ObfuscateSize, } # List of possible compression types. Does not include Auto, since it is a meta-Compressor. -COMPRESSOR_LIST = [LZ4, ZSTD, CNONE, ZLIB, LZMA, ObfuscateSize, ] # check fast stuff first +COMPRESSOR_LIST = [LZ4, ZSTD, CNONE, ZLIB, ZLIB_legacy, LZMA, ObfuscateSize, ] # check fast stuff first def get_compressor(name, **kwargs): cls = COMPRESSOR_TABLE[name] @@ -554,7 +593,7 @@ class CompressionSpec: self.name = values[0] if self.name in ('none', 'lz4', ): return - elif self.name in ('zlib', 'lzma', ): + elif self.name in ('zlib', 'lzma', 'zlib_legacy'): # zlib_legacy just for testing if count < 2: level = 6 # default compression level in py stdlib elif count == 2: @@ -597,7 +636,7 @@ class CompressionSpec: def compressor(self): if self.name in ('none', 'lz4', ): return get_compressor(self.name) - elif self.name in ('zlib', 'lzma', 'zstd', ): + elif self.name in ('zlib', 'lzma', 'zstd', 'zlib_legacy'): 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/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 5889b12ab..b69fe819f 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2442,7 +2442,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_compression_zlib_compressible(self): size, csize = self._get_sizes('zlib', compressible=True) assert csize < size * 0.1 - assert csize == 35 + assert csize == 37 def test_compression_zlib_uncompressible(self): size, csize = self._get_sizes('zlib', compressible=False) @@ -2451,7 +2451,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_compression_auto_compressible(self): size, csize = self._get_sizes('auto,zlib', compressible=True) assert csize < size * 0.1 - assert csize == 35 # same as compression 'zlib' + assert csize == 37 # same as compression 'zlib' def test_compression_auto_uncompressible(self): size, csize = self._get_sizes('auto,zlib', compressible=False) diff --git a/src/borg/testsuite/compress.py b/src/borg/testsuite/compress.py index 3942c3537..c93dd3bb6 100644 --- a/src/borg/testsuite/compress.py +++ b/src/borg/testsuite/compress.py @@ -88,11 +88,11 @@ def test_autodetect_invalid(): Compressor(**params).decompress(b'\x08\x00notreallyzlib') -def test_zlib_compat(): +def test_zlib_legacy_compat(): # for compatibility reasons, we do not add an extra header for zlib, # nor do we expect one when decompressing / autodetecting for level in range(10): - c = get_compressor(name='zlib', level=level) + c = get_compressor(name='zlib_legacy', level=level) cdata1 = c.compress(data) cdata2 = zlib.compress(data, level) assert cdata1 == cdata2 From e4a97ea8cc475b5eb5f5f43d622a485874224727 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 4 May 2022 01:58:24 +0200 Subject: [PATCH 005/160] transfer: all hardlinks have chunks, maybe chunks_healty, hlid Item.hlid: same id, same hardlink (xxh64 digest) Item.hardlink_master: not used for new archives any more Item.source: not used for hardlink slaves any more --- src/borg/archiver.py | 15 +++++++++++++++ src/borg/constants.py | 2 +- src/borg/item.pyx | 3 ++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 098208167..ea70f412a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -347,6 +347,20 @@ class Archiver: def upgrade_item(item): """upgrade item as needed, get rid of legacy crap""" + if item.get('hardlink_master', True) and 'source' not in item and hardlinkable(item.mode): + item._dict['hlid'] = hlid = hashlib.sha256(item._dict['path']) + hardlink_masters[hlid] = (item._dict.get('chunks'), item._dict.get('chunks_healthy')) + elif 'source' in item and hardlinkable(item.mode): + item._dict['hlid'] = hlid = hashlib.sha256(item._dict['source']) + chunks, chunks_healthy = hardlink_masters.get(hlid, (None, None)) + if chunks is not None: + item._dict['chunks'] = chunks + for chunk_id, _, _ in chunks: + cache.chunk_incref(chunk_id, archive.stats) + if chunks_healthy is not None: + item._dict['chunks_healthy'] = chunks + item._dict.pop('source') # not used for hardlinks any more, replaced by hlid + item._dict.pop('hardlink_master', None) # not used for hardlinks any more, replaced by hlid item._dict.pop('acl', None) # remove remnants of bug in attic <= 0.13 item.get_size(memorize=True) # if not already present: compute+remember size for items with chunks return item @@ -371,6 +385,7 @@ class Archiver: else: if not dry_run: print(f"{name}: copying archive to destination repo...") + hardlink_masters = {} other_archive = Archive(other_repository, other_key, other_manifest, name) archive = Archive(repository, key, manifest, name, cache=cache, create=True) if not dry_run else None for item in other_archive.iter_items(): diff --git a/src/borg/constants.py b/src/borg/constants.py index 0b2ef16a1..13eb8bd23 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -1,5 +1,5 @@ # this set must be kept complete, otherwise the RobustUnpacker might malfunction: -ITEM_KEYS = frozenset(['path', 'source', 'rdev', 'chunks', 'chunks_healthy', 'hardlink_master', +ITEM_KEYS = frozenset(['path', 'source', 'rdev', 'chunks', 'chunks_healthy', 'hardlink_master', 'hlid', 'mode', 'user', 'group', 'uid', 'gid', 'mtime', 'atime', 'ctime', 'birthtime', 'size', 'xattrs', 'bsdflags', 'acl_nfs4', 'acl_access', 'acl_default', 'acl_extended', 'part']) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 48debf183..0b2598ffe 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -181,7 +181,8 @@ class Item(PropDict): # 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) + hlid = PropDict._make_property('hlid', bytes) # hard link id: same value means same hard link. + hardlink_master = PropDict._make_property('hardlink_master', bool) # legacy chunks = PropDict._make_property('chunks', (list, type(None)), 'list or None') chunks_healthy = PropDict._make_property('chunks_healthy', (list, type(None)), 'list or None') From 7903dad1839c0c1e3a64dbbce8115e2d8aae5804 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 4 May 2022 10:34:33 +0200 Subject: [PATCH 006/160] transfer: convert timestamps int/bigint -> msgpack.Timestamp, see #2323 Timestamp scales to 64 or 96bit serialization formats, that should be enough for everybody. We use this in archived items and also in the files cache. --- src/borg/archiver.py | 4 ++++ src/borg/cache.py | 11 ++++++----- src/borg/helpers/msgpack.py | 17 ++++++++++++++--- src/borg/helpers/parseformat.py | 3 +++ src/borg/item.pyx | 11 +++++------ src/borg/testsuite/item.py | 7 ++++--- 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index ea70f412a..eac059bb7 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -360,6 +360,10 @@ class Archiver: if chunks_healthy is not None: item._dict['chunks_healthy'] = chunks item._dict.pop('source') # not used for hardlinks any more, replaced by hlid + for attr in 'atime', 'ctime', 'mtime', 'birthtime': + if attr in item: + ns = getattr(item, attr) # decode (bigint or Timestamp) --> int ns + setattr(item, attr, ns) # encode int ns --> msgpack.Timestamp only, no bigint any more item._dict.pop('hardlink_master', None) # not used for hardlinks any more, replaced by hlid item._dict.pop('acl', None) # remove remnants of bug in attic <= 0.13 item.get_size(memorize=True) # if not already present: compute+remember size for items with chunks diff --git a/src/borg/cache.py b/src/borg/cache.py index 6fa74e692..6cd612359 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -19,7 +19,7 @@ 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, parse_stringified_list +from .helpers import bin_to_hex, parse_stringified_list from .helpers import format_file_size from .helpers import safe_ns from .helpers import yes @@ -28,6 +28,7 @@ from .helpers import ProgressIndicatorPercent, ProgressIndicatorMessage from .helpers import set_ec, EXIT_WARNING from .helpers import safe_unlink from .helpers import msgpack +from .helpers.msgpack import int_to_timestamp, timestamp_to_int from .item import ArchiveItem, ChunkListEntry from .crypto.key import PlaintextKey from .crypto.file_integrity import IntegrityCheckedFile, DetachedIntegrityCheckedFile, FileIntegrityError @@ -623,7 +624,7 @@ class LocalCache(CacheStatsMixin): # this is to avoid issues with filesystem snapshots and cmtime 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.cmtime) < self._newest_cmtime or \ + if entry.age == 0 and timestamp_to_int(entry.cmtime) < self._newest_cmtime or \ entry.age > 0 and entry.age < ttl: msgpack.pack((path_hash, entry), fd) entry_count += 1 @@ -1018,10 +1019,10 @@ class LocalCache(CacheStatsMixin): if 'i' in cache_mode and entry.inode != st.st_ino: files_cache_logger.debug('KNOWN-CHANGED: file inode number has changed: %r', hashed_path) return True, None - if 'c' in cache_mode and bigint_to_int(entry.cmtime) != st.st_ctime_ns: + if 'c' in cache_mode and timestamp_to_int(entry.cmtime) != st.st_ctime_ns: files_cache_logger.debug('KNOWN-CHANGED: file ctime has changed: %r', hashed_path) return True, None - elif 'm' in cache_mode and bigint_to_int(entry.cmtime) != st.st_mtime_ns: + elif 'm' in cache_mode and timestamp_to_int(entry.cmtime) != st.st_mtime_ns: files_cache_logger.debug('KNOWN-CHANGED: file mtime has changed: %r', hashed_path) return True, None # we ignored the inode number in the comparison above or it is still same. @@ -1049,7 +1050,7 @@ class LocalCache(CacheStatsMixin): elif 'm' in cache_mode: cmtime_type = 'mtime' cmtime_ns = safe_ns(st.st_mtime_ns) - entry = FileCacheEntry(age=0, inode=st.st_ino, size=st.st_size, cmtime=int_to_bigint(cmtime_ns), chunk_ids=ids) + entry = FileCacheEntry(age=0, inode=st.st_ino, size=st.st_size, cmtime=int_to_timestamp(cmtime_ns), chunk_ids=ids) self.files[path_hash] = msgpack.packb(entry) self._newest_cmtime = max(self._newest_cmtime or 0, cmtime_ns) files_cache_logger.debug('FILES-CACHE-UPDATE: put %r [has %s] <- %r', diff --git a/src/borg/helpers/msgpack.py b/src/borg/helpers/msgpack.py index 2ace88fee..411f00fec 100644 --- a/src/borg/helpers/msgpack.py +++ b/src/borg/helpers/msgpack.py @@ -24,7 +24,7 @@ from msgpack import unpackb as mp_unpackb from msgpack import unpack as mp_unpack from msgpack import version as mp_version -from msgpack import ExtType +from msgpack import ExtType, Timestamp from msgpack import OutOfData @@ -164,7 +164,7 @@ def get_limited_unpacker(kind): return Unpacker(**args) -def bigint_to_int(mtime): +def bigint_to_int(mtime): # legacy """Convert bytearray to int """ if isinstance(mtime, bytes): @@ -172,7 +172,7 @@ def bigint_to_int(mtime): return mtime -def int_to_bigint(value): +def int_to_bigint(value): # legacy """Convert integers larger than 64 bits to bytearray Smaller integers are left alone @@ -180,3 +180,14 @@ def int_to_bigint(value): if value.bit_length() > 63: return value.to_bytes((value.bit_length() + 9) // 8, 'little', signed=True) return value + + +def int_to_timestamp(ns): + return Timestamp.from_unix_nano(ns) + + +def timestamp_to_int(ts): + if isinstance(ts, Timestamp): + return ts.to_unix_nano() + # legacy support note: we need to keep the bigint conversion for compatibility with borg < 1.3 archives. + return bigint_to_int(ts) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 3e145ada2..9bcbce22c 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -19,6 +19,7 @@ logger = create_logger() from .errors import Error from .fs import get_keys_dir +from .msgpack import Timestamp from .time import OutputTimestamp, format_time, to_localtime, safe_timestamp, safe_s from .. import __version__ as borg_version from .. import __version_tuple__ as borg_version_tuple @@ -1043,6 +1044,8 @@ def prepare_dump_dict(d): value = decode_tuple(value) elif isinstance(value, bytes): value = decode_bytes(value) + elif isinstance(value, Timestamp): + value = value.to_unix_nano() if isinstance(key, bytes): key = key.decode() res[key] = value diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 0b2598ffe..764279db0 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -3,9 +3,9 @@ from collections import namedtuple from .constants import ITEM_KEYS, ARCHIVE_KEYS from .helpers import safe_encode, safe_decode -from .helpers import bigint_to_int, int_to_bigint from .helpers import StableDict from .helpers import format_file_size +from .helpers.msgpack import timestamp_to_int, int_to_timestamp cdef extern from "_item.c": @@ -171,11 +171,10 @@ 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) - birthtime = PropDict._make_property('birthtime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int) + atime = PropDict._make_property('atime', int, 'int (ns)', encode=int_to_timestamp, decode=timestamp_to_int) + ctime = PropDict._make_property('ctime', int, 'int (ns)', encode=int_to_timestamp, decode=timestamp_to_int) + mtime = PropDict._make_property('mtime', int, 'int (ns)', encode=int_to_timestamp, decode=timestamp_to_int) + birthtime = PropDict._make_property('birthtime', int, 'int (ns)', encode=int_to_timestamp, decode=timestamp_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 aa40cc066..80b38edce 100644 --- a/src/borg/testsuite/item.py +++ b/src/borg/testsuite/item.py @@ -3,6 +3,7 @@ import pytest from ..cache import ChunkListEntry from ..item import Item from ..helpers import StableDict +from ..helpers.msgpack import Timestamp def test_item_empty(): @@ -77,15 +78,15 @@ def test_item_int_property(): item.mode = "invalid" -def test_item_bigint_property(): +def test_item_mptimestamp_property(): item = Item() small, big = 42, 2 ** 65 item.atime = small assert item.atime == small - assert item.as_dict() == {'atime': small} + assert item.as_dict() == {'atime': Timestamp.from_unix_nano(small)} item.atime = big assert item.atime == big - assert item.as_dict() == {'atime': b'\0' * 8 + b'\x02'} + assert item.as_dict() == {'atime': Timestamp.from_unix_nano(big)} def test_item_user_group_none(): From 6bfdb3f63016a73b320c89fce4a80ca08e4ad49d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 8 May 2022 14:14:47 +0200 Subject: [PATCH 007/160] refactor hardlink_master processing globally borg now has the chunks list in every item with content. due to the symmetric way how borg now deals with hardlinks using item.hlid, processing gets much simpler. but some places where borg deals with other "sources" of hardlinks still need to do some hardlink management: borg uses the HardLinkManager there now (which is not much more than a dict, but keeps documentation at one place and avoids some code duplication we had before). item.hlid is computed via hardlink_id function. support hardlinked symlinks, fixes #2379 as we use item.hlid now to group hardlinks together, there is no conflict with the item.source usage for symlink targets any more. 2nd+ hardlinks now add to the files count as did the 1st one. for borg, now all hardlinks are created equal. so any hardlink item with chunks now adds to the "file" count. ItemFormatter: support {hlid} instead of {source} for hardlinks --- docs/faq.rst | 3 - docs/internals/data-structures.rst | 2 +- docs/usage/general/file-metadata.rst.inc | 2 +- src/borg/archive.py | 286 +++++++++-------------- src/borg/archiver.py | 95 +++----- src/borg/fuse.py | 39 ++-- src/borg/helpers/fs.py | 68 +++++- src/borg/helpers/parseformat.py | 14 +- src/borg/item.pyx | 1 - src/borg/testsuite/archiver.py | 44 +++- 10 files changed, 263 insertions(+), 291 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 05f5176bc..d16eff6c0 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -132,9 +132,6 @@ Which file types, attributes, etc. are *not* preserved? Archive extraction has optional support to extract all-zero chunks as holes in a sparse file. * Some filesystem specific attributes, like btrfs NOCOW, see :ref:`platforms`. - * For hardlinked symlinks, the hardlinking can not be archived (and thus, - the hardlinking will not be done at extraction time). The symlinks will - be archived and extracted as non-hardlinked symlinks, see :issue:`2379`. Are there other known limitations? ---------------------------------- diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 2c5b7c194..d1a5a4cd3 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -567,7 +567,7 @@ dictionary created by the ``Item`` class that contains: * uid * gid * mode (item type + permissions) -* source (for symlinks, and for hardlinks within one archive) +* source (for symlinks) * rdev (for device files) * mtime, atime, ctime in nanoseconds * xattrs diff --git a/docs/usage/general/file-metadata.rst.inc b/docs/usage/general/file-metadata.rst.inc index 8f4c67cbf..c2694d188 100644 --- a/docs/usage/general/file-metadata.rst.inc +++ b/docs/usage/general/file-metadata.rst.inc @@ -10,7 +10,7 @@ Besides regular file and directory structures, Borg 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) +* hardlinked regular files, devices, symlinks, FIFOs (considering all items in the same archive) * timestamps in nanosecond precision: mtime, atime, ctime * other timestamps: birthtime (on platforms supporting it) * permissions: diff --git a/src/borg/archive.py b/src/borg/archive.py index ba95a0867..da083eac6 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -28,7 +28,7 @@ from .constants import * # NOQA from .crypto.low_level import IntegrityError as IntegrityErrorBase from .hashindex import ChunkIndex, ChunkIndexEntry, CacheSynchronizer from .helpers import Manifest -from .helpers import hardlinkable +from .helpers import HardLinkManager from .helpers import ChunkIteratorFileWrapper, open_item from .helpers import Error, IntegrityError, set_ec from .platform import uid2user, user2uid, gid2group, group2gid @@ -280,7 +280,7 @@ class DownloadPipeline: self.repository = repository self.key = key - def unpack_many(self, ids, filter=None, partial_extract=False, preload=False, hardlink_masters=None): + def unpack_many(self, ids, *, filter=None, preload=False): """ Return iterator of items. @@ -290,10 +290,7 @@ class DownloadPipeline: 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. """ - def _preload(chunks): - self.repository.preload([c.id for c in chunks]) - - masters_preloaded = set() + hlids_preloaded = set() unpacker = msgpack.Unpacker(use_list=False) for data in self.fetch_many(ids): unpacker.feed(data) @@ -306,33 +303,20 @@ class DownloadPipeline: items = [item for item in items if filter(item)] if preload: - if filter and partial_extract: - # if we do only a partial extraction, it gets a bit - # complicated with computing the preload items: if a hardlink master item is not - # selected (== not extracted), we will still need to preload its chunks if a - # corresponding hardlink slave is selected (== is extracted). - # due to a side effect of the filter() call, we now have hardlink_masters dict populated. - for item in items: - if hardlinkable(item.mode): - source = item.get('source') - if source is None: # maybe a hardlink master - if 'chunks' in item: - _preload(item.chunks) - # if this is a hl master, remember that we already preloaded all chunks of it (if any): - if item.get('hardlink_master', True): - masters_preloaded.add(item.path) - else: # hardlink slave - if source not in masters_preloaded: - # we only need to preload *once* (for the 1st selected slave) - chunks, _ = hardlink_masters[source] - if chunks is not None: - _preload(chunks) - masters_preloaded.add(source) - else: - # easy: we do not have a filter, thus all items are selected, thus we need to preload all chunks. - for item in items: - if 'chunks' in item: - _preload(item.chunks) + for item in items: + if 'chunks' in item: + hlid = item.get('hlid', None) + if hlid is None: + preload_chunks = True + else: + if hlid in hlids_preloaded: + preload_chunks = False + else: + # not having the hardlink's chunks already preloaded for other hardlink to same inode + preload_chunks = True + hlids_preloaded.add(hlid) + if preload_chunks: + self.repository.preload([c.id for c in item.chunks]) for item in items: yield item @@ -443,7 +427,6 @@ class Archive: self.repository = repository self.cache = cache self.manifest = manifest - self.hard_links = {} self.stats = Statistics(output_json=log_json, iec=iec) self.iec = iec self.show_progress = progress @@ -584,12 +567,10 @@ Utilization of max. archive size: {csize_max:.0%} return False return filter(item) if filter else True - def iter_items(self, filter=None, partial_extract=False, preload=False, hardlink_masters=None): + def iter_items(self, filter=None, preload=False): # note: when calling this with preload=True, later fetch_many() must be called with # is_preloaded=True or the RemoteRepository code will leak memory! - assert not (filter and partial_extract and preload) or hardlink_masters is not None - for item in self.pipeline.unpack_many(self.metadata.items, partial_extract=partial_extract, - preload=preload, hardlink_masters=hardlink_masters, + for item in self.pipeline.unpack_many(self.metadata.items, preload=preload, filter=lambda item: self.item_filter(item, filter)): yield item @@ -719,33 +700,30 @@ Utilization of max. archive size: {csize_max:.0%} return stats @contextmanager - def extract_helper(self, dest, item, path, stripped_components, original_path, hardlink_masters): + def extract_helper(self, item, path, hlm, *, dry_run=False): 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 and has_link: - # 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 'hlid' in item: + link_target = hlm.retrieve(id=item.hlid) + if link_target is not None and has_link: + if not dry_run: + # another hardlink to same inode (same hlid) was extracted previously, just link to it + with backup_io('link'): + os.link(link_target, path, follow_symlinks=False) + hardlink_set = True yield hardlink_set - if not hardlink_set and hardlink_masters: - if has_link: - # Update master entry with extracted item path, so that following hardlinks don't extract twice. + if not hardlink_set: + if 'hlid' in item and has_link: + # Update entry with extracted item path, so that following hardlinks don't extract twice. # We have hardlinking support, so we will hardlink not extract. - hardlink_masters[item.get('source') or original_path] = (None, path) + hlm.remember(id=item.hlid, info=path) else: # Broken platform with no hardlinking support. # In this case, we *want* to extract twice, because there is no other way. pass 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): + hlm=None, stripped_components=0, original_path=None, pi=None): """ Extract archive item. @@ -754,29 +732,33 @@ Utilization of max. archive size: {csize_max:.0%} :param dry_run: do not write any data :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 hlm: maps hlid to 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: - 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: - raise BackupError('Size inconsistency detected: size {}, chunks size {}'.format( - item_size, item_chunks_size)) + with self.extract_helper(item, '', hlm, dry_run=dry_run or stdout) as hardlink_set: + if not hardlink_set: + # it does not really set hardlinks due to dry_run, but we need to behave same + # as non-dry_run concerning fetching preloaded chunks from the pipeline or + # it would get stuck. + 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: + raise BackupError('Size inconsistency detected: size {}, chunks size {}'.format( + item_size, item_chunks_size)) if has_damaged_chunks: raise BackupError('File has damaged (all-zero) chunks. Try running borg check --repair.') return @@ -807,8 +789,7 @@ Utilization of max. archive size: {csize_max:.0%} if stat.S_ISREG(mode): with backup_io('makedirs'): make_parent(path) - with self.extract_helper(dest, item, path, stripped_components, original_path, - hardlink_masters) as hardlink_set: + with self.extract_helper(item, path, hlm) as hardlink_set: if hardlink_set: return with backup_io('open'): @@ -847,24 +828,26 @@ Utilization of max. archive size: {csize_max:.0%} self.restore_attrs(path, item) elif stat.S_ISLNK(mode): make_parent(path) - source = item.source - try: - os.symlink(source, path) - except UnicodeEncodeError: - raise self.IncompatibleFilesystemEncodingError(source, sys.getfilesystemencoding()) from None - self.restore_attrs(path, item, symlink=True) + with self.extract_helper(item, path, hlm) as hardlink_set: + if hardlink_set: + # unusual, but possible: this is a hardlinked symlink. + return + source = item.source + try: + os.symlink(source, path) + except UnicodeEncodeError: + raise self.IncompatibleFilesystemEncodingError(source, sys.getfilesystemencoding()) from None + self.restore_attrs(path, item, symlink=True) elif stat.S_ISFIFO(mode): make_parent(path) - with self.extract_helper(dest, item, path, stripped_components, original_path, - hardlink_masters) as hardlink_set: + with self.extract_helper(item, path, hlm) 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) - with self.extract_helper(dest, item, path, stripped_components, original_path, - hardlink_masters) as hardlink_set: + with self.extract_helper(item, path, hlm) as hardlink_set: if hardlink_set: return os.mknod(path, item.mode, item.rdev) @@ -1041,79 +1024,43 @@ Utilization of max. archive size: {csize_max:.0%} :param can_compare_chunk_ids: Whether --chunker-params are the same for both archives. """ - def hardlink_master_seen(item): - 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 and hardlinkable(item.mode) - - def update_hardlink_masters(item1, item2): - if is_hardlink_master(item1) or is_hardlink_master(item2): - hardlink_masters[item1.path] = (item1, item2) - - def has_hardlink_master(item, hardlink_masters): - return hardlinkable(item.mode) and item.get('source') in hardlink_masters - def compare_items(item1, item2): - if has_hardlink_master(item1, hardlink_masters): - item1 = hardlink_masters[item1.source][0] - if has_hardlink_master(item2, hardlink_masters): - item2 = hardlink_masters[item2.source][1] return ItemDiff(item1, item2, archive1.pipeline.fetch_many([c.id for c in item1.get('chunks', [])]), archive2.pipeline.fetch_many([c.id for c in item2.get('chunks', [])]), can_compare_chunk_ids=can_compare_chunk_ids) - def defer_if_necessary(item1, item2): - """Adds item tuple to deferred if necessary and returns True, if items were deferred""" - update_hardlink_masters(item1, item2) - defer = not hardlink_master_seen(item1) or not hardlink_master_seen(item2) - if defer: - deferred.append((item1, item2)) - return defer - orphans_archive1 = OrderedDict() orphans_archive2 = OrderedDict() - deferred = [] - hardlink_masters = {} for item1, item2 in zip_longest( archive1.iter_items(lambda item: matcher.match(item.path)), archive2.iter_items(lambda item: matcher.match(item.path)), ): if item1 and item2 and item1.path == item2.path: - if not defer_if_necessary(item1, item2): - yield (item1.path, compare_items(item1, item2)) + yield (item1.path, compare_items(item1, item2)) continue if item1: matching_orphan = orphans_archive2.pop(item1.path, None) if matching_orphan: - if not defer_if_necessary(item1, matching_orphan): - yield (item1.path, compare_items(item1, matching_orphan)) + yield (item1.path, compare_items(item1, matching_orphan)) else: orphans_archive1[item1.path] = item1 if item2: matching_orphan = orphans_archive1.pop(item2.path, None) if matching_orphan: - if not defer_if_necessary(matching_orphan, item2): - yield (matching_orphan.path, compare_items(matching_orphan, item2)) + yield (matching_orphan.path, compare_items(matching_orphan, item2)) else: orphans_archive2[item2.path] = item2 # At this point orphans_* contain items that had no matching partner in the other archive for added in orphans_archive2.values(): path = added.path deleted_item = Item.create_deleted(path) - update_hardlink_masters(deleted_item, added) yield (path, compare_items(deleted_item, added)) for deleted in orphans_archive1.values(): path = deleted.path deleted_item = Item.create_deleted(path) - update_hardlink_masters(deleted, deleted_item) yield (path, compare_items(deleted, deleted_item)) - for item1, item2 in deferred: - assert hardlink_master_seen(item1) - assert hardlink_master_seen(item2) - yield (path, compare_items(item1, item2)) class MetadataCollector: @@ -1289,7 +1236,7 @@ class FilesystemObjectProcessors: self.show_progress = show_progress self.print_file_status = file_status_printer or (lambda *args: None) - self.hard_links = {} + self.hlm = HardLinkManager(id_type=tuple, info_type=tuple) # (dev, ino) -> (hlid, chunks) self.stats = Statistics(output_json=log_json, iec=iec) # threading: done by cache (including progress) self.cwd = os.getcwd() self.chunker = get_chunker(*chunker_params, seed=key.chunk_seed, sparse=sparse) @@ -1298,29 +1245,32 @@ class FilesystemObjectProcessors: 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 = hardlinkable and st.st_nlink > 1 + update_map = False 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... + status = 'h' # hardlink + hlid, chunks = self.hlm.retrieve(id=(st.st_ino, st.st_dev), default=(None, None)) + if hlid is None: + update_map = True + hlid = self.hlm.hardlink_id(item._dict['path']) + item.hlid = hlid + if chunks is not None: + item.chunks = chunks + yield item, status, hardlinked self.add_item(item, stats=self.stats) - # ... 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 + if update_map: + # remember the hlid of this fs object and if the item has chunks, + # also remember them, so we do not have to re-chunk a hardlink. + chunks = item.chunks if 'chunks' in item else None + self.hlm.remember(id=(st.st_ino, st.st_dev), info=(hlid, chunks)) def process_dir_with_fd(self, *, path, fd, st): - with self.create_helper(path, st, 'd', hardlinkable=False) as (item, status, hardlinked, hardlink_master): + with self.create_helper(path, st, 'd', hardlinkable=False) as (item, status, hardlinked): item.update(self.metadata_collector.stat_attrs(st, path, fd=fd)) return status def process_dir(self, *, path, parent_fd, name, st): - with self.create_helper(path, st, 'd', hardlinkable=False) as (item, status, hardlinked, hardlink_master): + with self.create_helper(path, st, 'd', hardlinkable=False) as (item, status, hardlinked): with OsOpen(path=path, parent_fd=parent_fd, name=name, flags=flags_dir, noatime=True, op='dir_open') as fd: # fd is None for directories on windows, in that case a race condition check is not possible. @@ -1331,7 +1281,7 @@ class FilesystemObjectProcessors: return status def process_fifo(self, *, path, parent_fd, name, st): - with self.create_helper(path, st, 'f') as (item, status, hardlinked, hardlink_master): # fifo + with self.create_helper(path, st, 'f') as (item, status, hardlinked): # fifo with OsOpen(path=path, parent_fd=parent_fd, name=name, flags=flags_normal, noatime=True) as fd: with backup_io('fstat'): st = stat_update_check(st, os.fstat(fd)) @@ -1339,7 +1289,7 @@ class FilesystemObjectProcessors: return status def process_dev(self, *, path, parent_fd, name, st, dev_type): - with self.create_helper(path, st, dev_type) as (item, status, hardlinked, hardlink_master): # char/block device + with self.create_helper(path, st, dev_type) as (item, status, hardlinked): # char/block device # looks like we can not work fd-based here without causing issues when trying to open/close the device with backup_io('stat'): st = stat_update_check(st, os_stat(path=path, parent_fd=parent_fd, name=name, follow_symlinks=False)) @@ -1348,10 +1298,7 @@ class FilesystemObjectProcessors: return status def process_symlink(self, *, path, parent_fd, name, st): - # note: using hardlinkable=False because we can not support hardlinked symlinks, - # due to the dual-use of item.source, see issue #2343: - # hardlinked symlinks will be archived [and extracted] as non-hardlinked symlinks. - with self.create_helper(path, st, 's', hardlinkable=False) as (item, status, hardlinked, hardlink_master): + with self.create_helper(path, st, 's', hardlinkable=True) as (item, status, hardlinked): fname = name if name is not None and parent_fd is not None else path with backup_io('readlink'): source = os.readlink(fname, dir_fd=parent_fd) @@ -1384,7 +1331,7 @@ class FilesystemObjectProcessors: return status def process_file(self, *, path, parent_fd, name, st, cache, flags=flags_normal): - with self.create_helper(path, st, None) as (item, status, hardlinked, hardlink_master): # no status yet + with self.create_helper(path, st, None) as (item, status, hardlinked): # no status yet with OsOpen(path=path, parent_fd=parent_fd, name=name, flags=flags, noatime=True) as fd: with backup_io('fstat'): st = stat_update_check(st, os.fstat(fd)) @@ -1395,7 +1342,9 @@ class FilesystemObjectProcessors: # so it can be extracted / accessed in FUSE mount like a regular file. # this needs to be done early, so that part files also get the patched mode. item.mode = stat.S_IFREG | stat.S_IMODE(item.mode) - if not hardlinked or hardlink_master: + if 'chunks' in item: # create_helper might have put chunks from a previous hardlink there + [cache.chunk_incref(id_, self.stats) for id_, _, _ in item.chunks] + else: # normal case, no "2nd+" hardlink if not is_special_file: hashed_path = safe_encode(os.path.join(self.cwd, path)) path_hash = self.key.id_hash(hashed_path) @@ -1420,7 +1369,6 @@ class FilesystemObjectProcessors: status = 'M' if known else 'A' # regular file, modified or added self.print_file_status(status, path) status = None # we already printed the status - item.hardlink_master = hardlinked # Only chunkify the file if needed if chunks is not None: item.chunks = chunks @@ -1444,7 +1392,7 @@ class FilesystemObjectProcessors: # also, we must not memorize a potentially inconsistent/corrupt file that # changed while we backed it up. cache.memorize_file(hashed_path, path_hash, st, [c.id for c in item.chunks]) - self.stats.nfiles += 1 + self.stats.nfiles += 1 item.update(self.metadata_collector.stat_ext_attrs(st, path, fd=fd)) item.get_size(memorize=True) return status @@ -1464,6 +1412,7 @@ class TarfileObjectProcessors: self.stats = Statistics(output_json=log_json, iec=iec) # threading: done by cache (including progress) self.chunker = get_chunker(*chunker_params, seed=key.chunk_seed, sparse=False) + self.hlm = HardLinkManager(id_type=str, info_type=list) # path -> chunks @contextmanager def create_helper(self, tarinfo, status=None, type=None): @@ -1504,11 +1453,21 @@ class TarfileObjectProcessors: item.rdev = os.makedev(tarinfo.devmajor, tarinfo.devminor) return status - def process_link(self, *, tarinfo, status, type): + def process_symlink(self, *, tarinfo, status, type): with self.create_helper(tarinfo, status, type) as (item, status): item.source = tarinfo.linkname return status + def process_hardlink(self, *, tarinfo, status, type): + with self.create_helper(tarinfo, status, type) as (item, status): + # create a not hardlinked borg item, reusing the chunks, see HardLinkManager.__doc__ + chunks = self.hlm.retrieve(tarinfo.linkname) + if chunks is not None: + item.chunks = chunks + item.get_size(memorize=True, from_chunks=True) + self.stats.nfiles += 1 + return status + def process_file(self, *, tarinfo, status, type, tar): with self.create_helper(tarinfo, status, type) as (item, status): self.print_file_status(status, tarinfo.name) @@ -1516,8 +1475,10 @@ class TarfileObjectProcessors: fd = tar.extractfile(tarinfo) self.process_file_chunks(item, self.cache, self.stats, self.show_progress, backup_io_iter(self.chunker.chunkify(fd))) - item.get_size(memorize=True) + item.get_size(memorize=True, from_chunks=True) self.stats.nfiles += 1 + # we need to remember ALL files, see HardLinkManager.__doc__ + self.hlm.remember(id=tarinfo.name, info=item.chunks) return status @@ -2127,34 +2088,11 @@ class ArchiveRecreater: def process_items(self, archive, target): matcher = self.matcher - target_is_subset = not matcher.empty() - hardlink_masters = {} if target_is_subset else None - - def item_is_hardlink_master(item): - return (target_is_subset and - hardlinkable(item.mode) and - item.get('hardlink_master', True) and - 'source' not in item) for item in archive.iter_items(): 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'), item.get('chunks_healthy'), None) continue - 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, chunks_healthy, new_source = hardlink_masters[item.source] - if new_source is None: - # First item to use this master, move the chunks - item.chunks = chunks - if chunks_healthy is not None: - item.chunks_healthy = chunks_healthy - hardlink_masters[item.source] = (None, None, item.path) - del item.source - else: - # Master was already moved, only update this item's source - item.source = new_source if self.dry_run: self.print_file_status('-', item.path) else: @@ -2261,7 +2199,7 @@ class ArchiveRecreater: tag_files = [] tagged_dirs = [] - # to support reading hard-linked CACHEDIR.TAGs (aka CACHE_TAG_NAME), similar to hardlink_masters: + # to support reading hard-linked CACHEDIR.TAGs (aka CACHE_TAG_NAME): cachedir_masters = {} if self.exclude_caches: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index eac059bb7..4b6fd1782 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -59,7 +59,7 @@ try: from .helpers import timestamp from .helpers import get_cache_dir, os_stat from .helpers import Manifest, AI_HUMAN_SORT_KEYS - from .helpers import hardlinkable + from .helpers import HardLinkManager from .helpers import StableDict from .helpers import check_python, check_extension_modules from .helpers import dir_is_tagged, is_slow_msgpack, is_supported_msgpack, yes, sysinfo @@ -347,12 +347,12 @@ class Archiver: def upgrade_item(item): """upgrade item as needed, get rid of legacy crap""" - if item.get('hardlink_master', True) and 'source' not in item and hardlinkable(item.mode): - item._dict['hlid'] = hlid = hashlib.sha256(item._dict['path']) - hardlink_masters[hlid] = (item._dict.get('chunks'), item._dict.get('chunks_healthy')) - elif 'source' in item and hardlinkable(item.mode): - item._dict['hlid'] = hlid = hashlib.sha256(item._dict['source']) - chunks, chunks_healthy = hardlink_masters.get(hlid, (None, None)) + if hlm.borg1_hardlink_master(item): + item._dict['hlid'] = hlid = hlm.hardlink_id(item._dict['path']) + hlm.remember(id=hlid, info=(item._dict.get('chunks'), item._dict.get('chunks_healthy'))) + elif hlm.borg1_hardlink_slave(item): + item._dict['hlid'] = hlid = hlm.hardlink_id(item._dict['source']) + chunks, chunks_healthy = hlm.retrieve(id=hlid, default=(None, None)) if chunks is not None: item._dict['chunks'] = chunks for chunk_id, _, _ in chunks: @@ -389,7 +389,7 @@ class Archiver: else: if not dry_run: print(f"{name}: copying archive to destination repo...") - hardlink_masters = {} + hlm = HardLinkManager(id_type=bytes, info_type=tuple) # hlid -> (chunks, chunks_healthy) other_archive = Archive(other_repository, other_key, other_manifest, name) archive = Archive(repository, key, manifest, name, cache=cache, create=True) if not dry_run else None for item in other_archive.iter_items(): @@ -1154,16 +1154,14 @@ class Archiver: self.print_file_status(status, path) @staticmethod - def build_filter(matcher, peek_and_store_hardlink_masters, strip_components): + def build_filter(matcher, strip_components): if strip_components: def item_filter(item): 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): matched = matcher.match(item.path) - peek_and_store_hardlink_masters(item, matched) return matched return item_filter @@ -1186,33 +1184,18 @@ class Archiver: sparse = args.sparse strip_components = args.strip_components dirs = [] - partial_extract = not matcher.empty() or strip_components - hardlink_masters = {} if partial_extract or not has_link else None + hlm = HardLinkManager(id_type=bytes, info_type=str) # hlid -> path - def peek_and_store_hardlink_masters(item, matched): - # not has_link: - # OS does not have hardlink capability thus we need to remember the chunks so that - # we can extract all hardlinks as separate normal (not-hardlinked) files instead. - # - # partial_extract and not matched and hardlinkable: - # we do not extract the very first hardlink, so we need to remember the chunks - # in hardlinks_master, so we can use them when we extract some 2nd+ hardlink item - # that has no chunks list. - if ((not has_link or (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) + filter = self.build_filter(matcher, strip_components) if progress: pi = ProgressIndicatorPercent(msg='%5.1f%% Extracting: %s', step=0.1, msgid='extract') pi.output('Calculating total archive size for the progress indicator (might take long for large archives)') - extracted_size = sum(item.get_size(hardlink_masters) for item in archive.iter_items(filter)) + extracted_size = sum(item.get_size() for item in archive.iter_items(filter)) pi.total = extracted_size else: pi = None - for item in archive.iter_items(filter, partial_extract=partial_extract, - preload=True, hardlink_masters=hardlink_masters): + 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:]) @@ -1227,13 +1210,13 @@ class Archiver: logging.getLogger('borg.output.list').info(remove_surrogates(item.path)) try: if dry_run: - archive.extract_item(item, dry_run=True, pi=pi) + archive.extract_item(item, dry_run=True, hlm=hlm, pi=pi) else: if stat.S_ISDIR(item.mode): dirs.append(item) archive.extract_item(item, stdout=stdout, restore_attrs=False) else: - archive.extract_item(item, stdout=stdout, sparse=sparse, hardlink_masters=hardlink_masters, + archive.extract_item(item, stdout=stdout, sparse=sparse, hlm=hlm, stripped_components=strip_components, original_path=orig_path, pi=pi) except (BackupOSError, BackupError) as e: self.print_warning('%s: %s', remove_surrogates(orig_path), e) @@ -1298,15 +1281,9 @@ class Archiver: 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 + hlm = HardLinkManager(id_type=bytes, info_type=str) # hlid -> path - 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) + filter = self.build_filter(matcher, strip_components) # The | (pipe) symbol instructs tarfile to use a streaming mode of operation # where it never seeks on the passed fileobj. @@ -1316,7 +1293,7 @@ class Archiver: 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)) + extracted_size = sum(item.get_size() for item in archive.iter_items(filter)) pi.total = extracted_size else: pi = None @@ -1351,9 +1328,8 @@ class Archiver: 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. + # The linkname in tar has 2 uses: + # 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... @@ -1362,23 +1338,16 @@ class Archiver: 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. + if 'hlid' in item: + linkname = hlm.retrieve(id=item.hlid) + if linkname is not None: + # the first hardlink was already added to the archive, add a tar-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 + else: tarinfo.size = item.get_size() stream = item_content_stream(item) - hardlink_masters[item.get('source') or original_path] = (None, item.path) + hlm.remember(id=item.hlid, info=item.path) else: tarinfo.size = item.get_size() stream = item_content_stream(item) @@ -1436,8 +1405,7 @@ class Archiver: ph['BORG.item.meta'] = meta_text return ph - for item in archive.iter_items(filter, partial_extract=partial_extract, - preload=True, hardlink_masters=hardlink_masters): + 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:]) @@ -2072,12 +2040,11 @@ class Archiver: elif tarinfo.isdir(): status = tfo.process_dir(tarinfo=tarinfo, status='d', type=stat.S_IFDIR) elif tarinfo.issym(): - status = tfo.process_link(tarinfo=tarinfo, status='s', type=stat.S_IFLNK) + status = tfo.process_symlink(tarinfo=tarinfo, status='s', type=stat.S_IFLNK) elif tarinfo.islnk(): - # tar uses the same hardlink model as borg (rather vice versa); the first instance of a hardlink - # is stored as a regular file, later instances are special entries referencing back to the - # first instance. - status = tfo.process_link(tarinfo=tarinfo, status='h', type=stat.S_IFREG) + # tar uses a hardlink model like: the first instance of a hardlink is stored as a regular file, + # later instances are special entries referencing back to the first instance. + status = tfo.process_hardlink(tarinfo=tarinfo, status='h', type=stat.S_IFREG) elif tarinfo.isblk(): status = tfo.process_dev(tarinfo=tarinfo, status='b', type=stat.S_IFBLK) elif tarinfo.ischr(): diff --git a/src/borg/fuse.py b/src/borg/fuse.py index e2ef8eaa9..b81f37f1f 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -35,7 +35,8 @@ from .crypto.low_level import blake2b_128 from .archiver import Archiver from .archive import Archive, get_item_uid_gid from .hashindex import FuseVersionsIndex -from .helpers import daemonize, daemonizing, hardlinkable, signal_handler, format_file_size, Error +from .helpers import daemonize, daemonizing, signal_handler, format_file_size, Error +from .helpers import HardLinkManager from .helpers import msgpack from .item import Item from .lrucache import LRUCache @@ -339,15 +340,9 @@ class FuseBackend: consider_part_files=self._args.consider_part_files) strip_components = self._args.strip_components matcher = Archiver.build_matcher(self._args.patterns, self._args.paths) - partial_extract = not matcher.empty() or strip_components - hardlink_masters = {} if partial_extract else None + hlm = HardLinkManager(id_type=bytes, info_type=str) # hlid -> path - 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 = Archiver.build_filter(matcher, peek_and_store_hardlink_masters, strip_components) + filter = Archiver.build_filter(matcher, strip_components) for item_inode, item in self.cache.iter_archive_items(archive.metadata.items, filter=filter, consider_part_files=self._args.consider_part_files): if strip_components: @@ -369,15 +364,13 @@ class FuseBackend: 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, - hardlink_masters, strip_components) + self._process_leaf(segments[-1], item, parent, prefix, is_dir, item_inode, hlm) 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, item_inode, hardlink_masters, stripped_components): + def _process_leaf(self, name, item, parent, prefix, is_dir, item_inode, hlm): path = item.path del item.path # save some space - hardlink_masters = hardlink_masters or {} def file_version(item, path): if 'chunks' in item: @@ -402,10 +395,9 @@ class FuseBackend: version_enc = os.fsencode('.%05d' % version) return name + version_enc + ext - if 'source' in item and hardlinkable(item.mode): - source = os.sep.join(item.source.split(os.sep)[stripped_components:]) - chunks, link_target = hardlink_masters.get(item.source, (None, source)) - if link_target: + if 'hlid' in item: + link_target = hlm.retrieve(id=item.hlid, default=None) + if link_target is not None: # Hard link was extracted previously, just link link_target = os.fsencode(link_target) if self.versions: @@ -415,19 +407,16 @@ class FuseBackend: try: inode = self.find_inode(link_target, prefix) except KeyError: - logger.warning('Skipping broken hard link: %s -> %s', path, source) + logger.warning('Skipping broken hard link: %s -> %s', path, link_target) return item = self.get_item(inode) item.nlink = item.get('nlink', 1) + 1 self._items[inode] = item - elif chunks is not None: - # assign chunks to this item, since the item which had the chunks was not extracted - item.chunks = chunks + else: inode = item_inode self._items[inode] = item - if hardlink_masters: - # Update master entry with extracted item path, so that following hardlinks don't extract twice. - hardlink_masters[item.source] = (None, path) + # remember extracted item path, so that following hardlinks don't extract twice. + hlm.remember(id=item.hlid, info=path) else: inode = item_inode @@ -436,7 +425,7 @@ class FuseBackend: enc_path = os.fsencode(path) version = file_version(item, enc_path) if version is not None: - # regular file, with contents - maybe a hardlink master + # regular file, with contents name = make_versioned_name(name, version) self.file_versions[enc_path] = version diff --git a/src/borg/helpers/fs.py b/src/borg/helpers/fs.py index d1a412da0..89b54a09d 100644 --- a/src/borg/helpers/fs.py +++ b/src/borg/helpers/fs.py @@ -1,4 +1,5 @@ import errno +import hashlib import os import os.path import re @@ -165,9 +166,70 @@ def make_path_safe(path): return _safe_re.sub('', path) or '.' -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) +class HardLinkManager: + """ + Manage hardlinks (and avoid code duplication doing so). + + A) When creating a borg2 archive from the filesystem, we have to maintain a mapping like: + (dev, ino) -> (hlid, chunks) # for fs_hl_targets + If we encounter the same (dev, ino) again later, we'll just re-use the hlid and chunks list. + + B) When extracting a borg2 archive to the filesystem, we have to maintain a mapping like: + hlid -> path + If we encounter the same hlid again later, we hardlink to the path of the already extracted content of same hlid. + + C) When transferring from a borg1 archive, we need: + path -> chunks, chunks_healthy # for borg1_hl_targets + If we encounter a regular file item with source == path later, we reuse chunks and chunks_healthy + and create the same hlid = hardlink_id(source). + + D) When importing a tar file (simplified 1-pass way for now, not creating borg hardlink items): + path -> chunks + If we encounter a LNK tar entry later with linkname==path, we re-use the chunks and create a regular file item. + For better hardlink support (including the very first hardlink item for each group of same-target hardlinks), + we would need a 2-pass processing, which is not yet implemented. + """ + def __init__(self, *, id_type, info_type): + self._map = {} + self.id_type = id_type + self.info_type = info_type + + def borg1_hardlinkable(self, mode): # legacy + return stat.S_ISREG(mode) or stat.S_ISBLK(mode) or stat.S_ISCHR(mode) or stat.S_ISFIFO(mode) + + def borg1_hardlink_master(self, item): # legacy + return item.get('hardlink_master', True) and 'source' not in item and self.borg1_hardlinkable(item.mode) + + def borg1_hardlink_slave(self, item): # legacy + return 'source' in item and self.borg1_hardlinkable(item.mode) + + def hardlink_id(self, path): + """compute a hardlink id from a path""" + assert isinstance(path, bytes) + return hashlib.sha256(path).digest() + + def remember(self, *, id, info): + """ + remember stuff from a (usually contentful) item. + + :param id: some id used to reference to the contentful item, could be: + a path (tar style, old borg style) [bytes] + a hlid (new borg style) [bytes] + a (dev, inode) tuple (filesystem) + :param info: information to remember, could be: + chunks / chunks_healthy list + hlid + """ + assert isinstance(id, self.id_type) + assert isinstance(info, self.info_type) + self._map[id] = info + + def retrieve(self, id, *, default=None): + """ + retrieve stuff to use it in a (usually contentless) item. + """ + assert isinstance(id, self.id_type) + return self._map.get(id, default) def scandir_keyfunc(dirent): diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 9bcbce22c..414402de0 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -695,7 +695,8 @@ class ItemFormatter(BaseFormatter): KEY_DESCRIPTIONS = { 'bpath': 'verbatim POSIX path, can contain any character except NUL', 'path': 'path interpreted as text (might be missing non-text characters, see bpath)', - 'source': 'link target for links (identical to linktarget)', + 'source': 'link target for symlinks (identical to linktarget)', + 'hlid': 'hard link identity (same if hardlinking same fs object)', 'extra': 'prepends {source} with " -> " for soft links and " link to " for hard links', 'csize': 'compressed size', 'dsize': 'deduplicated size', @@ -706,7 +707,7 @@ class ItemFormatter(BaseFormatter): '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'), + ('type', 'mode', 'uid', 'gid', 'user', 'group', 'path', 'bpath', 'source', 'linktarget', 'hlid', 'flags'), ('size', 'csize', 'dsize', 'dcsize', 'num_chunks', 'unique_chunks'), ('mtime', 'ctime', 'atime', 'isomtime', 'isoctime', 'isoatime'), tuple(sorted(hash_algorithms)), @@ -802,11 +803,9 @@ class ItemFormatter(BaseFormatter): extra = '' if source: source = remove_surrogates(source) - if item_type == 'l': - extra = ' -> %s' % source - else: - mode = 'h' + mode[1:] - extra = ' link to %s' % source + extra = ' -> %s' % source + hlid = item.get('hlid') + hlid = bin_to_hex(hlid) if hlid else '' item_data['type'] = item_type item_data['mode'] = mode item_data['user'] = item.user or item.uid @@ -822,6 +821,7 @@ class ItemFormatter(BaseFormatter): item_data['health'] = 'broken' if 'chunks_healthy' in item else 'healthy' item_data['source'] = source item_data['linktarget'] = source + item_data['hlid'] = hlid item_data['flags'] = item.get('bsdflags') for key in self.used_call_keys: item_data[key] = self.call_keys[key](item) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 764279db0..9ea76f2de 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -214,7 +214,6 @@ class Item(PropDict): 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: diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index b69fe819f..402239455 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -321,7 +321,7 @@ class ArchiverTestCaseBase(BaseTestCase): contents = b'X' * size fd.write(contents) - def create_test_files(self): + def create_test_files(self, create_hardlinks=True): """Create a minimal test case including all supported file types """ # File @@ -332,7 +332,7 @@ class ArchiverTestCaseBase(BaseTestCase): # File mode os.chmod('input/file1', 0o4755) # Hard link - if are_hardlinks_supported(): + if are_hardlinks_supported() and create_hardlinks: os.link(os.path.join(self.input_path, 'file1'), os.path.join(self.input_path, 'hardlink')) # Symlink @@ -432,7 +432,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 = 4 if has_lchflags else 5 # one file is UF_NODUMP + item_count = 5 if has_lchflags else 6 # one file is UF_NODUMP self.assert_in('Number of files: %d' % item_count, info_output) shutil.rmtree(self.cache_path) info_output2 = self.cmd('info', self.repository_location + '::test') @@ -506,6 +506,29 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('extract', self.repository_location + '::test') assert os.readlink('input/link1') == 'somewhere' + @pytest.mark.skipif(not are_symlinks_supported() or not are_hardlinks_supported(), + reason='symlinks or hardlinks not supported') + def test_hardlinked_symlinks_extract(self): + self.create_regular_file('target', size=1024) + with changedir('input'): + os.symlink('target', 'symlink1') + os.link('symlink1', 'symlink2', follow_symlinks=False) + self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + with changedir('output'): + output = self.cmd('extract', self.repository_location + '::test') + print(output) + with changedir('input'): + assert os.path.exists('target') + assert os.readlink('symlink1') == 'target' + assert os.readlink('symlink2') == 'target' + st1 = os.stat('symlink1', follow_symlinks=False) + st2 = os.stat('symlink2', follow_symlinks=False) + assert st1.st_nlink == 2 + assert st2.st_nlink == 2 + assert st1.st_ino == st2.st_ino + assert st1.st_size == st2.st_size + @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): @@ -2661,7 +2684,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): hl3 = os.path.join(mountpoint, 'input', 'hardlink3', 'hardlink3.00001') assert os.stat(hl1).st_ino == os.stat(hl2).st_ino == os.stat(hl3).st_ino assert open(hl3, 'rb').read() == b'123456' - # similar again, but exclude the hardlink master: + # similar again, but exclude the 1st hardlink: with self.fuse_mount(self.repository_location, mountpoint, '-o', 'versions', '-e', 'input/hardlink1'): if are_hardlinks_supported(): hl2 = os.path.join(mountpoint, 'input', 'hardlink2', 'hardlink2.00001') @@ -3475,7 +3498,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 assert os.stat('input/dir1/source2').st_nlink == 2 def test_import_tar(self, tar_format='PAX'): - self.create_test_files() + self.create_test_files(create_hardlinks=False) # hardlinks become separate files os.unlink('input/flagfile') self.cmd('init', '--encryption=none', self.repository_location) self.cmd('create', self.repository_location + '::src', 'input') @@ -3489,7 +3512,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_import_tar_gz(self, tar_format='GNU'): if not shutil.which('gzip'): pytest.skip('gzip is not installed') - self.create_test_files() + self.create_test_files(create_hardlinks=False) # hardlinks become separate files os.unlink('input/flagfile') self.cmd('init', '--encryption=none', self.repository_location) self.cmd('create', self.repository_location + '::src', 'input') @@ -4444,26 +4467,23 @@ def test_chunk_content_equal(): class TestBuildFilter: - @staticmethod - def peek_and_store_hardlink_masters(item, matched): - pass def test_basic(self): matcher = PatternMatcher() matcher.add([parse_pattern('included')], IECommand.Include) - filter = Archiver.build_filter(matcher, self.peek_and_store_hardlink_masters, 0) + filter = Archiver.build_filter(matcher, 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.peek_and_store_hardlink_masters, 0) + filter = Archiver.build_filter(matcher, 0) assert filter(Item(path='anything')) def test_strip_components(self): matcher = PatternMatcher(fallback=True) - filter = Archiver.build_filter(matcher, self.peek_and_store_hardlink_masters, strip_components=1) + filter = Archiver.build_filter(matcher, 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 e5f1a4fb4d74cc5cdc1cd38f14200f4db782a35a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 11 May 2022 23:52:04 +0200 Subject: [PATCH 008/160] recreate: cachedir_masters not needed any more now all hardlinked regular file items have chunks. --- src/borg/archive.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index da083eac6..ed17465ea 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -2199,30 +2199,13 @@ class ArchiveRecreater: tag_files = [] tagged_dirs = [] - # to support reading hard-linked CACHEDIR.TAGs (aka CACHE_TAG_NAME): - cachedir_masters = {} - - if self.exclude_caches: - # sadly, due to how CACHEDIR.TAG works (filename AND file [header] contents) and - # how borg deals with hardlinks (slave hardlinks referring back to master hardlinks), - # we need to pass over the archive collecting hardlink master paths. - # as seen in issue #4911, the master paths can have an arbitrary filenames, - # not just CACHEDIR.TAG. - for item in archive.iter_items(filter=lambda item: os.path.basename(item.path) == CACHE_TAG_NAME): - if stat.S_ISREG(item.mode) and 'chunks' not in item and 'source' in item: - # this is a hardlink slave, referring back to its hardlink master (via item.source) - cachedir_masters[item.source] = None # we know the key (path), but not the value (item) yet - for item in archive.iter_items( filter=lambda item: os.path.basename(item.path) == CACHE_TAG_NAME or matcher.match(item.path)): - if self.exclude_caches and item.path in cachedir_masters: - cachedir_masters[item.path] = item dir, tag_file = os.path.split(item.path) if tag_file in self.exclude_if_present: exclude(dir, item) elif self.exclude_caches and tag_file == CACHE_TAG_NAME and stat.S_ISREG(item.mode): - content_item = item if 'chunks' in item else cachedir_masters[item.source] - file = open_item(archive, content_item) + file = open_item(archive, item) if file.read(len(CACHE_TAG_CONTENTS)) == CACHE_TAG_CONTENTS: exclude(dir, item) matcher.add(tag_files, IECommand.Include) From d3dfa3be30a01012f1948a086e84d6d36cd33f49 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 17 May 2022 02:00:00 +0200 Subject: [PATCH 009/160] use version 2 for new archives but still be able to read v1 archives for borg transfer. --- src/borg/archive.py | 8 ++++---- src/borg/cache.py | 2 +- src/borg/testsuite/archiver.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index ed17465ea..ba19e72bb 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -472,7 +472,7 @@ class Archive: def _load_meta(self, id): data = self.key.decrypt(id, self.repository.get(id)) metadata = ArchiveItem(internal_dict=msgpack.unpackb(data)) - if metadata.version != 1: + if metadata.version not in (1, 2): # legacy: still need to read v1 archives raise Exception('Unknown archive metadata version') return metadata @@ -601,7 +601,7 @@ Utilization of max. archive size: {csize_max:.0%} self.start = start self.end = end metadata = { - 'version': 1, + 'version': 2, 'name': name, 'comment': comment or '', 'items': self.items_buffer.chunks, @@ -1748,7 +1748,7 @@ class ArchiveChecker: continue if not valid_msgpacked_dict(data, archive_keys_serialized): continue - if b'cmdline' not in data or b'\xa7version\x01' not in data: + if b'cmdline' not in data or b'\xa7version\x02' not in data: continue try: archive = msgpack.unpackb(data) @@ -1989,7 +1989,7 @@ class ArchiveChecker: del self.manifest.archives[info.name] continue archive = ArchiveItem(internal_dict=msgpack.unpackb(data)) - if archive.version != 1: + if archive.version != 2: raise Exception('Unknown archive metadata version') archive.cmdline = [safe_decode(arg) for arg in archive.cmdline] items_buffer = ChunkBuffer(self.key) diff --git a/src/borg/cache.py b/src/borg/cache.py index 6cd612359..58ceb541b 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -757,7 +757,7 @@ class LocalCache(CacheStatsMixin): 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: + if archive.version not in (1, 2): # legacy raise Exception('Unknown archive metadata version') sync = CacheSynchronizer(chunk_idx) for item_id, (csize, data) in zip(archive.items, decrypted_repository.get_many(archive.items)): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 402239455..32feeb44e 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -3873,7 +3873,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): 'username': 'bar', 'name': 'archive1', 'time': '2016-12-15T18:49:51.849711', - 'version': 1, + 'version': 2, }) archive_id = key.id_hash(archive) repository.put(archive_id, key.encrypt(archive_id, archive)) From 8798b0340a63af9829e7d223755c58b2d04a4363 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 17 May 2022 17:21:19 +0200 Subject: [PATCH 010/160] use whitelist approach to make sure item._dict is clean --- src/borg/archiver.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 4b6fd1782..23ab03d93 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -345,6 +345,11 @@ class Archiver: other_repository=None, other_manifest=None, other_key=None): """archives transfer from other repository""" + ITEM_KEY_WHITELIST = {'path', 'source', 'rdev', 'chunks', 'chunks_healthy', 'hlid', + 'mode', 'user', 'group', 'uid', 'gid', 'mtime', 'atime', 'ctime', 'birthtime', 'size', + 'xattrs', 'bsdflags', 'acl_nfs4', 'acl_access', 'acl_default', 'acl_extended', + 'part'} + def upgrade_item(item): """upgrade item as needed, get rid of legacy crap""" if hlm.borg1_hardlink_master(item): @@ -364,10 +369,14 @@ class Archiver: if attr in item: ns = getattr(item, attr) # decode (bigint or Timestamp) --> int ns setattr(item, attr, ns) # encode int ns --> msgpack.Timestamp only, no bigint any more - item._dict.pop('hardlink_master', None) # not used for hardlinks any more, replaced by hlid - item._dict.pop('acl', None) # remove remnants of bug in attic <= 0.13 - item.get_size(memorize=True) # if not already present: compute+remember size for items with chunks - return item + # make sure we only have desired stuff in the new item. specifically, make sure to get rid of: + # - 'acl' remnants of bug in attic <= 0.13 + # - 'hardlink_master' (superseded by hlid) + new_item_dict = {key: value for key, value in item.as_dict().items() if key in ITEM_KEY_WHITELIST} + new_item = Item(internal_dict=new_item_dict) + new_item.get_size(memorize=True) # if not already present: compute+remember size for items with chunks + assert all(key in new_item for key in REQUIRED_ITEM_KEYS) + return new_item def upgrade_compressed_chunk(chunk): if ZLIB_legacy.detect(chunk): From 32a3601e4a10ac381eaa6c7a6e665c676ae036b9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 17 May 2022 19:46:52 +0200 Subject: [PATCH 011/160] compute hlid from inode / device --- src/borg/archive.py | 14 +++++++------- src/borg/archiver.py | 4 ++-- src/borg/helpers/fs.py | 14 ++++++++++---- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index ba19e72bb..15cfc5d55 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1236,7 +1236,7 @@ class FilesystemObjectProcessors: self.show_progress = show_progress self.print_file_status = file_status_printer or (lambda *args: None) - self.hlm = HardLinkManager(id_type=tuple, info_type=tuple) # (dev, ino) -> (hlid, chunks) + self.hlm = HardLinkManager(id_type=tuple, info_type=(list, type(None))) # (dev, ino) -> chunks or None self.stats = Statistics(output_json=log_json, iec=iec) # threading: done by cache (including progress) self.cwd = os.getcwd() self.chunker = get_chunker(*chunker_params, seed=key.chunk_seed, sparse=sparse) @@ -1249,20 +1249,20 @@ class FilesystemObjectProcessors: update_map = False if hardlinked: status = 'h' # hardlink - hlid, chunks = self.hlm.retrieve(id=(st.st_ino, st.st_dev), default=(None, None)) - if hlid is None: + nothing = object() + chunks = self.hlm.retrieve(id=(st.st_ino, st.st_dev), default=nothing) + if chunks is nothing: update_map = True - hlid = self.hlm.hardlink_id(item._dict['path']) - item.hlid = hlid - if chunks is not None: + elif chunks is not None: item.chunks = chunks + item.hlid = self.hlm.hardlink_id_from_inode(ino=st.st_ino, dev=st.st_dev) yield item, status, hardlinked self.add_item(item, stats=self.stats) if update_map: # remember the hlid of this fs object and if the item has chunks, # also remember them, so we do not have to re-chunk a hardlink. chunks = item.chunks if 'chunks' in item else None - self.hlm.remember(id=(st.st_ino, st.st_dev), info=(hlid, chunks)) + self.hlm.remember(id=(st.st_ino, st.st_dev), info=chunks) def process_dir_with_fd(self, *, path, fd, st): with self.create_helper(path, st, 'd', hardlinkable=False) as (item, status, hardlinked): diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 23ab03d93..7e0f67f48 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -353,10 +353,10 @@ class Archiver: def upgrade_item(item): """upgrade item as needed, get rid of legacy crap""" if hlm.borg1_hardlink_master(item): - item._dict['hlid'] = hlid = hlm.hardlink_id(item._dict['path']) + item._dict['hlid'] = hlid = hlm.hardlink_id_from_path(item._dict['path']) hlm.remember(id=hlid, info=(item._dict.get('chunks'), item._dict.get('chunks_healthy'))) elif hlm.borg1_hardlink_slave(item): - item._dict['hlid'] = hlid = hlm.hardlink_id(item._dict['source']) + item._dict['hlid'] = hlid = hlm.hardlink_id_from_path(item._dict['source']) chunks, chunks_healthy = hlm.retrieve(id=hlid, default=(None, None)) if chunks is not None: item._dict['chunks'] = chunks diff --git a/src/borg/helpers/fs.py b/src/borg/helpers/fs.py index 89b54a09d..fecda9c69 100644 --- a/src/borg/helpers/fs.py +++ b/src/borg/helpers/fs.py @@ -181,7 +181,7 @@ class HardLinkManager: C) When transferring from a borg1 archive, we need: path -> chunks, chunks_healthy # for borg1_hl_targets If we encounter a regular file item with source == path later, we reuse chunks and chunks_healthy - and create the same hlid = hardlink_id(source). + and create the same hlid = hardlink_id_from_path(source). D) When importing a tar file (simplified 1-pass way for now, not creating borg hardlink items): path -> chunks @@ -203,11 +203,17 @@ class HardLinkManager: def borg1_hardlink_slave(self, item): # legacy return 'source' in item and self.borg1_hardlinkable(item.mode) - def hardlink_id(self, path): + def hardlink_id_from_path(self, path): """compute a hardlink id from a path""" assert isinstance(path, bytes) return hashlib.sha256(path).digest() + def hardlink_id_from_inode(self, *, ino, dev): + """compute a hardlink id from an inode""" + assert isinstance(ino, int) + assert isinstance(dev, int) + return hashlib.sha256(f'{ino}/{dev}'.encode()).digest() + def remember(self, *, id, info): """ remember stuff from a (usually contentful) item. @@ -220,8 +226,8 @@ class HardLinkManager: chunks / chunks_healthy list hlid """ - assert isinstance(id, self.id_type) - assert isinstance(info, self.info_type) + assert isinstance(id, self.id_type), f"key is {key!r}, not of type {self.key_type}" + assert isinstance(info, self.info_type), f"info is {info!r}, not of type {self.info_type}" self._map[id] = info def retrieve(self, id, *, default=None): From 6584a92c8108194adfd4ab8ab63ab0a9f5e87cc6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 17 May 2022 22:54:12 +0200 Subject: [PATCH 012/160] compression: use the 2 bytes for type and level, fixes #6698 adapt borg transfer, transferred chunks are set to compression level "unknown". --- src/borg/archiver.py | 7 ++++- src/borg/compress.pyx | 56 ++++++++++++++++++++++----------------- src/borg/testsuite/key.py | 8 +++--- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 7e0f67f48..7408931ca 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -379,8 +379,13 @@ class Archiver: return new_item def upgrade_compressed_chunk(chunk): + level = b'\xFF' # FF means unknown compression level if ZLIB_legacy.detect(chunk): - chunk = ZLIB.ID + chunk # get rid of the attic legacy: prepend separate type bytes for zlib + ctype = ZLIB.ID + chunk = ctype + level + chunk # get rid of the attic legacy: prepend separate type/level bytes + else: + ctype = chunk[0:1] + chunk = ctype + level + chunk[2:] # keep type same, but set level return chunk dry_run = args.dry_run diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index 7997456c6..70c95df70 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -56,16 +56,21 @@ cdef class CompressorBase: also handles compression format auto detection and adding/stripping the ID header (which enable auto detection). """ - ID = b'\xFF\xFF' # reserved and not used - # overwrite with a unique 2-bytes bytestring in child classes + ID = b'\xFF' # reserved and not used + # overwrite with a unique 1-byte bytestring in child classes name = 'baseclass' @classmethod def detect(cls, data): return data.startswith(cls.ID) - def __init__(self, **kwargs): - pass + def __init__(self, level=255, **kwargs): + assert 0 <= level <= 255 + if self.ID is not None: + self.id_level = self.ID + bytes((level, )) # level 255 means "unknown level" + assert len(self.id_level) == 2 + else: + self.id_level = None def decide(self, data): """ @@ -85,8 +90,8 @@ cdef class CompressorBase: 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 + # add id_level bytes + return self.id_level + data def decompress(self, data): """ @@ -96,7 +101,7 @@ cdef class CompressorBase: Only handles input generated by _this_ Compressor - for a general purpose decompression method see *Compressor.decompress*. """ - # strip ID bytes + # strip id_level bytes return data[2:] cdef class DecidingCompressor(CompressorBase): @@ -106,8 +111,8 @@ cdef class DecidingCompressor(CompressorBase): """ name = 'decidebaseclass' - def __init__(self, **kwargs): - super().__init__(**kwargs) + def __init__(self, level=255, **kwargs): + super().__init__(level=level, **kwargs) def _decide(self, data): """ @@ -148,9 +153,12 @@ class CNONE(CompressorBase): """ none - no compression, just pass through data """ - ID = b'\x00\x00' + ID = b'\x00' name = 'none' + def __init__(self, level=255, **kwargs): + super().__init__(level=level, **kwargs) # no defined levels for CNONE, so just say "unknown" + def compress(self, data): return super().compress(data) @@ -170,11 +178,11 @@ class LZ4(DecidingCompressor): - wrapper releases CPython's GIL to support multithreaded code - uses safe lz4 methods that never go beyond the end of the output buffer """ - ID = b'\x01\x00' + ID = b'\x01' name = 'lz4' - def __init__(self, **kwargs): - pass + def __init__(self, level=255, **kwargs): + super().__init__(level=level, **kwargs) # no defined levels for LZ4, so just say "unknown" def _decide(self, idata): """ @@ -235,11 +243,11 @@ class LZMA(DecidingCompressor): """ lzma compression / decompression """ - ID = b'\x02\x00' + ID = b'\x02' name = 'lzma' def __init__(self, level=6, **kwargs): - super().__init__(**kwargs) + super().__init__(level=level, **kwargs) self.level = level if lzma is None: raise ValueError('No lzma support found.') @@ -270,11 +278,11 @@ class ZSTD(DecidingCompressor): # This is a NOT THREAD SAFE implementation. # Only ONE python context must be created at a time. # It should work flawlessly as long as borg will call ONLY ONE compression job at time. - ID = b'\x03\x00' + ID = b'\x03' name = 'zstd' def __init__(self, level=3, **kwargs): - super().__init__(**kwargs) + super().__init__(level=level, **kwargs) self.level = level def _decide(self, idata): @@ -335,11 +343,11 @@ class ZLIB(DecidingCompressor): """ zlib compression / decompression (python stdlib) """ - ID = b'\x05\x00' + ID = b'\x05' name = 'zlib' def __init__(self, level=6, **kwargs): - super().__init__(**kwargs) + super().__init__(level=level, **kwargs) self.level = level def _decide(self, data): @@ -373,8 +381,8 @@ class ZLIB_legacy(CompressorBase): Newer borg uses the ZLIB class that has separate ID bytes (as all the other compressors) and does not need this hack. """ - ID = b'\x08\x00' # not used here, see detect() - # avoid all 0x.8.. IDs elsewhere! + ID = b'\x08' # not used here, see detect() + # avoid all 0x.8 IDs elsewhere! name = 'zlib_legacy' @classmethod @@ -386,7 +394,7 @@ class ZLIB_legacy(CompressorBase): return check_ok and is_deflate def __init__(self, level=6, **kwargs): - super().__init__(**kwargs) + super().__init__(level=level, **kwargs) self.level = level def compress(self, data): @@ -478,14 +486,14 @@ class ObfuscateSize(CompressorBase): """ Meta-Compressor that obfuscates the compressed data size. """ - ID = b'\x04\x00' + ID = b'\x04' name = 'obfuscate' header_fmt = Struct('>I') header_len = len(header_fmt.pack(0)) def __init__(self, level=None, compressor=None): - super().__init__() + super().__init__(level=level) # data will be encrypted, so we can tell the level self.compressor = compressor if level is None: pass # decompression diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index e0be752fa..5073c5b23 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -256,8 +256,8 @@ class TestKey: plaintext = b'123456789' id = key.id_hash(plaintext) authenticated = key.encrypt(id, plaintext) - # 0x07 is the key TYPE, \x0000 identifies no compression. - assert authenticated == b'\x07\x00\x00' + plaintext + # 0x07 is the key TYPE, \x00ff identifies no compression / unknown level. + assert authenticated == b'\x07\x00\xff' + plaintext def test_blake2_authenticated_encrypt(self, monkeypatch): monkeypatch.setenv('BORG_PASSPHRASE', 'test') @@ -267,8 +267,8 @@ class TestKey: plaintext = b'123456789' id = key.id_hash(plaintext) authenticated = key.encrypt(id, plaintext) - # 0x06 is the key TYPE, 0x0000 identifies no compression. - assert authenticated == b'\x06\x00\x00' + plaintext + # 0x06 is the key TYPE, 0x00ff identifies no compression / unknown level. + assert authenticated == b'\x06\x00\xff' + plaintext class TestTAM: From 72c68c49d09963e3106dca873d76fbd68db32682 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 17 May 2022 23:29:58 +0200 Subject: [PATCH 013/160] obfuscation: fix byte order for size, fixes #6701 --- src/borg/archiver.py | 12 +++++++++++- src/borg/compress.pyx | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 7408931ca..faf6aa949 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -29,6 +29,7 @@ try: from contextlib import contextmanager from datetime import datetime, timedelta from io import TextIOWrapper + from struct import Struct from .logger import create_logger, setup_logging @@ -44,7 +45,7 @@ try: from .archive import has_link from .cache import Cache, assert_secure, SecurityManager from .constants import * # NOQA - from .compress import CompressionSpec, ZLIB, ZLIB_legacy + from .compress import CompressionSpec, ZLIB, ZLIB_legacy, ObfuscateSize from .crypto.key import key_creator, key_argument_names, tam_required_file, tam_required from .crypto.key import RepoKey, KeyfileKey, Blake2RepoKey, Blake2KeyfileKey, FlexiKey from .crypto.keymanager import KeyManager @@ -386,6 +387,15 @@ class Archiver: else: ctype = chunk[0:1] chunk = ctype + level + chunk[2:] # keep type same, but set level + if ctype == ObfuscateSize.ID: + # in older borg, we used unusual byte order + old_header_fmt = Struct('>I') + new_header_fmt = ObfuscateSize.header_fmt + length = ObfuscateSize.header_len + size_bytes = chunk[2:2+length] + size = old_header_fmt.unpack(size_bytes) + size_bytes = new_header_fmt.pack(size) + chunk = chunk[0:2] + size_bytes + chunk[2+length:] return chunk dry_run = args.dry_run diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index 70c95df70..c63a04df3 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -489,7 +489,7 @@ class ObfuscateSize(CompressorBase): ID = b'\x04' name = 'obfuscate' - header_fmt = Struct('>I') + header_fmt = Struct(' Date: Wed, 18 May 2022 14:47:47 +0200 Subject: [PATCH 014/160] upgrade compressed chunk: fix treatment of ObfuscateSize chunks the inner payload of ObfuscateSize chunks are compressed chunks and need the same zlib fix and level patching as non-obfuscated compressed chunks. --- src/borg/archiver.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index faf6aa949..dc0db82f2 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -380,13 +380,18 @@ class Archiver: return new_item def upgrade_compressed_chunk(chunk): + def upgrade_zlib_and_level(chunk): + if ZLIB_legacy.detect(chunk): + ctype = ZLIB.ID + chunk = ctype + level + chunk # get rid of the attic legacy: prepend separate type/level bytes + else: + ctype = chunk[0:1] + chunk = ctype + level + chunk[2:] # keep type same, but set level + return chunk + + ctype = chunk[0:1] level = b'\xFF' # FF means unknown compression level - if ZLIB_legacy.detect(chunk): - ctype = ZLIB.ID - chunk = ctype + level + chunk # get rid of the attic legacy: prepend separate type/level bytes - else: - ctype = chunk[0:1] - chunk = ctype + level + chunk[2:] # keep type same, but set level + if ctype == ObfuscateSize.ID: # in older borg, we used unusual byte order old_header_fmt = Struct('>I') @@ -395,7 +400,11 @@ class Archiver: size_bytes = chunk[2:2+length] size = old_header_fmt.unpack(size_bytes) size_bytes = new_header_fmt.pack(size) - chunk = chunk[0:2] + size_bytes + chunk[2+length:] + compressed = chunk[2+length:] + compressed = upgrade_zlib_and_level(compressed) + chunk = ctype + level + size_bytes + compressed + else: + chunk = upgrade_zlib_and_level(chunk) return chunk dry_run = args.dry_run From f8dbe5b54293c18d3e6668a563c1cc861c0982bd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 5 May 2022 19:36:02 +0200 Subject: [PATCH 015/160] cleanup msgpack related str/bytes mess, see #968 see ticket and borg.helpers.msgpack docstring. --- src/borg/archive.py | 16 ++- src/borg/archiver.py | 12 +-- src/borg/cache_sync/unpack.h | 36 ++++--- src/borg/helpers/manifest.py | 46 ++++----- src/borg/helpers/msgpack.py | 101 +++++++++++++------ src/borg/item.pyx | 172 +++++++++++++++++++++++++++------ src/borg/remote.py | 49 +++++----- src/borg/testsuite/archive.py | 14 +-- src/borg/testsuite/archiver.py | 14 +-- src/borg/testsuite/item.py | 6 +- 10 files changed, 311 insertions(+), 155 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 15cfc5d55..dc244a9f1 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -34,7 +34,7 @@ from .helpers import Error, IntegrityError, set_ec from .platform import uid2user, user2uid, gid2group, group2gid from .helpers import parse_timestamp, to_localtime from .helpers import OutputTimestamp, format_timedelta, format_file_size, file_status, FileSize -from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates +from .helpers import safe_encode, make_path_safe, remove_surrogates from .helpers import StableDict from .helpers import bin_to_hex from .helpers import safe_ns @@ -479,7 +479,6 @@ class Archive: def load(self, id): self.id = id self.metadata = self._load_meta(self.id) - self.metadata.cmdline = [safe_decode(arg) for arg in self.metadata.cmdline] self.name = self.metadata.name self.comment = self.metadata.get('comment', '') @@ -1515,7 +1514,7 @@ class RobustUnpacker: """ def __init__(self, validator, item_keys): super().__init__() - self.item_keys = [msgpack.packb(name.encode()) for name in item_keys] + self.item_keys = [msgpack.packb(name) for name in item_keys] self.validator = validator self._buffered_data = [] self._resync = False @@ -1734,7 +1733,7 @@ class ArchiveChecker: # lost manifest on a older borg version than the most recent one that was ever used # within this repository (assuming that newer borg versions support more item keys). manifest = Manifest(self.key, self.repository) - archive_keys_serialized = [msgpack.packb(name.encode()) for name in ARCHIVE_KEYS] + archive_keys_serialized = [msgpack.packb(name) for name in ARCHIVE_KEYS] pi = ProgressIndicatorPercent(total=len(self.chunks), msg="Rebuilding manifest %6.2f%%", step=0.01, msgid='check.rebuild_manifest') for chunk_id, _ in self.chunks.iteritems(): @@ -1881,9 +1880,9 @@ class ArchiveChecker: Missing item chunks will be skipped and the msgpack stream will be restarted """ - item_keys = frozenset(key.encode() for key in self.manifest.item_keys) - required_item_keys = frozenset(key.encode() for key in REQUIRED_ITEM_KEYS) - unpacker = RobustUnpacker(lambda item: isinstance(item, StableDict) and b'path' in item, + item_keys = self.manifest.item_keys + required_item_keys = REQUIRED_ITEM_KEYS + unpacker = RobustUnpacker(lambda item: isinstance(item, StableDict) and 'path' in item, self.manifest.item_keys) _state = 0 @@ -1905,7 +1904,7 @@ class ArchiveChecker: def valid_item(obj): if not isinstance(obj, StableDict): return False, 'not a dictionary' - keys = set(obj) + keys = set(k.decode('utf-8', errors='replace') for k in obj) 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): @@ -1991,7 +1990,6 @@ class ArchiveChecker: archive = ArchiveItem(internal_dict=msgpack.unpackb(data)) if archive.version != 2: raise Exception('Unknown archive metadata version') - 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): diff --git a/src/borg/archiver.py b/src/borg/archiver.py index dc0db82f2..acef0db5e 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -55,7 +55,7 @@ try: from .helpers import PrefixSpec, GlobSpec, CommentSpec, SortBySpec, FilesCacheMode 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, eval_escapes + from .helpers import remove_surrogates, bin_to_hex, prepare_dump_dict, eval_escapes from .helpers import interval, prune_within, prune_split, PRUNING_PATTERNS from .helpers import timestamp from .helpers import get_cache_dir, os_stat @@ -1944,12 +1944,12 @@ class Archiver: 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): + if not manifest.tam_verified or not manifest.config.get('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.config['tam_required'] = True manifest.write() repository.commit(compact=False) if not key.tam_required: @@ -1972,7 +1972,7 @@ class Archiver: print('Key updated') if hasattr(key, 'find_key'): print('Key location:', key.find_key()) - manifest.config[b'tam_required'] = False + manifest.config['tam_required'] = False manifest.write() repository.commit(compact=False) else: @@ -2304,7 +2304,7 @@ class Archiver: """dump decoded archive metadata (not: data)""" try: - archive_meta_orig = manifest.archives.get_raw_dict()[safe_encode(args.location.archive)] + archive_meta_orig = manifest.archives.get_raw_dict()[args.location.archive] except KeyError: raise Archive.DoesNotExist(args.location.archive) @@ -2321,7 +2321,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['id'], repository.get(archive_meta_orig['id'])) archive_org_dict = msgpack.unpackb(data, object_hook=StableDict) fd.write(' "_meta":\n') diff --git a/src/borg/cache_sync/unpack.h b/src/borg/cache_sync/unpack.h index f6dd9ca8e..bba566419 100644 --- a/src/borg/cache_sync/unpack.h +++ b/src/borg/cache_sync/unpack.h @@ -384,19 +384,11 @@ static inline int unpack_callback_map_end(unpack_user* u) 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 */ + /* raw = what Borg uses for text stuff */ /* Note: p points to an internal buffer which contains l bytes. */ (void)b; switch(u->expect) { - case expect_key: - if(length != 32) { - SET_LAST_ERROR("Incorrect key length"); - return -1; - } - memcpy(u->current.key, p, 32); - u->expect = expect_size; - break; case expect_map_key: if(length == 6 && !memcmp("chunks", p, 6)) { u->expect = expect_chunks_begin; @@ -409,19 +401,31 @@ static inline int unpack_callback_raw(unpack_user* u, const char* b, const char* 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 length) { - (void)u; (void)b; (void)p; (void)length; - UNEXPECTED("bin"); + /* bin = what Borg uses for binary stuff */ + /* Note: p points to an internal buffer which contains l bytes. */ + (void)b; + + switch(u->expect) { + case expect_key: + if(length != 32) { + SET_LAST_ERROR("Incorrect key length"); + return -1; + } + memcpy(u->current.key, p, 32); + u->expect = expect_size; + break; + default: + if(u->inside_chunks) { + SET_LAST_ERROR("Unexpected bytes in chunks structure"); + return -1; + } + } return 0; } diff --git a/src/borg/helpers/manifest.py b/src/borg/helpers/manifest.py index 425feb4e6..1b9d91fb4 100644 --- a/src/borg/helpers/manifest.py +++ b/src/borg/helpers/manifest.py @@ -12,7 +12,7 @@ from ..logger import create_logger logger = create_logger() from .datastruct import StableDict -from .parseformat import bin_to_hex, safe_encode, safe_decode +from .parseformat import bin_to_hex from .time import parse_timestamp from .. import shellpattern from ..constants import * # NOQA @@ -39,39 +39,35 @@ class Archives(abc.MutableMapping): str timestamps or datetime timestamps. """ def __init__(self): - # key: encoded archive name, value: dict(b'id': bytes_id, b'time': bytes_iso_ts) + # key: str archive name, value: dict('id': bytes_id, 'time': str_iso_ts) self._archives = {} def __len__(self): return len(self._archives) def __iter__(self): - return iter(safe_decode(name) for name in self._archives) + return iter(self._archives) def __getitem__(self, name): assert isinstance(name, str) - _name = safe_encode(name) - values = self._archives.get(_name) + values = self._archives.get(name) if values is None: raise KeyError - ts = parse_timestamp(values[b'time'].decode()) - return ArchiveInfo(name=name, id=values[b'id'], ts=ts) + ts = parse_timestamp(values['time']) + return ArchiveInfo(name=name, id=values['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).strftime(ISO_FORMAT) assert isinstance(ts, str) - ts = ts.encode() - self._archives[name] = {b'id': id, b'time': ts} + self._archives[name] = {'id': id, 'time': ts} def __delitem__(self, name): assert isinstance(name, str) - name = safe_encode(name) del self._archives[name] def list(self, *, glob=None, match_end=r'\Z', sort_by=(), consider_checkpoints=True, first=None, last=None, reverse=False): @@ -116,8 +112,8 @@ class Archives(abc.MutableMapping): 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 + assert isinstance(k, str) + assert isinstance(v, dict) and 'id' in v and 'time' in v self._archives[k] = v def get_raw_dict(self): @@ -196,10 +192,10 @@ class Manifest: 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('item_keys', [])) + manifest.item_keys = ITEM_KEYS | frozenset(m.get('item_keys', [])) if manifest.tam_verified: - manifest_required = manifest.config.get(b'tam_required', False) + manifest_required = manifest.config.get('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...') @@ -214,32 +210,32 @@ class Manifest: def check_repository_compatibility(self, operations): for operation in operations: assert isinstance(operation, self.Operation) - feature_flags = self.config.get(b'feature_flags', None) + feature_flags = self.config.get('feature_flags', None) if feature_flags is None: return - if operation.value.encode() not in feature_flags: + if operation.value 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 + requirements = feature_flags[operation.value] + if 'mandatory' in requirements: + unsupported = set(requirements['mandatory']) - self.SUPPORTED_REPO_FEATURES if unsupported: - raise MandatoryFeatureUnsupported([f.decode() for f in unsupported]) + raise MandatoryFeatureUnsupported(list(unsupported)) def get_all_mandatory_features(self): result = {} - feature_flags = self.config.get(b'feature_flags', None) + feature_flags = self.config.get('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()] = {feature.decode() for feature in requirements[b'mandatory']} + if 'mandatory' in requirements: + result[operation] = set(requirements['mandatory']) return result def write(self): from ..item import ManifestItem if self.key.tam_required: - self.config[b'tam_required'] = True + self.config['tam_required'] = True # self.timestamp needs to be strictly monotonically increasing. Clocks often are not set correctly if self.timestamp is None: self.timestamp = datetime.utcnow().strftime(ISO_FORMAT) diff --git a/src/borg/helpers/msgpack.py b/src/borg/helpers/msgpack.py index 411f00fec..5a2edecd6 100644 --- a/src/borg/helpers/msgpack.py +++ b/src/borg/helpers/msgpack.py @@ -1,21 +1,56 @@ +""" +wrapping msgpack +================ + +Due to the planned breaking api changes in upstream msgpack, we wrap it the way we need it - +to avoid having lots of clutter in the calling code. see tickets #968 and #3632. + +Packing +------- +- use_bin_type = True (used by borg since borg 1.3) + This is used to generate output according to new msgpack 2.0 spec. + This cleanly keeps bytes and str types apart. + +- use_bin_type = False (used by borg < 1.3) + This creates output according to the older msgpack spec. + BAD: str and bytes were packed into same "raw" representation. + +- unicode_errors = 'surrogateescape' + Guess backup applications are one of the rare cases when this needs to be used. + It is needed because borg also needs to deal with data that does not cleanly encode/decode using utf-8. + There's a lot of crap out there, e.g. in filenames and as a backup tool, we must keep them as good as possible. + +Unpacking +--------- +- raw = True (the old way, used by borg <= 1.3) + This is currently still needed to not try to decode "raw" msgpack objects. + These could come either from str (new or old msgpack) or bytes (old msgpack). + Thus, we basically must know what we want and either keep the bytes we get + or decode them to str, if we want str. + +- raw = False (the new way) + This can be used in future, when we do not have to deal with data any more that was packed the old way. + It will then unpack according to the msgpack 2.0 spec format and directly output bytes or str. + +- unicode_errors = 'surrogateescape' -> see description above (will be used when raw is False). + +As of borg 1.3, we have the first part on the way to fix the msgpack str/bytes mess, #968. +borg now still needs to **read** old repos, archives, keys, ... so we can not yet fix it completely. +But from now on, borg only **writes** new data according to the new msgpack spec, +thus we can complete the fix for #968 in a later borg release. + +current way in msgpack terms +---------------------------- + +- pack with use_bin_type=True (according to msgpack 2.0 spec) +- packs str -> raw and bytes -> bin +- unpack with raw=True (aka "the old way") +- unpacks raw to bytes (thus we always need to decode manually if we want str) +""" + from .datastruct import StableDict from ..constants import * # NOQA -# wrapping msgpack --------------------------------------------------------------------------------------------------- -# -# due to the planned breaking api changes in upstream msgpack, we wrap it the way we need it - -# to avoid having lots of clutter in the calling code. see tickets #968 and #3632. -# -# Packing -# ------- -# use_bin_type = False is needed to generate the old msgpack format (not msgpack 2.0 spec) as borg always did. -# unicode_errors = None is needed because usage of it is deprecated -# -# Unpacking -# --------- -# raw = True is needed to unpack the old msgpack format to bytes (not str, about the decoding see item.pyx). -# unicode_errors = None is needed because usage of it is deprecated - from msgpack import Packer as mp_Packer from msgpack import packb as mp_packb from msgpack import pack as mp_pack @@ -30,6 +65,10 @@ from msgpack import OutOfData version = mp_version +USE_BIN_TYPE = True +RAW = True # should become False later when we do not need to read old stuff any more +UNICODE_ERRORS = 'surrogateescape' # previously done by safe_encode, safe_decode + class PackException(Exception): """Exception while msgpack packing""" @@ -40,10 +79,10 @@ class UnpackException(Exception): class Packer(mp_Packer): - def __init__(self, *, default=None, unicode_errors=None, - use_single_float=False, autoreset=True, use_bin_type=False, + def __init__(self, *, default=None, unicode_errors=UNICODE_ERRORS, + use_single_float=False, autoreset=True, use_bin_type=USE_BIN_TYPE, strict_types=False): - assert unicode_errors is None + assert unicode_errors == UNICODE_ERRORS super().__init__(default=default, unicode_errors=unicode_errors, use_single_float=use_single_float, autoreset=autoreset, use_bin_type=use_bin_type, strict_types=strict_types) @@ -55,16 +94,16 @@ class Packer(mp_Packer): raise PackException(e) -def packb(o, *, use_bin_type=False, unicode_errors=None, **kwargs): - assert unicode_errors is None +def packb(o, *, use_bin_type=USE_BIN_TYPE, unicode_errors=UNICODE_ERRORS, **kwargs): + assert unicode_errors == UNICODE_ERRORS try: return mp_packb(o, use_bin_type=use_bin_type, unicode_errors=unicode_errors, **kwargs) except Exception as e: raise PackException(e) -def pack(o, stream, *, use_bin_type=False, unicode_errors=None, **kwargs): - assert unicode_errors is None +def pack(o, stream, *, use_bin_type=USE_BIN_TYPE, unicode_errors=UNICODE_ERRORS, **kwargs): + assert unicode_errors == UNICODE_ERRORS try: return mp_pack(o, stream, use_bin_type=use_bin_type, unicode_errors=unicode_errors, **kwargs) except Exception as e: @@ -72,13 +111,13 @@ def pack(o, stream, *, use_bin_type=False, unicode_errors=None, **kwargs): class Unpacker(mp_Unpacker): - def __init__(self, file_like=None, *, read_size=0, use_list=True, raw=True, + def __init__(self, file_like=None, *, read_size=0, use_list=True, raw=RAW, object_hook=None, object_pairs_hook=None, list_hook=None, - unicode_errors=None, max_buffer_size=0, + unicode_errors=UNICODE_ERRORS, max_buffer_size=0, ext_hook=ExtType, strict_map_key=False): - assert raw is True - assert unicode_errors is None + assert raw == RAW + assert unicode_errors == UNICODE_ERRORS kw = dict(file_like=file_like, read_size=read_size, use_list=use_list, raw=raw, object_hook=object_hook, object_pairs_hook=object_pairs_hook, list_hook=list_hook, unicode_errors=unicode_errors, max_buffer_size=max_buffer_size, @@ -105,10 +144,11 @@ class Unpacker(mp_Unpacker): next = __next__ -def unpackb(packed, *, raw=True, unicode_errors=None, +def unpackb(packed, *, raw=RAW, unicode_errors=UNICODE_ERRORS, strict_map_key=False, **kwargs): - assert unicode_errors is None + assert raw == RAW + assert unicode_errors == UNICODE_ERRORS try: kw = dict(raw=raw, unicode_errors=unicode_errors, strict_map_key=strict_map_key) @@ -118,10 +158,11 @@ def unpackb(packed, *, raw=True, unicode_errors=None, raise UnpackException(e) -def unpack(stream, *, raw=True, unicode_errors=None, +def unpack(stream, *, raw=RAW, unicode_errors=UNICODE_ERRORS, strict_map_key=False, **kwargs): - assert unicode_errors is None + # assert raw == RAW + assert unicode_errors == UNICODE_ERRORS try: kw = dict(raw=raw, unicode_errors=unicode_errors, strict_map_key=strict_map_key) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 9ea76f2de..89f476c1a 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -2,7 +2,6 @@ import stat from collections import namedtuple from .constants import ITEM_KEYS, ARCHIVE_KEYS -from .helpers import safe_encode, safe_decode from .helpers import StableDict from .helpers import format_file_size from .helpers.msgpack import timestamp_to_int, int_to_timestamp @@ -16,6 +15,51 @@ cdef extern from "_item.c": API_VERSION = '1.2_01' +def fix_key(data, key): + """if k is a bytes-typed key, migrate key/value to a str-typed key in dict data""" + if isinstance(key, bytes): + value = data.pop(key) + key = key.decode() + data[key] = value + assert isinstance(key, str) + return key + + +def fix_str_value(data, key, errors='surrogateescape'): + """makes sure that data[key] is a str (decode if it is bytes)""" + assert isinstance(key, str) # fix_key must be called first + value = data[key] + if isinstance(value, bytes): + value = value.decode('utf-8', errors=errors) + data[key] = value + assert isinstance(value, str) + return value + + +def fix_list_of_str(t): + """make sure we have a list of str""" + assert isinstance(t, (tuple, list)) + l = [e.decode() if isinstance(e, bytes) else e for e in t] + assert all(isinstance(e, str) for e in l), repr(l) + return l + + +def fix_tuple_of_str(t): + """make sure we have a tuple of str""" + assert isinstance(t, (tuple, list)) + t = tuple(e.decode() if isinstance(e, bytes) else e for e in t) + assert all(isinstance(e, str) for e in t), repr(t) + return t + + +def fix_tuple_of_str_and_int(t): + """make sure we have a tuple of str""" + assert isinstance(t, (tuple, list)) + t = tuple(e.decode() if isinstance(e, bytes) else e for e in t) + assert all(isinstance(e, (str, int)) for e in t), repr(t) + return t + + class PropDict: """ Manage a dictionary via properties. @@ -155,10 +199,10 @@ class Item(PropDict): # properties statically defined, so that IDEs can know their names: - path = PropDict._make_property('path', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode) - source = PropDict._make_property('source', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode) - user = PropDict._make_property('user', (str, type(None)), 'surrogate-escaped str or None', encode=safe_encode, decode=safe_decode) - group = PropDict._make_property('group', (str, type(None)), 'surrogate-escaped str or None', encode=safe_encode, decode=safe_decode) + path = PropDict._make_property('path', str, 'surrogate-escaped str') + source = PropDict._make_property('source', str, 'surrogate-escaped str') + user = PropDict._make_property('user', (str, type(None)), 'surrogate-escaped str or None') + group = PropDict._make_property('group', (str, type(None)), 'surrogate-escaped str or None') acl_access = PropDict._make_property('acl_access', bytes) acl_default = PropDict._make_property('acl_default', bytes) @@ -290,6 +334,14 @@ class Item(PropDict): except AttributeError: return False + def update_internal(self, d): + # legacy support for migration (data from old msgpacks comes in as bytes always, but sometimes we want str) + for k, v in list(d.items()): + k = fix_key(d, k) + if k in ('path', 'source', 'user', 'group'): + v = fix_str_value(d, k) + self._dict[k] = v + class EncryptedKey(PropDict): """ @@ -309,7 +361,7 @@ class EncryptedKey(PropDict): __slots__ = ("_dict", ) # avoid setting attributes not supported by properties version = PropDict._make_property('version', int) - algorithm = PropDict._make_property('algorithm', str, encode=str.encode, decode=bytes.decode) + algorithm = PropDict._make_property('algorithm', str) iterations = PropDict._make_property('iterations', int) salt = PropDict._make_property('salt', bytes) hash = PropDict._make_property('hash', bytes) @@ -317,7 +369,17 @@ class EncryptedKey(PropDict): argon2_time_cost = PropDict._make_property('argon2_time_cost', int) argon2_memory_cost = PropDict._make_property('argon2_memory_cost', int) argon2_parallelism = PropDict._make_property('argon2_parallelism', int) - argon2_type = PropDict._make_property('argon2_type', str, encode=str.encode, decode=bytes.decode) + argon2_type = PropDict._make_property('argon2_type', str) + + def update_internal(self, d): + # legacy support for migration (data from old msgpacks comes in as bytes always, but sometimes we want str) + for k, v in list(d.items()): + k = fix_key(d, k) + if k == 'version': + assert isinstance(v, int) + if k in ('algorithm', 'argon2_type'): + v = fix_str_value(d, k) + self._dict[k] = v class Key(PropDict): @@ -344,17 +406,13 @@ class Key(PropDict): chunk_seed = PropDict._make_property('chunk_seed', int) tam_required = PropDict._make_property('tam_required', bool) - -def tuple_encode(t): - """encode a tuple that might contain str items""" - # we have str, but want to give bytes to msgpack.pack - return tuple(safe_encode(e) if isinstance(e, str) else e for e in t) - - -def tuple_decode(t): - """decode a tuple that might contain bytes items""" - # we get bytes objects from msgpack.unpack, but want str - return tuple(safe_decode(e) if isinstance(e, bytes) else e for e in t) + def update_internal(self, d): + # legacy support for migration (data from old msgpacks comes in as bytes always, but sometimes we want str) + for k, v in list(d.items()): + k = fix_key(d, k) + if k == 'version': + assert isinstance(v, int) + self._dict[k] = v class ArchiveItem(PropDict): @@ -374,15 +432,15 @@ class ArchiveItem(PropDict): __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) + name = PropDict._make_property('name', str, 'surrogate-escaped str') 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, 'chunker-params tuple', encode=tuple_encode, decode=tuple_decode) + hostname = PropDict._make_property('hostname', str, 'surrogate-escaped str') + username = PropDict._make_property('username', str, 'surrogate-escaped str') + time = PropDict._make_property('time', str) + time_end = PropDict._make_property('time_end', str) + comment = PropDict._make_property('comment', str, 'surrogate-escaped str') + chunker_params = PropDict._make_property('chunker_params', tuple) recreate_cmdline = PropDict._make_property('recreate_cmdline', list) # list of s-e-str # recreate_source_id, recreate_args, recreate_partial_chunks were used in 1.1.0b1 .. b2 recreate_source_id = PropDict._make_property('recreate_source_id', bytes) @@ -395,6 +453,22 @@ class ArchiveItem(PropDict): csize_parts = PropDict._make_property('csize_parts', int) nfiles_parts = PropDict._make_property('nfiles_parts', int) + def update_internal(self, d): + # legacy support for migration (data from old msgpacks comes in as bytes always, but sometimes we want str) + for k, v in list(d.items()): + k = fix_key(d, k) + if k == 'version': + assert isinstance(v, int) + if k in ('name', 'hostname', 'username', 'comment'): + v = fix_str_value(d, k) + if k in ('time', 'time_end'): + v = fix_str_value(d, k, 'replace') + if k == 'chunker_params': + v = fix_tuple_of_str_and_int(v) + if k in ('cmdline', 'recreate_cmdline'): + v = fix_list_of_str(v) + self._dict[k] = v + class ManifestItem(PropDict): """ @@ -413,10 +487,52 @@ class ManifestItem(PropDict): __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('timestamp', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode) + archives = PropDict._make_property('archives', dict, 'dict of str -> dict') # name -> dict + timestamp = PropDict._make_property('timestamp', str) config = PropDict._make_property('config', dict) - item_keys = PropDict._make_property('item_keys', tuple) + item_keys = PropDict._make_property('item_keys', tuple, 'tuple of str') + + def update_internal(self, d): + # legacy support for migration (data from old msgpacks comes in as bytes always, but sometimes we want str) + for k, v in list(d.items()): + k = fix_key(d, k) + if k == 'version': + assert isinstance(v, int) + if k == 'archives': + ad = v + assert isinstance(ad, dict) + for ak, av in list(ad.items()): + ak = fix_key(ad, ak) + assert isinstance(av, dict) + for ik, iv in list(av.items()): + ik = fix_key(av, ik) + assert set(av) == {'id', 'time'} + assert isinstance(av['id'], bytes) + fix_str_value(av, 'time') + if k == 'timestamp': + v = fix_str_value(d, k, 'replace') + if k == 'config': + cd = v + assert isinstance(cd, dict) + for ck, cv in list(cd.items()): + ck = fix_key(cd, ck) + if ck == 'tam_required': + assert isinstance(cv, bool) + if ck == 'feature_flags': + assert isinstance(cv, dict) + ops = {'read', 'check', 'write', 'delete'} + for op, specs in list(cv.items()): + op = fix_key(cv, op) + assert op in ops + for speck, specv in list(specs.items()): + speck = fix_key(specs, speck) + if speck == 'mandatory': + specs[speck] = fix_tuple_of_str(specv) + assert set(cv).issubset(ops) + if k == 'item_keys': + v = fix_tuple_of_str(v) + self._dict[k] = v + class ItemDiff: """ diff --git a/src/borg/remote.py b/src/borg/remote.py index 2870d71d9..8de302871 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -38,7 +38,8 @@ logger = create_logger(__name__) RPC_PROTOCOL_VERSION = 2 BORG_VERSION = parse_version(__version__) -MSGID, MSG, ARGS, RESULT = b'i', b'm', b'a', b'r' +MSGID, MSG, ARGS, RESULT = 'i', 'm', 'a', 'r' # pack +MSGIDB, MSGB, ARGSB, RESULTB = b'i', b'm', b'a', b'r' # unpack MAX_INFLIGHT = 100 @@ -216,9 +217,9 @@ class RepositoryServer: # pragma: no cover for unpacked in unpacker: if isinstance(unpacked, dict): dictFormat = True - msgid = unpacked[MSGID] - method = unpacked[MSG].decode() - args = decode_keys(unpacked[ARGS]) + msgid = unpacked[MSGIDB] + method = unpacked[MSGB].decode() + args = decode_keys(unpacked[ARGSB]) elif isinstance(unpacked, tuple) and len(unpacked) == 4: dictFormat = False # The first field 'type' was always 1 and has always been ignored @@ -256,21 +257,21 @@ class RepositoryServer: # pragma: no cover 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'exception_trace': ex_trace, - b'sysinfo': sysinfo()}) + 'exception_class': e.__class__.__name__, + 'exception_args': e.args, + 'exception_full': ex_full, + 'exception_short': ex_short, + 'exception_trace': ex_trace, + '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'exception_trace': ex_trace, - b'sysinfo': sysinfo()}) + 'exception_class': e.__class__.__name__, + 'exception_args': [x if isinstance(x, (str, bytes, int)) else None + for x in e.args], + 'exception_full': ex_full, + 'exception_short': ex_short, + 'exception_trace': ex_trace, + 'sysinfo': sysinfo()}) os_write(stdout_fd, msg) else: @@ -570,7 +571,7 @@ class RemoteRepository: try: try: version = self.call('negotiate', {'client_data': { - b'client_version': BORG_VERSION, + 'client_version': BORG_VERSION, }}) except ConnectionClosed: raise ConnectionClosedWithHint('Is borg working on the server?') from None @@ -791,7 +792,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. if b'exception_class' in unpacked: handle_error(unpacked) else: - yield unpacked[RESULT] + yield unpacked[RESULTB] if not waiting_for and not calls: return except KeyError: @@ -811,7 +812,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. if b'exception_class' in unpacked: handle_error(unpacked) else: - yield unpacked[RESULT] + yield unpacked[RESULTB] if self.to_send or ((calls or self.preload_ids) and len(waiting_for) < MAX_INFLIGHT): w_fds = [self.stdin_fd] else: @@ -828,15 +829,15 @@ 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 isinstance(unpacked, dict): - msgid = unpacked[MSGID] + msgid = unpacked[MSGIDB] elif isinstance(unpacked, tuple) and len(unpacked) == 4: # 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} + unpacked = {MSGIDB: msgid, b'exception_class': error} else: - unpacked = {MSGID: msgid, RESULT: res} + unpacked = {MSGIDB: msgid, RESULTB: res} else: raise UnexpectedRPCDataFormatFromServer(data) if msgid in self.ignore_responses: @@ -847,7 +848,7 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. 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: + if unpacked[RESULTB] is not None: self.async_responses[msgid] = unpacked else: self.responses[msgid] = unpacked diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index 3f8535ff5..0eed9f7e8 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -186,8 +186,8 @@ class RobustUnpackerTestCase(BaseTestCase): return result def test_extra_garbage_no_sync(self): - chunks = [(False, [self.make_chunks([b'foo', b'bar'])]), - (False, [b'garbage'] + [self.make_chunks([b'boo', b'baz'])])] + chunks = [(False, [self.make_chunks(['foo', 'bar'])]), + (False, [b'garbage'] + [self.make_chunks(['boo', 'baz'])])] result = self.process(chunks) self.assert_equal(result, [ {b'path': b'foo'}, {b'path': b'bar'}, @@ -203,19 +203,19 @@ class RobustUnpackerTestCase(BaseTestCase): return parts def test_correct_stream(self): - chunks = self.split(self.make_chunks([b'foo', b'bar', b'boo', b'baz']), 2) + chunks = self.split(self.make_chunks(['foo', 'bar', 'boo', 'baz']), 2) input = [(False, chunks)] result = self.process(input) self.assert_equal(result, [{b'path': b'foo'}, {b'path': b'bar'}, {b'path': b'boo'}, {b'path': b'baz'}]) def test_missing_chunk(self): - chunks = self.split(self.make_chunks([b'foo', b'bar', b'boo', b'baz']), 4) + chunks = self.split(self.make_chunks(['foo', 'bar', 'boo', 'baz']), 4) input = [(False, chunks[:3]), (True, chunks[4:])] result = self.process(input) self.assert_equal(result, [{b'path': b'foo'}, {b'path': b'boo'}, {b'path': b'baz'}]) def test_corrupt_chunk(self): - chunks = self.split(self.make_chunks([b'foo', b'bar', b'boo', b'baz']), 4) + chunks = self.split(self.make_chunks(['foo', 'bar', 'boo', 'baz']), 4) input = [(False, chunks[:3]), (True, [b'gar', b'bage'] + chunks[3:])] result = self.process(input) self.assert_equal(result, [{b'path': b'foo'}, {b'path': b'boo'}, {b'path': b'baz'}]) @@ -242,7 +242,7 @@ 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!) + {'path': b'/a/b/c'}, # small (different msgpack mapping type!) 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 ]]) @@ -251,7 +251,7 @@ def test_valid_msgpacked_items(packed, item_keys_serialized): def test_key_length_msgpacked_items(): - key = b'x' * 32 # 31 bytes is the limit for fixstr msgpack type + key = 'x' * 32 # 31 bytes is the limit for fixstr msgpack type data = {key: b''} item_keys_serialized = [msgpack.packb(key), ] assert valid_msgpacked_dict(msgpack.packb(data), item_keys_serialized) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 32feeb44e..f3315b676 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1810,7 +1810,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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.config['feature_flags'] = {operation.value: {'mandatory': ['unknown-feature']}} manifest.write() repository.commit(compact=False) @@ -3640,13 +3640,13 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 with Repository(self.repository_path) as repository: key = msgpack.unpackb(a2b_base64(repository.load_key())) - assert key[b'algorithm'] == expected_algorithm + assert key[b'algorithm'] == expected_algorithm.encode() def test_change_passphrase_does_not_change_algorithm_argon2(self): - self.verify_change_passphrase_does_not_change_algorithm('argon2', b'argon2 chacha20-poly1305') + self.verify_change_passphrase_does_not_change_algorithm('argon2', 'argon2 chacha20-poly1305') def test_change_passphrase_does_not_change_algorithm_pbkdf2(self): - self.verify_change_passphrase_does_not_change_algorithm('pbkdf2', b'sha256') + self.verify_change_passphrase_does_not_change_algorithm('pbkdf2', 'sha256') def verify_change_location_does_not_change_algorithm(self, given_algorithm, expected_algorithm): self.cmd('init', '--encryption=keyfile', '--key-algorithm', given_algorithm, self.repository_location) @@ -3655,13 +3655,13 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 with Repository(self.repository_path) as repository: key = msgpack.unpackb(a2b_base64(repository.load_key())) - assert key[b'algorithm'] == expected_algorithm + assert key[b'algorithm'] == expected_algorithm.encode() def test_change_location_does_not_change_algorithm_argon2(self): - self.verify_change_location_does_not_change_algorithm('argon2', b'argon2 chacha20-poly1305') + self.verify_change_location_does_not_change_algorithm('argon2', 'argon2 chacha20-poly1305') def test_change_location_does_not_change_algorithm_pbkdf2(self): - self.verify_change_location_does_not_change_algorithm('pbkdf2', b'sha256') + self.verify_change_location_does_not_change_algorithm('pbkdf2', 'sha256') def test_key_change_algorithm(self): self.cmd('init', '--encryption=repokey', '--key-algorithm=pbkdf2', self.repository_location) diff --git a/src/borg/testsuite/item.py b/src/borg/testsuite/item.py index 80b38edce..94167e7ea 100644 --- a/src/borg/testsuite/item.py +++ b/src/borg/testsuite/item.py @@ -102,7 +102,7 @@ def test_item_se_str_property(): item = Item() item.path = '/a/b/c' assert item.path == '/a/b/c' - assert item.as_dict() == {'path': b'/a/b/c'} + assert item.as_dict() == {'path': '/a/b/c'} del item.path assert item.as_dict() == {} with pytest.raises(TypeError): @@ -111,11 +111,11 @@ def test_item_se_str_property(): # non-utf-8 path, needing surrogate-escaping for latin-1 u-umlaut item = Item(internal_dict={'path': b'/a/\xfc/c'}) assert item.path == '/a/\udcfc/c' # getting a surrogate-escaped representation - assert item.as_dict() == {'path': b'/a/\xfc/c'} + assert item.as_dict() == {'path': '/a/\udcfc/c'} del item.path assert 'path' not in item item.path = '/a/\udcfc/c' # setting using a surrogate-escaped representation - assert item.as_dict() == {'path': b'/a/\xfc/c'} + assert item.as_dict() == {'path': '/a/\udcfc/c'} def test_item_list_property(): From 8e87f1111b02f35c2ac14c0605b60ccd8db486b8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 6 May 2022 03:59:10 +0200 Subject: [PATCH 016/160] cleanup msgpack related str/bytes mess, fixes #968 see ticket and borg.helpers.msgpack docstring. this changeset implements the full migration to msgpack 2.0 spec (use_bin_type=True, raw=False). still needed compat to the past is done via want_bytes decoder in borg.item. --- src/borg/archive.py | 7 +-- src/borg/archiver.py | 2 +- src/borg/crypto/key.py | 10 ++--- src/borg/helpers/msgpack.py | 34 +++++++------- src/borg/item.pyx | 35 +++++++++------ src/borg/remote.py | 77 +++++++++++++++----------------- src/borg/repository.py | 44 +++++++++--------- src/borg/testsuite/archive.py | 14 +++--- src/borg/testsuite/archiver.py | 10 ++--- src/borg/testsuite/key.py | 16 +++---- src/borg/testsuite/repository.py | 4 +- 11 files changed, 124 insertions(+), 129 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index dc244a9f1..ff6ca7ce6 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1718,13 +1718,10 @@ class ArchiveChecker: Iterates through all objects in the repository looking for archive metadata blocks. """ - required_archive_keys = frozenset(key.encode() for key in REQUIRED_ARCHIVE_KEYS) - def valid_archive(obj): if not isinstance(obj, dict): return False - keys = set(obj) - return required_archive_keys.issubset(keys) + return REQUIRED_ARCHIVE_KEYS.issubset(obj) logger.info('Rebuilding missing manifest, this might take some time...') # as we have lost the manifest, we do not know any more what valid item keys we had. @@ -1904,7 +1901,7 @@ class ArchiveChecker: def valid_item(obj): if not isinstance(obj, StableDict): return False, 'not a dictionary' - keys = set(k.decode('utf-8', errors='replace') for k in obj) + keys = set(obj) 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): diff --git a/src/borg/archiver.py b/src/borg/archiver.py index acef0db5e..a91e43c54 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2331,7 +2331,7 @@ class Archiver: unpacker = msgpack.Unpacker(use_list=False, object_hook=StableDict) first = True - for item_id in archive_org_dict[b'items']: + for item_id in archive_org_dict['items']: data = key.decrypt(item_id, repository.get(item_id)) unpacker.feed(data) for item in unpacker: diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 6ca6fbac0..15df53d00 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -232,24 +232,24 @@ class KeyBase: unpacker = get_limited_unpacker('manifest') unpacker.feed(data) unpacked = unpacker.unpack() - if b'tam' not in unpacked: + if '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) + tam = unpacked.pop('tam', None) if not isinstance(tam, dict): raise TAMInvalid() - tam_type = tam.get(b'type', b'').decode('ascii', 'replace') + tam_type = tam.get('type', '') 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') + tam_hmac = tam.get('hmac') + tam_salt = tam.get('salt') if not isinstance(tam_salt, bytes) or not isinstance(tam_hmac, bytes): raise TAMInvalid() offset = data.index(tam_hmac) diff --git a/src/borg/helpers/msgpack.py b/src/borg/helpers/msgpack.py index 5a2edecd6..268ee30e7 100644 --- a/src/borg/helpers/msgpack.py +++ b/src/borg/helpers/msgpack.py @@ -2,8 +2,7 @@ wrapping msgpack ================ -Due to the planned breaking api changes in upstream msgpack, we wrap it the way we need it - -to avoid having lots of clutter in the calling code. see tickets #968 and #3632. +We wrap msgpack here the way we need it - to avoid having lots of clutter in the calling code. Packing ------- @@ -22,30 +21,27 @@ Packing Unpacking --------- -- raw = True (the old way, used by borg <= 1.3) - This is currently still needed to not try to decode "raw" msgpack objects. - These could come either from str (new or old msgpack) or bytes (old msgpack). - Thus, we basically must know what we want and either keep the bytes we get - or decode them to str, if we want str. - -- raw = False (the new way) - This can be used in future, when we do not have to deal with data any more that was packed the old way. +- raw = False (used by borg since borg 1.3) + We already can use this with borg 1.3 due to the want_bytes decoder. + This decoder can be removed in future, when we do not have to deal with data any more that was packed the old way. It will then unpack according to the msgpack 2.0 spec format and directly output bytes or str. +- raw = True (the old way, used by borg < 1.3) + - unicode_errors = 'surrogateescape' -> see description above (will be used when raw is False). -As of borg 1.3, we have the first part on the way to fix the msgpack str/bytes mess, #968. -borg now still needs to **read** old repos, archives, keys, ... so we can not yet fix it completely. -But from now on, borg only **writes** new data according to the new msgpack spec, -thus we can complete the fix for #968 in a later borg release. +As of borg 1.3, we have fixed most of the msgpack str/bytes mess, #968. +Borg now still needs to **read** old repos, archives, keys, ... so we can not yet fix it completely. +But from now on, borg only **writes** new data according to the new msgpack 2.0 spec, +thus we can remove some legacy support in a later borg release (some places are marked with "legacy"). current way in msgpack terms ---------------------------- - pack with use_bin_type=True (according to msgpack 2.0 spec) - packs str -> raw and bytes -> bin -- unpack with raw=True (aka "the old way") -- unpacks raw to bytes (thus we always need to decode manually if we want str) +- unpack with raw=False (according to msgpack 2.0 spec, using unicode_errors='surrogateescape') +- unpacks bin to bytes and raw to str (thus we need to re-encode manually if we want bytes from "raw") """ from .datastruct import StableDict @@ -66,8 +62,8 @@ from msgpack import OutOfData version = mp_version USE_BIN_TYPE = True -RAW = True # should become False later when we do not need to read old stuff any more -UNICODE_ERRORS = 'surrogateescape' # previously done by safe_encode, safe_decode +RAW = False +UNICODE_ERRORS = 'surrogateescape' class PackException(Exception): @@ -161,7 +157,7 @@ def unpackb(packed, *, raw=RAW, unicode_errors=UNICODE_ERRORS, def unpack(stream, *, raw=RAW, unicode_errors=UNICODE_ERRORS, strict_map_key=False, **kwargs): - # assert raw == RAW + assert raw == RAW assert unicode_errors == UNICODE_ERRORS try: kw = dict(raw=raw, unicode_errors=unicode_errors, diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 89f476c1a..4a6c81163 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -60,6 +60,15 @@ def fix_tuple_of_str_and_int(t): return t +def want_bytes(v): + """we know that we want bytes and the value should be bytes""" + # legacy support: it being str can be caused by msgpack unpack decoding old data that was packed with use_bin_type=False + if isinstance(v, str): + v = v.encode('utf-8', errors='surrogateescape') + assert isinstance(v, bytes) + return v + + class PropDict: """ Manage a dictionary via properties. @@ -204,10 +213,10 @@ class Item(PropDict): user = PropDict._make_property('user', (str, type(None)), 'surrogate-escaped str or None') group = PropDict._make_property('group', (str, type(None)), 'surrogate-escaped str or None') - acl_access = PropDict._make_property('acl_access', bytes) - acl_default = PropDict._make_property('acl_default', bytes) - acl_extended = PropDict._make_property('acl_extended', bytes) - acl_nfs4 = PropDict._make_property('acl_nfs4', bytes) + acl_access = PropDict._make_property('acl_access', bytes, decode=want_bytes) + acl_default = PropDict._make_property('acl_default', bytes, decode=want_bytes) + acl_extended = PropDict._make_property('acl_extended', bytes, decode=want_bytes) + acl_nfs4 = PropDict._make_property('acl_nfs4', bytes, decode=want_bytes) mode = PropDict._make_property('mode', int) uid = PropDict._make_property('uid', int) @@ -224,7 +233,7 @@ class Item(PropDict): # compatibility note: this is a new feature, in old archives size will be missing. size = PropDict._make_property('size', int) - hlid = PropDict._make_property('hlid', bytes) # hard link id: same value means same hard link. + hlid = PropDict._make_property('hlid', bytes, decode=want_bytes) # hard link id: same value means same hard link. hardlink_master = PropDict._make_property('hardlink_master', bool) # legacy chunks = PropDict._make_property('chunks', (list, type(None)), 'list or None') @@ -363,9 +372,9 @@ class EncryptedKey(PropDict): version = PropDict._make_property('version', int) algorithm = PropDict._make_property('algorithm', str) iterations = PropDict._make_property('iterations', int) - salt = PropDict._make_property('salt', bytes) - hash = PropDict._make_property('hash', bytes) - data = PropDict._make_property('data', bytes) + salt = PropDict._make_property('salt', bytes, decode=want_bytes) + hash = PropDict._make_property('hash', bytes, decode=want_bytes) + data = PropDict._make_property('data', bytes, decode=want_bytes) argon2_time_cost = PropDict._make_property('argon2_time_cost', int) argon2_memory_cost = PropDict._make_property('argon2_memory_cost', int) argon2_parallelism = PropDict._make_property('argon2_parallelism', int) @@ -399,10 +408,10 @@ class Key(PropDict): __slots__ = ("_dict", ) # avoid setting attributes not supported by properties version = PropDict._make_property('version', int) - repository_id = PropDict._make_property('repository_id', bytes) - enc_key = PropDict._make_property('enc_key', bytes) - enc_hmac_key = PropDict._make_property('enc_hmac_key', bytes) - id_key = PropDict._make_property('id_key', bytes) + repository_id = PropDict._make_property('repository_id', bytes, decode=want_bytes) + enc_key = PropDict._make_property('enc_key', bytes, decode=want_bytes) + enc_hmac_key = PropDict._make_property('enc_hmac_key', bytes, decode=want_bytes) + id_key = PropDict._make_property('id_key', bytes, decode=want_bytes) chunk_seed = PropDict._make_property('chunk_seed', int) tam_required = PropDict._make_property('tam_required', bool) @@ -443,7 +452,7 @@ class ArchiveItem(PropDict): chunker_params = PropDict._make_property('chunker_params', tuple) recreate_cmdline = PropDict._make_property('recreate_cmdline', list) # list of s-e-str # recreate_source_id, recreate_args, recreate_partial_chunks were used in 1.1.0b1 .. b2 - recreate_source_id = PropDict._make_property('recreate_source_id', bytes) + recreate_source_id = PropDict._make_property('recreate_source_id', bytes, decode=want_bytes) 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 size = PropDict._make_property('size', int) diff --git a/src/borg/remote.py b/src/borg/remote.py index 8de302871..6ea51d3c3 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -38,8 +38,7 @@ logger = create_logger(__name__) RPC_PROTOCOL_VERSION = 2 BORG_VERSION = parse_version(__version__) -MSGID, MSG, ARGS, RESULT = 'i', 'm', 'a', 'r' # pack -MSGIDB, MSGB, ARGSB, RESULTB = b'i', b'm', b'a', b'r' # unpack +MSGID, MSG, ARGS, RESULT = 'i', 'm', 'a', 'r' MAX_INFLIGHT = 100 @@ -139,10 +138,6 @@ compatMap = { } -def decode_keys(d): - return {k.decode(): d[k] for k in d} - - class RepositoryServer: # pragma: no cover rpc_methods = ( '__len__', @@ -217,14 +212,13 @@ class RepositoryServer: # pragma: no cover for unpacked in unpacker: if isinstance(unpacked, dict): dictFormat = True - msgid = unpacked[MSGIDB] - method = unpacked[MSGB].decode() - args = decode_keys(unpacked[ARGSB]) + msgid = unpacked[MSGID] + method = unpacked[MSG] + args = unpacked[ARGS] elif isinstance(unpacked, tuple) and len(unpacked) == 4: dictFormat = False # 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: if self.repository is not None: @@ -308,7 +302,7 @@ class RepositoryServer: # pragma: no cover # clients since 1.1.0b3 use a dict as client_data # clients since 1.1.0b6 support json log format from server if isinstance(client_data, dict): - self.client_version = client_data[b'client_version'] + self.client_version = client_data['client_version'] level = logging.getLevelName(logging.getLogger('').level) setup_logging(is_serve=True, json=True, level=level) logger.debug('Initialized logging system for JSON-based protocol') @@ -370,7 +364,6 @@ class RepositoryServer: # pragma: no cover return self.repository.id def inject_exception(self, kind): - kind = kind.decode() s1 = 'test string' s2 = 'test string2' if kind == 'DoesNotExist': @@ -484,35 +477,35 @@ class RemoteRepository: class RPCError(Exception): 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' + # for borg < 1.1: unpacked only has 'exception_class' as key + # for borg 1.1+: unpacked has keys: 'exception_args', 'exception_full', 'exception_short', '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() + if 'exception_short' in self.unpacked: + return '\n'.join(self.unpacked['exception_short']) else: return self.exception_class @property def traceback(self): - return self.unpacked.get(b'exception_trace', True) + return self.unpacked.get('exception_trace', True) @property def exception_class(self): - return self.unpacked[b'exception_class'].decode() + return self.unpacked['exception_class'] @property def exception_full(self): - if b'exception_full' in self.unpacked: - return b'\n'.join(self.unpacked[b'exception_full']).decode() + if 'exception_full' in self.unpacked: + return '\n'.join(self.unpacked['exception_full']) 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() + if 'sysinfo' in self.unpacked: + return self.unpacked['sysinfo'] else: return '' @@ -577,9 +570,9 @@ class RemoteRepository: raise ConnectionClosedWithHint('Is borg working on the server?') from None if version == RPC_PROTOCOL_VERSION: self.dictFormat = False - elif isinstance(version, dict) and b'server_version' in version: + elif isinstance(version, dict) and 'server_version' in version: self.dictFormat = True - self.server_version = version[b'server_version'] + self.server_version = version['server_version'] else: raise Exception('Server insisted on using unsupported protocol version %s' % version) @@ -734,9 +727,9 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. return msgid def handle_error(unpacked): - error = unpacked[b'exception_class'].decode() - old_server = b'exception_args' not in unpacked - args = unpacked.get(b'exception_args') + error = unpacked['exception_class'] + old_server = 'exception_args' not in unpacked + args = unpacked.get('exception_args') if error == 'DoesNotExist': raise Repository.DoesNotExist(self.location.processed) @@ -748,29 +741,29 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. if old_server: raise IntegrityError('(not available)') else: - raise IntegrityError(args[0].decode()) + raise IntegrityError(args[0]) elif error == 'AtticRepository': if old_server: raise Repository.AtticRepository('(not available)') else: - raise Repository.AtticRepository(args[0].decode()) + raise Repository.AtticRepository(args[0]) elif error == 'PathNotAllowed': if old_server: raise PathNotAllowed('(unknown)') else: - raise PathNotAllowed(args[0].decode()) + raise PathNotAllowed(args[0]) elif error == 'ParentPathDoesNotExist': - raise Repository.ParentPathDoesNotExist(args[0].decode()) + raise Repository.ParentPathDoesNotExist(args[0]) elif error == 'ObjectNotFound': if old_server: raise Repository.ObjectNotFound('(not available)', self.location.processed) else: - raise Repository.ObjectNotFound(args[0].decode(), self.location.processed) + raise Repository.ObjectNotFound(args[0], self.location.processed) elif error == 'InvalidRPCMethod': if old_server: raise InvalidRPCMethod('(not available)') else: - raise InvalidRPCMethod(args[0].decode()) + raise InvalidRPCMethod(args[0]) else: raise self.RPCError(unpacked) @@ -789,10 +782,10 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. try: unpacked = self.responses.pop(waiting_for[0]) waiting_for.pop(0) - if b'exception_class' in unpacked: + if 'exception_class' in unpacked: handle_error(unpacked) else: - yield unpacked[RESULTB] + yield unpacked[RESULT] if not waiting_for and not calls: return except KeyError: @@ -809,10 +802,10 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. else: return else: - if b'exception_class' in unpacked: + if 'exception_class' in unpacked: handle_error(unpacked) else: - yield unpacked[RESULTB] + 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: @@ -829,26 +822,26 @@ 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 isinstance(unpacked, dict): - msgid = unpacked[MSGIDB] + msgid = unpacked[MSGID] elif isinstance(unpacked, tuple) and len(unpacked) == 4: # 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 = {MSGIDB: msgid, b'exception_class': error} + unpacked = {MSGID: msgid, 'exception_class': error} else: - unpacked = {MSGIDB: msgid, RESULTB: res} + unpacked = {MSGID: msgid, RESULT: res} else: 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: + if 'exception_class' in 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[RESULTB] is not None: + if unpacked[RESULT] is not None: self.async_responses[msgid] = unpacked else: self.responses[msgid] = unpacked diff --git a/src/borg/repository.py b/src/borg/repository.py index 9267fe0e6..3fcc72aad 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -516,16 +516,16 @@ class Repository: integrity = msgpack.unpack(fd) except FileNotFoundError: return - if integrity.get(b'version') != 2: - logger.warning('Unknown integrity data version %r in %s', integrity.get(b'version'), integrity_file) + if integrity.get('version') != 2: + logger.warning('Unknown integrity data version %r in %s', integrity.get('version'), integrity_file) return - return integrity[key].decode() + return integrity[key] 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) - integrity_data = self._read_integrity(transaction_id, b'index') + integrity_data = self._read_integrity(transaction_id, 'index') try: with IntegrityCheckedFile(index_path, write=False, integrity_data=integrity_data) as fd: return NSIndex.read(fd) @@ -575,7 +575,7 @@ 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') + integrity_data = self._read_integrity(transaction_id, 'hints') try: with IntegrityCheckedFile(hints_path, write=False, integrity_data=integrity_data) as fd: hints = msgpack.unpack(fd) @@ -588,23 +588,23 @@ class Repository: self.check_transaction() self.prepare_txn(transaction_id) return - if hints[b'version'] == 1: + if hints['version'] == 1: logger.debug('Upgrading from v1 hints.%d', transaction_id) - self.segments = hints[b'segments'] + self.segments = hints['segments'] self.compact = FreeSpace() self.storage_quota_use = 0 self.shadow_index = {} - for segment in sorted(hints[b'compact']): + for segment in sorted(hints['compact']): logger.debug('Rebuilding sparse info for segment %d', segment) self._rebuild_sparse(segment) logger.debug('Upgrade to v2 hints complete') - elif hints[b'version'] != 2: - raise ValueError('Unknown hints file version: %d' % hints[b'version']) + elif hints['version'] != 2: + raise ValueError('Unknown hints file version: %d' % hints['version']) else: - self.segments = hints[b'segments'] - self.compact = FreeSpace(hints[b'compact']) - self.storage_quota_use = hints.get(b'storage_quota_use', 0) - self.shadow_index = hints.get(b'shadow_index', {}) + self.segments = hints['segments'] + self.compact = FreeSpace(hints['compact']) + self.storage_quota_use = hints.get('storage_quota_use', 0) + self.shadow_index = hints.get('shadow_index', {}) self.log_storage_quota() # Drop uncommitted segments in the shadow index for key, shadowed_segments in self.shadow_index.items(): @@ -621,16 +621,16 @@ class Repository: 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, - b'shadow_index': self.shadow_index, + 'version': 2, + 'segments': self.segments, + 'compact': self.compact, + 'storage_quota_use': self.storage_quota_use, + 'shadow_index': self.shadow_index, } integrity = { # Integrity version started at 2, the current hints version. # Thus, integrity version == hints version, for now. - b'version': 2, + 'version': 2, } transaction_id = self.io.get_segments_transaction_id() assert transaction_id is not None @@ -647,7 +647,7 @@ class Repository: 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 + integrity['hints'] = fd.integrity_data # Write repository index index_name = 'index.%d' % transaction_id @@ -656,7 +656,7 @@ class Repository: # XXX: Consider using SyncFile for index write-outs. self.index.write(fd) flush_and_sync(fd) - integrity[b'index'] = fd.integrity_data + integrity['index'] = fd.integrity_data # Write integrity file, containing checksums of the hints and index files integrity_name = 'integrity.%d' % transaction_id diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index 0eed9f7e8..9cdcf5046 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -171,7 +171,7 @@ class RobustUnpackerTestCase(BaseTestCase): return b''.join(msgpack.packb({'path': item}) for item in items) def _validator(self, value): - return isinstance(value, dict) and value.get(b'path') in (b'foo', b'bar', b'boo', b'baz') + return isinstance(value, dict) and value.get('path') in ('foo', 'bar', 'boo', 'baz') def process(self, input): unpacker = RobustUnpacker(validator=self._validator, item_keys=ITEM_KEYS) @@ -190,10 +190,10 @@ class RobustUnpackerTestCase(BaseTestCase): (False, [b'garbage'] + [self.make_chunks(['boo', 'baz'])])] result = self.process(chunks) self.assert_equal(result, [ - {b'path': b'foo'}, {b'path': b'bar'}, + {'path': 'foo'}, {'path': 'bar'}, 103, 97, 114, 98, 97, 103, 101, - {b'path': b'boo'}, - {b'path': b'baz'}]) + {'path': 'boo'}, + {'path': 'baz'}]) def split(self, left, length): parts = [] @@ -206,19 +206,19 @@ class RobustUnpackerTestCase(BaseTestCase): chunks = self.split(self.make_chunks(['foo', 'bar', 'boo', 'baz']), 2) input = [(False, chunks)] result = self.process(input) - self.assert_equal(result, [{b'path': b'foo'}, {b'path': b'bar'}, {b'path': b'boo'}, {b'path': b'baz'}]) + self.assert_equal(result, [{'path': 'foo'}, {'path': 'bar'}, {'path': 'boo'}, {'path': 'baz'}]) def test_missing_chunk(self): chunks = self.split(self.make_chunks(['foo', 'bar', 'boo', 'baz']), 4) input = [(False, chunks[:3]), (True, chunks[4:])] result = self.process(input) - self.assert_equal(result, [{b'path': b'foo'}, {b'path': b'boo'}, {b'path': b'baz'}]) + self.assert_equal(result, [{'path': 'foo'}, {'path': 'boo'}, {'path': 'baz'}]) def test_corrupt_chunk(self): chunks = self.split(self.make_chunks(['foo', 'bar', 'boo', 'baz']), 4) input = [(False, chunks[:3]), (True, [b'gar', b'bage'] + chunks[3:])] result = self.process(input) - self.assert_equal(result, [{b'path': b'foo'}, {b'path': b'boo'}, {b'path': b'baz'}]) + self.assert_equal(result, [{'path': 'foo'}, {'path': 'boo'}, {'path': 'baz'}]) @pytest.fixture diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index f3315b676..a4205ea76 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -3623,14 +3623,14 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.cmd('init', '--encryption=repokey', self.repository_location) with Repository(self.repository_path) as repository: key = msgpack.unpackb(a2b_base64(repository.load_key())) - assert key[b'algorithm'] == b'argon2 chacha20-poly1305' + assert key['algorithm'] == 'argon2 chacha20-poly1305' def test_init_with_explicit_key_algorithm(self): """https://github.com/borgbackup/borg/issues/747#issuecomment-1076160401""" self.cmd('init', '--encryption=repokey', '--key-algorithm=pbkdf2', self.repository_location) with Repository(self.repository_path) as repository: key = msgpack.unpackb(a2b_base64(repository.load_key())) - assert key[b'algorithm'] == b'sha256' + assert key['algorithm'] == 'sha256' def verify_change_passphrase_does_not_change_algorithm(self, given_algorithm, expected_algorithm): self.cmd('init', '--encryption=repokey', '--key-algorithm', given_algorithm, self.repository_location) @@ -3640,7 +3640,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 with Repository(self.repository_path) as repository: key = msgpack.unpackb(a2b_base64(repository.load_key())) - assert key[b'algorithm'] == expected_algorithm.encode() + assert key['algorithm'] == expected_algorithm def test_change_passphrase_does_not_change_algorithm_argon2(self): self.verify_change_passphrase_does_not_change_algorithm('argon2', 'argon2 chacha20-poly1305') @@ -3655,7 +3655,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 with Repository(self.repository_path) as repository: key = msgpack.unpackb(a2b_base64(repository.load_key())) - assert key[b'algorithm'] == expected_algorithm.encode() + assert key['algorithm'] == expected_algorithm def test_change_location_does_not_change_algorithm_argon2(self): self.verify_change_location_does_not_change_algorithm('argon2', 'argon2 chacha20-poly1305') @@ -3969,7 +3969,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): key.change_passphrase(key._passphrase) manifest = msgpack.unpackb(key.decrypt(Manifest.MANIFEST_ID, repository.get(Manifest.MANIFEST_ID))) - del manifest[b'tam'] + del manifest['tam'] repository.put(Manifest.MANIFEST_ID, key.encrypt(Manifest.MANIFEST_ID, msgpack.packb(manifest))) repository.commit(compact=False) output = self.cmd('list', '--debug', self.repository_location) diff --git a/src/borg/testsuite/key.py b/src/borg/testsuite/key.py index 5073c5b23..02eaa86e5 100644 --- a/src/borg/testsuite/key.py +++ b/src/borg/testsuite/key.py @@ -360,23 +360,23 @@ class TestTAM: assert blob.startswith(b'\x82') unpacked = msgpack.unpackb(blob) - assert unpacked[b'tam'][b'type'] == b'HKDF_HMAC_SHA512' + assert unpacked['tam']['type'] == '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 + assert unpacked['foo'] == 'bar' + assert 'tam' not in unpacked - @pytest.mark.parametrize('which', (b'hmac', b'salt')) + @pytest.mark.parametrize('which', ('hmac', '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 + assert len(unpacked['tam'][which]) == 64 + unpacked['tam'][which] = unpacked['tam'][which][0:32] + bytes(32) + assert len(unpacked['tam'][which]) == 64 blob = msgpack.packb(unpacked) with pytest.raises(TAMInvalid): @@ -421,4 +421,4 @@ def test_key_file_roundtrip(monkeypatch, cli_argument, expected_algorithm): load_me = RepoKey.detect(repository, manifest_data=None) assert to_dict(load_me) == to_dict(save_me) - assert msgpack.unpackb(a2b_base64(saved))[b'algorithm'] == expected_algorithm.encode() + assert msgpack.unpackb(a2b_base64(saved))['algorithm'] == expected_algorithm diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index b4944e58a..52f03e668 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -655,8 +655,8 @@ class RepositoryAuxiliaryCorruptionTestCase(RepositoryTestCaseBase): hints = msgpack.unpack(fd) fd.seek(0) # Corrupt segment refcount - assert hints[b'segments'][2] == 1 - hints[b'segments'][2] = 0 + assert hints['segments'][2] == 1 + hints['segments'][2] = 0 msgpack.pack(hints, fd) fd.truncate() From 33444be9268d70f163b16f1bf8c7a5bf0f1fb353 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 19 May 2022 23:12:21 +0200 Subject: [PATCH 017/160] more str vs bytes fixing --- src/borg/crypto/key.py | 6 ++- src/borg/helpers/fs.py | 4 +- src/borg/item.pyx | 88 ++++++++++++++++++++++++++++++------------ 3 files changed, 70 insertions(+), 28 deletions(-) diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 15df53d00..2b7b50da8 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -22,7 +22,7 @@ from ..helpers import bin_to_hex from ..helpers.passphrase import Passphrase, PasswordRetriesExceeded, PassphraseWrong from ..helpers import msgpack from ..helpers.manifest import Manifest -from ..item import Key, EncryptedKey +from ..item import Key, EncryptedKey, want_bytes from ..platform import SaveFile from .nonces import NonceManager @@ -250,8 +250,10 @@ class KeyBase: return unpacked, False tam_hmac = tam.get('hmac') tam_salt = tam.get('salt') - if not isinstance(tam_salt, bytes) or not isinstance(tam_hmac, bytes): + if not isinstance(tam_salt, (bytes, str)) or not isinstance(tam_hmac, (bytes, str)): raise TAMInvalid() + tam_hmac = want_bytes(tam_hmac) # legacy + tam_salt = want_bytes(tam_salt) # legacy offset = data.index(tam_hmac) data[offset:offset + 64] = bytes(64) tam_key = self._tam_key(tam_salt, context=b'manifest') diff --git a/src/borg/helpers/fs.py b/src/borg/helpers/fs.py index fecda9c69..5509e0d0f 100644 --- a/src/borg/helpers/fs.py +++ b/src/borg/helpers/fs.py @@ -205,8 +205,8 @@ class HardLinkManager: def hardlink_id_from_path(self, path): """compute a hardlink id from a path""" - assert isinstance(path, bytes) - return hashlib.sha256(path).digest() + assert isinstance(path, str) + return hashlib.sha256(path.encode('utf-8', errors='surrogateescape')).digest() def hardlink_id_from_inode(self, *, ino, dev): """compute a hardlink id from an inode""" diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 4a6c81163..9fddfa457 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -15,11 +15,11 @@ cdef extern from "_item.c": API_VERSION = '1.2_01' -def fix_key(data, key): +def fix_key(data, key, *, errors='strict'): """if k is a bytes-typed key, migrate key/value to a str-typed key in dict data""" if isinstance(key, bytes): value = data.pop(key) - key = key.decode() + key = key.decode('utf-8', errors=errors) data[key] = value assert isinstance(key, str) return key @@ -29,46 +29,77 @@ def fix_str_value(data, key, errors='surrogateescape'): """makes sure that data[key] is a str (decode if it is bytes)""" assert isinstance(key, str) # fix_key must be called first value = data[key] - if isinstance(value, bytes): - value = value.decode('utf-8', errors=errors) - data[key] = value - assert isinstance(value, str) + value = want_str(value, errors=errors) + data[key] = value return value -def fix_list_of_str(t): +def fix_bytes_value(data, key): + """makes sure that data[key] is bytes (encode if it is str)""" + assert isinstance(key, str) # fix_key must be called first + value = data[key] + value = want_bytes(value) + data[key] = value + return value + + +def fix_list_of_str(v): """make sure we have a list of str""" - assert isinstance(t, (tuple, list)) - l = [e.decode() if isinstance(e, bytes) else e for e in t] - assert all(isinstance(e, str) for e in l), repr(l) - return l + assert isinstance(v, (tuple, list)) + return [want_str(e) for e in v] -def fix_tuple_of_str(t): +def fix_list_of_bytes(v): + """make sure we have a list of bytes""" + assert isinstance(v, (tuple, list)) + return [want_bytes(e) for e in v] + + +def fix_list_of_chunkentries(v): + """make sure we have a list of correct chunkentries""" + assert isinstance(v, (tuple, list)) + chunks = [] + for ce in v: + assert isinstance(ce, (tuple, list)) + assert len(ce) == 3 # id, size, csize + assert isinstance(ce[1], int) + assert isinstance(ce[2], int) + ce_fixed = [want_bytes(ce[0]), ce[1], ce[2]] # list! + chunks.append(ce_fixed) # create a list of lists + return chunks + + +def fix_tuple_of_str(v): """make sure we have a tuple of str""" - assert isinstance(t, (tuple, list)) - t = tuple(e.decode() if isinstance(e, bytes) else e for e in t) - assert all(isinstance(e, str) for e in t), repr(t) - return t + assert isinstance(v, (tuple, list)) + return tuple(want_str(e) for e in v) -def fix_tuple_of_str_and_int(t): +def fix_tuple_of_str_and_int(v): """make sure we have a tuple of str""" - assert isinstance(t, (tuple, list)) - t = tuple(e.decode() if isinstance(e, bytes) else e for e in t) + assert isinstance(v, (tuple, list)) + t = tuple(e.decode() if isinstance(e, bytes) else e for e in v) assert all(isinstance(e, (str, int)) for e in t), repr(t) return t -def want_bytes(v): +def want_bytes(v, *, errors='surrogateescape'): """we know that we want bytes and the value should be bytes""" # legacy support: it being str can be caused by msgpack unpack decoding old data that was packed with use_bin_type=False if isinstance(v, str): - v = v.encode('utf-8', errors='surrogateescape') + v = v.encode('utf-8', errors=errors) assert isinstance(v, bytes) return v +def want_str(v, *, errors='surrogateescape'): + """we know that we want str and the value should be str""" + if isinstance(v, bytes): + v = v.decode('utf-8', errors=errors) + assert isinstance(v, str) + return v + + class PropDict: """ Manage a dictionary via properties. @@ -349,6 +380,11 @@ class Item(PropDict): k = fix_key(d, k) if k in ('path', 'source', 'user', 'group'): v = fix_str_value(d, k) + if k in ('chunks', 'chunks_healthy'): + v = fix_list_of_chunkentries(v) + if k in ('acl_access', 'acl_default', 'acl_extended', 'acl_nfs4'): + v = fix_bytes_value(d, k) + # TODO: xattrs self._dict[k] = v @@ -476,6 +512,8 @@ class ArchiveItem(PropDict): v = fix_tuple_of_str_and_int(v) if k in ('cmdline', 'recreate_cmdline'): v = fix_list_of_str(v) + if k == 'items': + v = fix_list_of_bytes(v) self._dict[k] = v @@ -511,13 +549,15 @@ class ManifestItem(PropDict): ad = v assert isinstance(ad, dict) for ak, av in list(ad.items()): - ak = fix_key(ad, ak) + ak = fix_key(ad, ak, errors='surrogateescape') assert isinstance(av, dict) for ik, iv in list(av.items()): ik = fix_key(av, ik) + if ik == 'id': + fix_bytes_value(av, 'id') + if ik == 'time': + fix_str_value(av, 'time') assert set(av) == {'id', 'time'} - assert isinstance(av['id'], bytes) - fix_str_value(av, 'time') if k == 'timestamp': v = fix_str_value(d, k, 'replace') if k == 'config': From 655c1b9cc2ed9927d52bc6338d2debeb24c69786 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 28 May 2022 21:57:22 +0200 Subject: [PATCH 018/160] update docstrings / comments --- src/borg/item.pyx | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 9fddfa457..c44ce5428 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -223,14 +223,9 @@ class Item(PropDict): Items are 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 Item(internal_dict=d) and use item.key_name later. - msgpack gives us byte-typed values for stuff that should be str, we automatically decode when getting - such a property and encode when setting it. + msgpack unpacker gives us a dict, just give it to Item(internal_dict=d) and use item.key_name later. If an Item shall be serialized, give as_dict() method output to msgpack packer. - - A bug in Attic up to and including release 0.13 added a (meaningless) 'acl' key to every item. - We must never re-use this key. See test_attic013_acl_bug for details. """ VALID_KEYS = ITEM_KEYS | {'deleted', 'nlink', } # str-typed keys @@ -261,7 +256,6 @@ class Item(PropDict): birthtime = PropDict._make_property('birthtime', int, 'int (ns)', encode=int_to_timestamp, decode=timestamp_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. size = PropDict._make_property('size', int) hlid = PropDict._make_property('hlid', bytes, decode=want_bytes) # hard link id: same value means same hard link. @@ -395,13 +389,13 @@ class EncryptedKey(PropDict): A EncryptedKey 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 EncryptedKey(d) and use enc_key.xxx later. + msgpack unpacker gives us a dict, just give it to EncryptedKey(d) and use enc_key.xxx later. If a EncryptedKey shall be serialized, give as_dict() method output to msgpack packer. """ - VALID_KEYS = { 'version', 'algorithm', 'iterations', 'salt', 'hash', 'data', - 'argon2_time_cost', 'argon2_memory_cost', 'argon2_parallelism', 'argon2_type' } + VALID_KEYS = {'version', 'algorithm', 'iterations', 'salt', 'hash', 'data', + 'argon2_time_cost', 'argon2_memory_cost', 'argon2_parallelism', 'argon2_type'} __slots__ = ("_dict", ) # avoid setting attributes not supported by properties @@ -434,7 +428,7 @@ class Key(PropDict): A Key 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 Key(d) and use key.xxx later. + msgpack unpacker gives us a dict, just give it to Key(d) and use key.xxx later. If a Key shall be serialized, give as_dict() method output to msgpack packer. """ @@ -467,7 +461,7 @@ class ArchiveItem(PropDict): 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. + msgpack unpacker gives us a dict, 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. """ @@ -524,7 +518,7 @@ class ManifestItem(PropDict): 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. + msgpack unpacker gives us a dict, 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. """ From 8e58525fc6d3eceae4d846a7be9e2eed20084580 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 29 May 2022 15:38:43 +0200 Subject: [PATCH 019/160] Item: remove some decode= params update_internal() makes sure they have the desired type already. --- src/borg/item.pyx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index c44ce5428..0fabc0241 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -239,10 +239,10 @@ class Item(PropDict): user = PropDict._make_property('user', (str, type(None)), 'surrogate-escaped str or None') group = PropDict._make_property('group', (str, type(None)), 'surrogate-escaped str or None') - acl_access = PropDict._make_property('acl_access', bytes, decode=want_bytes) - acl_default = PropDict._make_property('acl_default', bytes, decode=want_bytes) - acl_extended = PropDict._make_property('acl_extended', bytes, decode=want_bytes) - acl_nfs4 = PropDict._make_property('acl_nfs4', bytes, decode=want_bytes) + acl_access = PropDict._make_property('acl_access', bytes) + acl_default = PropDict._make_property('acl_default', bytes) + acl_extended = PropDict._make_property('acl_extended', bytes) + acl_nfs4 = PropDict._make_property('acl_nfs4', bytes) mode = PropDict._make_property('mode', int) uid = PropDict._make_property('uid', int) @@ -258,7 +258,7 @@ class Item(PropDict): # size is only present for items with a chunk list and then it is sum(chunk_sizes) size = PropDict._make_property('size', int) - hlid = PropDict._make_property('hlid', bytes, decode=want_bytes) # hard link id: same value means same hard link. + hlid = PropDict._make_property('hlid', bytes) # hard link id: same value means same hard link. hardlink_master = PropDict._make_property('hardlink_master', bool) # legacy chunks = PropDict._make_property('chunks', (list, type(None)), 'list or None') @@ -482,7 +482,7 @@ class ArchiveItem(PropDict): chunker_params = PropDict._make_property('chunker_params', tuple) recreate_cmdline = PropDict._make_property('recreate_cmdline', list) # list of s-e-str # recreate_source_id, recreate_args, recreate_partial_chunks were used in 1.1.0b1 .. b2 - recreate_source_id = PropDict._make_property('recreate_source_id', bytes, decode=want_bytes) + recreate_source_id = PropDict._make_property('recreate_source_id', bytes) 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 size = PropDict._make_property('size', int) From 7b138cc7107ebc00f5b9ed422e2794da8eedba61 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 29 May 2022 16:43:51 +0200 Subject: [PATCH 020/160] Item: convert timestamps once, get rid of bigint code --- src/borg/archiver.py | 4 ---- src/borg/helpers/__init__.py | 2 +- src/borg/helpers/msgpack.py | 25 +++---------------------- src/borg/item.pyx | 18 ++++++++++++++++-- src/borg/testsuite/helpers.py | 14 +------------- 5 files changed, 21 insertions(+), 42 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index a91e43c54..7ac3c7e6d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -366,10 +366,6 @@ class Archiver: if chunks_healthy is not None: item._dict['chunks_healthy'] = chunks item._dict.pop('source') # not used for hardlinks any more, replaced by hlid - for attr in 'atime', 'ctime', 'mtime', 'birthtime': - if attr in item: - ns = getattr(item, attr) # decode (bigint or Timestamp) --> int ns - setattr(item, attr, ns) # encode int ns --> msgpack.Timestamp only, no bigint any more # make sure we only have desired stuff in the new item. specifically, make sure to get rid of: # - 'acl' remnants of bug in attic <= 0.13 # - 'hardlink_master' (superseded by hlid) diff --git a/src/borg/helpers/__init__.py b/src/borg/helpers/__init__.py index 34a59b903..d8a4cda62 100644 --- a/src/borg/helpers/__init__.py +++ b/src/borg/helpers/__init__.py @@ -18,7 +18,7 @@ from .progress import * # NOQA from .time import * # NOQA from .yes import * # NOQA -from .msgpack import is_slow_msgpack, is_supported_msgpack, int_to_bigint, bigint_to_int, get_limited_unpacker +from .msgpack import is_slow_msgpack, is_supported_msgpack, get_limited_unpacker from . import msgpack # generic mechanism to enable users to invoke workarounds by setting the diff --git a/src/borg/helpers/msgpack.py b/src/borg/helpers/msgpack.py index 268ee30e7..4e79c0274 100644 --- a/src/borg/helpers/msgpack.py +++ b/src/borg/helpers/msgpack.py @@ -201,30 +201,11 @@ def get_limited_unpacker(kind): return Unpacker(**args) -def bigint_to_int(mtime): # legacy - """Convert bytearray to int - """ - if isinstance(mtime, bytes): - return int.from_bytes(mtime, 'little', signed=True) - return mtime - - -def int_to_bigint(value): # legacy - """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 int_to_timestamp(ns): + assert isinstance(ns, int) return Timestamp.from_unix_nano(ns) def timestamp_to_int(ts): - if isinstance(ts, Timestamp): - return ts.to_unix_nano() - # legacy support note: we need to keep the bigint conversion for compatibility with borg < 1.3 archives. - return bigint_to_int(ts) + assert isinstance(ts, Timestamp) + return ts.to_unix_nano() diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 0fabc0241..1520527a3 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -4,7 +4,7 @@ from collections import namedtuple from .constants import ITEM_KEYS, ARCHIVE_KEYS from .helpers import StableDict from .helpers import format_file_size -from .helpers.msgpack import timestamp_to_int, int_to_timestamp +from .helpers.msgpack import timestamp_to_int, int_to_timestamp, Timestamp cdef extern from "_item.c": @@ -83,6 +83,17 @@ def fix_tuple_of_str_and_int(v): return t +def fix_timestamp(v): + """make sure v is a Timestamp""" + if isinstance(v, Timestamp): + return v + # legacy support + if isinstance(v, bytes): # was: bigint_to_int() + v = int.from_bytes(v, 'little', signed=True) + assert isinstance(v, int) + return int_to_timestamp(v) + + def want_bytes(v, *, errors='surrogateescape'): """we know that we want bytes and the value should be bytes""" # legacy support: it being str can be caused by msgpack unpack decoding old data that was packed with use_bin_type=False @@ -369,13 +380,16 @@ class Item(PropDict): return False def update_internal(self, d): - # legacy support for migration (data from old msgpacks comes in as bytes always, but sometimes we want str) + # legacy support for migration (data from old msgpacks comes in as bytes always, but sometimes we want str), + # also need to fix old timestamp data types. for k, v in list(d.items()): k = fix_key(d, k) if k in ('path', 'source', 'user', 'group'): v = fix_str_value(d, k) if k in ('chunks', 'chunks_healthy'): v = fix_list_of_chunkentries(v) + if k in ('atime', 'ctime', 'mtime', 'birthtime'): + v = fix_timestamp(v) if k in ('acl_access', 'acl_default', 'acl_extended', 'acl_nfs4'): v = fix_bytes_value(d, k) # TODO: xattrs diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index b181da564..fd0414116 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -22,7 +22,7 @@ from ..helpers import get_base_dir, get_cache_dir, get_keys_dir, get_security_di from ..helpers import is_slow_msgpack from ..helpers import 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 from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless from ..helpers import swidth_slice @@ -38,18 +38,6 @@ from ..helpers.passphrase import Passphrase, PasswordRetriesExceeded 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 9d684120a2ae91fcd75003cda1ce36e7c1f22056 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 29 May 2022 17:32:42 +0200 Subject: [PATCH 021/160] Item: assert type also in property getter also: fixed Item.xattrs to be StableDict (not just a dict, as the msgpack unpacker gives us) --- src/borg/item.pyx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 1520527a3..0e8ff179e 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -207,6 +207,8 @@ class PropDict: raise AttributeError(attr_error_msg) from None if decode is not None: value = decode(value) + if not isinstance(value, value_type): + raise TypeError(type_error_msg) return value def _set(self, value): @@ -392,7 +394,10 @@ class Item(PropDict): v = fix_timestamp(v) if k in ('acl_access', 'acl_default', 'acl_extended', 'acl_nfs4'): v = fix_bytes_value(d, k) - # TODO: xattrs + if k == 'xattrs': + if not isinstance(v, StableDict): + v = StableDict(v) + # TODO: xattrs key/value types self._dict[k] = v From 64cc16a9f470925dc0c9da152ec3ff2864b3ae95 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 29 May 2022 21:22:50 +0200 Subject: [PATCH 022/160] Item: fix xattr processing Item.xattrs is now always a StableDict mapping bytes keys -> bytes values. The special casing of empty values (b'') getting replaced by None was removed. --- src/borg/item.pyx | 12 +++++++++--- src/borg/xattr.py | 8 ++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 0e8ff179e..c3f59ed27 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -99,7 +99,7 @@ def want_bytes(v, *, errors='surrogateescape'): # legacy support: it being str can be caused by msgpack unpack decoding old data that was packed with use_bin_type=False if isinstance(v, str): v = v.encode('utf-8', errors=errors) - assert isinstance(v, bytes) + assert isinstance(v, bytes), f'not a bytes object, but {v!r}' return v @@ -107,7 +107,7 @@ def want_str(v, *, errors='surrogateescape'): """we know that we want str and the value should be str""" if isinstance(v, bytes): v = v.decode('utf-8', errors=errors) - assert isinstance(v, str) + assert isinstance(v, str), f'not a str object, but {v!r}' return v @@ -397,7 +397,13 @@ class Item(PropDict): if k == 'xattrs': if not isinstance(v, StableDict): v = StableDict(v) - # TODO: xattrs key/value types + v_new = StableDict() + for xk, xv in list(v.items()): + xk = want_bytes(xk) + # old borg used to store None instead of a b'' value + xv = b'' if xv is None else want_bytes(xv) + v_new[xk] = xv + v = v_new # xattrs is a StableDict(bytes keys -> bytes values) self._dict[k] = v diff --git a/src/borg/xattr.py b/src/borg/xattr.py index 8a731340c..175f6372c 100644 --- a/src/borg/xattr.py +++ b/src/borg/xattr.py @@ -76,9 +76,7 @@ def get_all(path, follow_symlinks=False): for name in names: try: # xattr name is a bytes object, we directly use it. - # if we get an empty xattr value (b''), we store None into the result dict - - # borg always did it like that... - result[name] = getxattr(path, name, follow_symlinks=follow_symlinks) or None + result[name] = getxattr(path, name, follow_symlinks=follow_symlinks) except OSError as e: name_str = name.decode() if isinstance(path, int): @@ -122,9 +120,7 @@ def set_all(path, xattrs, follow_symlinks=False): warning = False for k, v in xattrs.items(): try: - # the key k is a bytes object due to msgpack unpacking it as such. - # if we have a None value, it means "empty", so give b'' to setxattr in that case: - setxattr(path, k, v or b'', follow_symlinks=follow_symlinks) + setxattr(path, k, v, follow_symlinks=follow_symlinks) except OSError as e: warning = True k_str = k.decode() From f2b085787b0fb13634073b1df3979a30ac66fa5a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 30 May 2022 00:05:07 +0200 Subject: [PATCH 023/160] Item: disallow None value for .user/group/chunks/chunks_healthy If we do not know the value, just do not have that key/value pair in the item. --- src/borg/archive.py | 23 ++++++++++++++--------- src/borg/archiver.py | 4 ++-- src/borg/helpers/parseformat.py | 4 ++-- src/borg/item.pyx | 8 ++++---- src/borg/testsuite/item.py | 8 -------- 5 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index ff6ca7ce6..e068413f5 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -392,14 +392,14 @@ def get_item_uid_gid(item, *, numeric, uid_forced=None, gid_forced=None, uid_def if uid_forced is not None: uid = uid_forced else: - uid = None if numeric else user2uid(item.user) + uid = None if numeric else user2uid(item.get('user')) uid = item.uid if uid is None else uid if uid < 0: uid = uid_default if gid_forced is not None: gid = gid_forced else: - gid = None if numeric else group2gid(item.group) + gid = None if numeric else group2gid(item.get('group')) gid = item.gid if gid is None else gid if gid < 0: gid = gid_default @@ -1089,11 +1089,13 @@ class MetadataCollector: if not self.nobirthtime and hasattr(st, 'st_birthtime'): # sadly, there's no stat_result.st_birthtime_ns attrs['birthtime'] = safe_ns(int(st.st_birthtime * 10**9)) - if self.numeric_ids: - attrs['user'] = attrs['group'] = None - else: - attrs['user'] = uid2user(st.st_uid) - attrs['group'] = gid2group(st.st_gid) + if not self.numeric_ids: + user = uid2user(st.st_uid) + if user is not None: + attrs['user'] = user + group = gid2group(st.st_gid) + if group is not None: + attrs['group'] = group return attrs def stat_ext_attrs(self, st, path, fd=None): @@ -1426,8 +1428,11 @@ class TarfileObjectProcessors: return safe_ns(int(float(s) * 1e9)) item = Item(path=make_path_safe(tarinfo.name), mode=tarinfo.mode | type, - uid=tarinfo.uid, gid=tarinfo.gid, user=tarinfo.uname or None, group=tarinfo.gname or None, - mtime=s_to_ns(tarinfo.mtime)) + uid=tarinfo.uid, gid=tarinfo.gid, mtime=s_to_ns(tarinfo.mtime)) + if tarinfo.uname: + item.user = tarinfo.uname + if tarinfo.gname: + item.group = tarinfo.gname if ph: # note: for mtime this is a bit redundant as it is already done by tarfile module, # but we just do it in our way to be consistent for sure. diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 7ac3c7e6d..8d5513973 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1355,8 +1355,8 @@ class Archiver: 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 '' + tarinfo.uname = item.get('user', '') + tarinfo.gname = item.get('group', '') # The linkname in tar has 2 uses: # 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 diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 414402de0..e772f38c4 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -808,8 +808,8 @@ class ItemFormatter(BaseFormatter): hlid = bin_to_hex(hlid) if hlid else '' item_data['type'] = item_type item_data['mode'] = mode - item_data['user'] = item.user or item.uid - item_data['group'] = item.group or item.gid + item_data['user'] = item.get('user', str(item.uid)) + item_data['group'] = item.get('group', str(item.gid)) item_data['uid'] = item.uid item_data['gid'] = item.gid item_data['path'] = remove_surrogates(item.path) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index c3f59ed27..1822cf9de 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -249,8 +249,8 @@ class Item(PropDict): path = PropDict._make_property('path', str, 'surrogate-escaped str') source = PropDict._make_property('source', str, 'surrogate-escaped str') - user = PropDict._make_property('user', (str, type(None)), 'surrogate-escaped str or None') - group = PropDict._make_property('group', (str, type(None)), 'surrogate-escaped str or None') + user = PropDict._make_property('user', str, 'surrogate-escaped str') + group = PropDict._make_property('group', str, 'surrogate-escaped str') acl_access = PropDict._make_property('acl_access', bytes) acl_default = PropDict._make_property('acl_default', bytes) @@ -274,8 +274,8 @@ class Item(PropDict): hlid = PropDict._make_property('hlid', bytes) # hard link id: same value means same hard link. hardlink_master = PropDict._make_property('hardlink_master', bool) # legacy - chunks = PropDict._make_property('chunks', (list, type(None)), 'list or None') - chunks_healthy = PropDict._make_property('chunks_healthy', (list, type(None)), 'list or None') + chunks = PropDict._make_property('chunks', list, 'list') + chunks_healthy = PropDict._make_property('chunks_healthy', list, 'list') xattrs = PropDict._make_property('xattrs', StableDict) diff --git a/src/borg/testsuite/item.py b/src/borg/testsuite/item.py index 94167e7ea..a0c1d9d13 100644 --- a/src/borg/testsuite/item.py +++ b/src/borg/testsuite/item.py @@ -89,14 +89,6 @@ def test_item_mptimestamp_property(): assert item.as_dict() == {'atime': Timestamp.from_unix_nano(big)} -def test_item_user_group_none(): - item = Item() - item.user = None - assert item.user is None - item.group = None - assert item.group is None - - def test_item_se_str_property(): # start simple item = Item() From ed22f721f3e43e7800173d87b7a1d4cefedbff8e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 30 May 2022 13:32:11 +0200 Subject: [PATCH 024/160] EncryptedKey: fix once, remove decode=... --- src/borg/item.pyx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 1822cf9de..5e6e401f5 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -427,9 +427,9 @@ class EncryptedKey(PropDict): version = PropDict._make_property('version', int) algorithm = PropDict._make_property('algorithm', str) iterations = PropDict._make_property('iterations', int) - salt = PropDict._make_property('salt', bytes, decode=want_bytes) - hash = PropDict._make_property('hash', bytes, decode=want_bytes) - data = PropDict._make_property('data', bytes, decode=want_bytes) + salt = PropDict._make_property('salt', bytes) + hash = PropDict._make_property('hash', bytes) + data = PropDict._make_property('data', bytes) argon2_time_cost = PropDict._make_property('argon2_time_cost', int) argon2_memory_cost = PropDict._make_property('argon2_memory_cost', int) argon2_parallelism = PropDict._make_property('argon2_parallelism', int) @@ -443,6 +443,8 @@ class EncryptedKey(PropDict): assert isinstance(v, int) if k in ('algorithm', 'argon2_type'): v = fix_str_value(d, k) + if k in ('salt', 'hash', 'data'): + v = fix_bytes_value(d, k) self._dict[k] = v From 58009f6773c5658a3b0dc7b8fd8c956f7ed158c8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 30 May 2022 13:38:05 +0200 Subject: [PATCH 025/160] Key: fix once, remove decode=... --- src/borg/item.pyx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 5e6e401f5..169aec048 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -465,10 +465,10 @@ class Key(PropDict): __slots__ = ("_dict", ) # avoid setting attributes not supported by properties version = PropDict._make_property('version', int) - repository_id = PropDict._make_property('repository_id', bytes, decode=want_bytes) - enc_key = PropDict._make_property('enc_key', bytes, decode=want_bytes) - enc_hmac_key = PropDict._make_property('enc_hmac_key', bytes, decode=want_bytes) - id_key = PropDict._make_property('id_key', bytes, decode=want_bytes) + repository_id = PropDict._make_property('repository_id', bytes) + enc_key = PropDict._make_property('enc_key', bytes) + 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) @@ -478,6 +478,8 @@ class Key(PropDict): k = fix_key(d, k) if k == 'version': assert isinstance(v, int) + if k in ('repository_id', 'enc_key', 'enc_hmac_key', 'id_key'): + v = fix_bytes_value(d, k) self._dict[k] = v From 421d4bdfb05f0917f1337ad0e00f8cfaa6952d61 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 30 May 2022 23:22:48 +0200 Subject: [PATCH 026/160] docs: fix bytes -> str in data-structures docs --- docs/internals/data-structures.rst | 38 +++++++++++++++--------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index d1a5a4cd3..8de20761b 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -329,17 +329,17 @@ or modified. It looks like this: .. code-block:: python { - 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'2017-05-05-system-backup': { - b'id': b'<32 byte binary object ID>', - b'time': b'2017-05-05T12:42:22.942864', + 'version': 1, + 'timestamp': '2017-05-05T12:42:23.042864', + 'item_keys': ['acl_access', 'acl_default', ...], + 'config': {}, + 'archives': { + '2017-05-05-system-backup': { + 'id': b'<32 byte binary object ID>', + 'time': '2017-05-05T12:42:22.942864', }, }, - b'tam': ..., + 'tam': ..., } The *version* field can be either 1 or 2. The versions differ in the @@ -393,15 +393,15 @@ 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'], + 'feature_flags': { + 'read': { + 'mandatory': ['some_feature'], }, - b'check': { - b'mandatory': [b'other_feature'], + 'check': { + 'mandatory': ['other_feature'], } - b'write': ..., - b'delete': ... + 'write': ..., + 'delete': ... }, } @@ -1220,9 +1220,9 @@ transaction ID in the file names. Integrity data is stored in a third file .. code-block:: python { - b'version': 2, - b'hints': b'{"algorithm": "XXH64", "digests": {"final": "411208db2aa13f1a"}}', - b'index': b'{"algorithm": "XXH64", "digests": {"HashHeader": "846b7315f91b8e48", "final": "cb3e26cadc173e40"}}' + 'version': 2, + 'hints': '{"algorithm": "XXH64", "digests": {"final": "411208db2aa13f1a"}}', + 'index': '{"algorithm": "XXH64", "digests": {"HashHeader": "846b7315f91b8e48", "final": "cb3e26cadc173e40"}}' } The *version* key started at 2, the same version used for the hints. Since Borg has From 08228fbd32cc29822085be9fd43926f0d4fb79fa Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 31 May 2022 00:07:01 +0200 Subject: [PATCH 027/160] Item: remove unused hardlink_masters param --- src/borg/item.pyx | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 169aec048..cd1a3544c 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -284,12 +284,10 @@ class Item(PropDict): part = PropDict._make_property('part', int) - def get_size(self, hardlink_masters=None, memorize=False, compressed=False, from_chunks=False, consider_ids=None): + def get_size(self, memorize=False, compressed=False, from_chunks=False, consider_ids=None): """ Determine the (uncompressed or compressed) size of this item. - :param hardlink_masters: If given, the size of hardlink slaves is computed via the hardlink master's chunk list, - otherwise size will be returned as 0. :param memorize: Whether the computed size value will be stored into the item. :param compressed: Whether the compressed or uncompressed size will be returned. :param from_chunks: If true, size is computed from chunks even if a precomputed value is available. @@ -309,31 +307,14 @@ class Item(PropDict): # 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 + return 0 if consider_ids is not None: size = sum(getattr(ChunkListEntry(*chunk), attr) for chunk in chunks if chunk.id in consider_ids) else: 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: + if memorize: setattr(self, attr, size) return size From d4ee968b07b8cf4971049d13af8ed56d6ef7b5d1 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 9 Jun 2022 18:13:40 +0200 Subject: [PATCH 028/160] use borg 2.0 to refer to this, not 1.3 also, some type conversions are now done in update_internal once, not in the decode methods of the classes in item.pyx. --- src/borg/constants.py | 2 +- src/borg/helpers/msgpack.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/borg/constants.py b/src/borg/constants.py index 13eb8bd23..d2fa11919 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -128,7 +128,7 @@ class KeyType: # upper 4 bits are ciphersuite, 0 == legacy AES-CTR KEYFILE = 0x00 # repos with PASSPHRASE mode could not be created any more since borg 1.0, see #97. - # in borg 1.3 all of its code and also the "borg key migrate-to-repokey" command was removed. + # in borg 2. all of its code and also the "borg key migrate-to-repokey" command was removed. # if you still need to, you can use "borg key migrate-to-repokey" with borg 1.0, 1.1 and 1.2. # Nowadays, we just dispatch this to RepoKey and assume the passphrase was migrated to a repokey. PASSPHRASE = 0x01 # legacy, attic and borg < 1.0 diff --git a/src/borg/helpers/msgpack.py b/src/borg/helpers/msgpack.py index 4e79c0274..ffb87be03 100644 --- a/src/borg/helpers/msgpack.py +++ b/src/borg/helpers/msgpack.py @@ -6,7 +6,7 @@ We wrap msgpack here the way we need it - to avoid having lots of clutter in the Packing ------- -- use_bin_type = True (used by borg since borg 1.3) +- use_bin_type = True (used by borg since borg 2.0) This is used to generate output according to new msgpack 2.0 spec. This cleanly keeps bytes and str types apart. @@ -21,16 +21,17 @@ Packing Unpacking --------- -- raw = False (used by borg since borg 1.3) - We already can use this with borg 1.3 due to the want_bytes decoder. - This decoder can be removed in future, when we do not have to deal with data any more that was packed the old way. +- raw = False (used by borg since borg 2.0) + We already can use this with borg 2.0 due to the type conversion to the desired type in item.py update_internal + methods. This type conversion code can be removed in future, when we do not have to deal with data any more + that was packed the old way. It will then unpack according to the msgpack 2.0 spec format and directly output bytes or str. - raw = True (the old way, used by borg < 1.3) - unicode_errors = 'surrogateescape' -> see description above (will be used when raw is False). -As of borg 1.3, we have fixed most of the msgpack str/bytes mess, #968. +As of borg 2.0, we have fixed most of the msgpack str/bytes mess, #968. Borg now still needs to **read** old repos, archives, keys, ... so we can not yet fix it completely. But from now on, borg only **writes** new data according to the new msgpack 2.0 spec, thus we can remove some legacy support in a later borg release (some places are marked with "legacy"). @@ -41,7 +42,7 @@ current way in msgpack terms - pack with use_bin_type=True (according to msgpack 2.0 spec) - packs str -> raw and bytes -> bin - unpack with raw=False (according to msgpack 2.0 spec, using unicode_errors='surrogateescape') -- unpacks bin to bytes and raw to str (thus we need to re-encode manually if we want bytes from "raw") +- unpacks bin to bytes and raw to str (thus we need to convert to desired type if we want bytes from "raw") """ from .datastruct import StableDict From b9f9623a6d7301b0586df226b05ed81d032f77c5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 10 Jun 2022 15:59:29 +0200 Subject: [PATCH 029/160] prepare to remove csize (set it to 0 for now) --- src/borg/archive.py | 114 +++++++------------------------- src/borg/archiver.py | 6 +- src/borg/cache.py | 84 +++++------------------ src/borg/constants.py | 2 +- src/borg/hashindex.pyx | 2 - src/borg/helpers/parseformat.py | 14 +--- src/borg/item.pyx | 5 +- src/borg/testsuite/archive.py | 23 +++---- src/borg/testsuite/archiver.py | 91 +++---------------------- src/borg/testsuite/hashindex.py | 4 +- src/borg/testsuite/item.py | 8 +-- 11 files changed, 74 insertions(+), 279 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index e068413f5..b2aabfee6 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -58,60 +58,46 @@ class Statistics: def __init__(self, output_json=False, iec=False): self.output_json = output_json self.iec = iec - self.osize = self.csize = self.usize = self.nfiles = 0 - self.osize_parts = self.csize_parts = self.usize_parts = self.nfiles_parts = 0 + self.osize = self.nfiles = 0 + self.osize_parts = self.nfiles_parts = 0 self.last_progress = 0 # timestamp when last progress was shown - def update(self, size, csize, unique, part=False): + def update(self, size, part=False): if not part: self.osize += size - self.csize += csize - if unique: - self.usize += csize else: self.osize_parts += size - self.csize_parts += csize - if unique: - self.usize_parts += csize def __add__(self, other): if not isinstance(other, Statistics): raise TypeError('can only add Statistics objects') stats = Statistics(self.output_json, self.iec) stats.osize = self.osize + other.osize - stats.csize = self.csize + other.csize - stats.usize = self.usize + other.usize stats.nfiles = self.nfiles + other.nfiles stats.osize_parts = self.osize_parts + other.osize_parts - stats.csize_parts = self.csize_parts + other.csize_parts - stats.usize_parts = self.usize_parts + other.usize_parts stats.nfiles_parts = self.nfiles_parts + other.nfiles_parts return stats - summary = "{label:15} {stats.osize_fmt:>20s} {stats.csize_fmt:>20s} {stats.usize_fmt:>20s}" + summary = "{label:15} {stats.osize_fmt:>20s}" def __str__(self): return self.summary.format(stats=self, label='This archive:') def __repr__(self): - return "<{cls} object at {hash:#x} ({self.osize}, {self.csize}, {self.usize})>".format( + return "<{cls} object at {hash:#x} ({self.osize})>".format( cls=type(self).__name__, hash=id(self), self=self) def as_dict(self): return { 'original_size': FileSize(self.osize, iec=self.iec), - 'compressed_size': FileSize(self.csize, iec=self.iec), - 'deduplicated_size': FileSize(self.usize, iec=self.iec), 'nfiles': self.nfiles, } def as_raw_dict(self): return { 'size': self.osize, - 'csize': self.csize, 'nfiles': self.nfiles, 'size_parts': self.osize_parts, - 'csize_parts': self.csize_parts, 'nfiles_parts': self.nfiles_parts, } @@ -119,10 +105,8 @@ class Statistics: def from_raw_dict(cls, **kw): self = cls() self.osize = kw['size'] - self.csize = kw['csize'] self.nfiles = kw['nfiles'] self.osize_parts = kw['size_parts'] - self.csize_parts = kw['csize_parts'] self.nfiles_parts = kw['nfiles_parts'] return self @@ -130,14 +114,6 @@ class Statistics: def osize_fmt(self): return format_file_size(self.osize, iec=self.iec) - @property - def usize_fmt(self): - return format_file_size(self.usize, iec=self.iec) - - @property - def csize_fmt(self): - return format_file_size(self.csize, iec=self.iec) - def show_progress(self, item=None, final=False, stream=None, dt=None): now = time.monotonic() if dt is None or now - self.last_progress > dt: @@ -158,7 +134,7 @@ class Statistics: else: 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) + msg = '{0.osize_fmt} O {0.nfiles} N '.format(self) path = remove_surrogates(item.path) if item else '' space = columns - swidth(msg) if space < 12: @@ -614,10 +590,8 @@ Utilization of max. archive size: {csize_max:.0%} if stats is not None: metadata.update({ 'size': stats.osize, - 'csize': stats.csize, 'nfiles': stats.nfiles, 'size_parts': stats.osize_parts, - 'csize_parts': stats.csize_parts, 'nfiles_parts': stats.nfiles_parts}) metadata.update(additional_metadata or {}) metadata = ArchiveItem(metadata) @@ -651,51 +625,12 @@ Utilization of max. archive size: {csize_max:.0%} return stats def _calc_stats(self, cache, want_unique=True): - have_borg12_meta = self.metadata.get('nfiles') is not None - - if have_borg12_meta and not want_unique: - unique_csize = 0 - else: - def add(id): - entry = cache.chunks[id] - archive_index.add(id, 1, entry.size, entry.csize) - - archive_index = ChunkIndex() - sync = CacheSynchronizer(archive_index) - add(self.id) - # we must escape any % char in the archive name, because we use it in a format string, see #6500 - arch_name_escd = self.name.replace('%', '%%') - pi = ProgressIndicatorPercent(total=len(self.metadata.items), - msg='Calculating statistics for archive %s ... %%3.0f%%%%' % arch_name_escd, - msgid='archive.calc_stats') - 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) - sync.feed(data) - unique_csize = archive_index.stats_against(cache.chunks)[3] - pi.finish() - stats = Statistics(iec=self.iec) - stats.usize = unique_csize # the part files use same chunks as the full file - if not have_borg12_meta: - if self.consider_part_files: - stats.nfiles = sync.num_files_totals - stats.osize = sync.size_totals - stats.csize = sync.csize_totals - else: - stats.nfiles = sync.num_files_totals - sync.num_files_parts - stats.osize = sync.size_totals - sync.size_parts - stats.csize = sync.csize_totals - sync.csize_parts - else: - if self.consider_part_files: - stats.nfiles = self.metadata.nfiles_parts + self.metadata.nfiles - stats.osize = self.metadata.size_parts + self.metadata.size - stats.csize = self.metadata.csize_parts + self.metadata.csize - else: - stats.nfiles = self.metadata.nfiles - stats.osize = self.metadata.size - stats.csize = self.metadata.csize + stats.nfiles = self.metadata.nfiles + stats.osize = self.metadata.size + if self.consider_part_files: + stats.nfiles += self.metadata.nfiles_parts + stats.osize += self.metadata.size_parts return stats @contextmanager @@ -986,7 +921,7 @@ Utilization of max. archive size: {csize_max:.0%} item = Item(internal_dict=item) if 'chunks' in item: part = not self.consider_part_files and 'part' in item - for chunk_id, size, csize in item.chunks: + for chunk_id, size, _ in item.chunks: chunk_decref(chunk_id, stats, part=part) except (TypeError, ValueError): # if items metadata spans multiple chunks and one chunk got dropped somehow, @@ -1789,15 +1724,15 @@ class ArchiveChecker: def add_callback(chunk): id_ = self.key.id_hash(chunk) cdata = self.key.encrypt(id_, chunk) - add_reference(id_, len(chunk), len(cdata), cdata) + add_reference(id_, len(chunk), cdata) return id_ - def add_reference(id_, size, csize, cdata=None): + def add_reference(id_, size, cdata=None): try: self.chunks.incref(id_) except KeyError: assert cdata is not None - self.chunks[id_] = ChunkIndexEntry(refcount=1, size=size, csize=csize) + self.chunks[id_] = ChunkIndexEntry(refcount=1, size=size, csize=0) # was: csize=csize if self.repair: self.repository.put(id_, cdata) @@ -1811,8 +1746,7 @@ class ArchiveChecker: chunk = Chunk(None, allocation=CH_ALLOC, size=size) chunk_id, data = cached_hash(chunk, self.key.id_hash) cdata = self.key.encrypt(chunk_id, data) - csize = len(cdata) - return chunk_id, size, csize, cdata + return chunk_id, size, cdata offset = 0 chunk_list = [] @@ -1835,30 +1769,30 @@ class ArchiveChecker: 'Replacing with all-zero chunk.'.format( archive_name, item.path, offset, offset + size, bin_to_hex(chunk_id))) self.error_found = chunks_replaced = True - chunk_id, size, csize, cdata = replacement_chunk(size) - add_reference(chunk_id, size, csize, cdata) + chunk_id, size, cdata = replacement_chunk(size) + add_reference(chunk_id, size, cdata) else: logger.info('{}: {}: Previously missing file chunk is still missing (Byte {}-{}, Chunk {}). ' 'It has an all-zero replacement chunk already.'.format( archive_name, item.path, offset, offset + size, bin_to_hex(chunk_id))) chunk_id, size, csize = chunk_current if chunk_id in self.chunks: - add_reference(chunk_id, size, csize) + add_reference(chunk_id, size) else: logger.warning('{}: {}: Missing all-zero replacement chunk detected (Byte {}-{}, Chunk {}). ' 'Generating new replacement chunk.'.format( archive_name, item.path, offset, offset + size, bin_to_hex(chunk_id))) self.error_found = chunks_replaced = True - chunk_id, size, csize, cdata = replacement_chunk(size) - add_reference(chunk_id, size, csize, cdata) + chunk_id, size, cdata = replacement_chunk(size) + add_reference(chunk_id, size, cdata) else: if chunk_current == chunk_healthy: # normal case, all fine. - add_reference(chunk_id, size, csize) + add_reference(chunk_id, size) else: logger.info('{}: {}: Healed previously missing file chunk! (Byte {}-{}, Chunk {}).'.format( archive_name, item.path, offset, offset + size, bin_to_hex(chunk_id))) - add_reference(chunk_id, size, csize) + add_reference(chunk_id, size) mark_as_possibly_superseded(chunk_current[0]) # maybe orphaned the all-zero replacement chunk chunk_list.append([chunk_id, size, csize]) # list-typed element as chunks_healthy is list-of-lists offset += size @@ -2005,7 +1939,7 @@ class ArchiveChecker: data = msgpack.packb(archive.as_dict()) new_archive_id = self.key.id_hash(data) cdata = self.key.encrypt(new_archive_id, data) - add_reference(new_archive_id, len(data), len(cdata), cdata) + add_reference(new_archive_id, len(data), cdata) self.manifest.archives[info.name] = (new_archive_id, info.ts) pi.finish() diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 8d5513973..b46bfa3f3 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -99,7 +99,7 @@ except BaseException: assert EXIT_ERROR == 2, "EXIT_ERROR is not 2, as expected - fix assert AND exception handler right above this line." -STATS_HEADER = " Original size Compressed size Deduplicated size" +STATS_HEADER = " Original size" PURE_PYTHON_MSGPACK_WARNING = "Using a pure-python msgpack! This will result in lower performance." @@ -1797,8 +1797,8 @@ class Archiver: Command line: {command_line} Utilization of maximum supported 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} + Original size + This archive: {stats[original_size]:>20s} {cache} """).strip().format(cache=cache, **info)) if self.exit_code: diff --git a/src/borg/cache.py b/src/borg/cache.py index 58ceb541b..413e83bb4 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -406,7 +406,7 @@ class Cache: class CacheStatsMixin: str_format = """\ -All archives: {0.total_size:>20s} {0.total_csize:>20s} {0.unique_csize:>20s} +All archives: {0.total_size:>20s} Unique chunks Total chunks Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" @@ -418,39 +418,32 @@ 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']) + Summary = namedtuple('Summary', ['total_size', 'unique_size', 'total_unique_chunks', 'total_chunks']) def stats(self): from .archive import Archive # XXX: this should really be moved down to `hashindex.pyx` - total_size, total_csize, unique_size, unique_csize, total_unique_chunks, total_chunks = self.chunks.summarize() + total_size, _, unique_size, _, total_unique_chunks, total_chunks = self.chunks.summarize() # the above values have the problem that they do not consider part files, - # thus the total_size and total_csize might be too high (chunks referenced + # thus the total_size might be too high (chunks referenced # by the part files AND by the complete file). - # since borg 1.2 we have new archive metadata telling the total size and - # csize per archive, so we can just sum up all archives to get the "all - # archives" stats: - total_size, total_csize = 0, 0 + # since borg 1.2 we have new archive metadata telling the total size per archive, + # so we can just sum up all archives to get the "all archives" stats: + total_size = 0 for archive_name in self.manifest.archives: archive = Archive(self.repository, self.key, self.manifest, archive_name, consider_part_files=self.consider_part_files) stats = archive.calc_stats(self, want_unique=False) total_size += stats.osize - total_csize += stats.csize - stats = self.Summary(total_size, total_csize, unique_size, unique_csize, - total_unique_chunks, total_chunks)._asdict() + stats = self.Summary(total_size, unique_size, total_unique_chunks, total_chunks)._asdict() return stats def format_tuple(self): stats = self.stats() - for field in ['total_size', 'total_csize', 'unique_csize']: + for field in ['total_size', ]: stats[field] = format_file_size(stats[field], iec=self.iec) return self.Summary(**stats) - def chunks_stored_size(self): - return self.stats()['unique_csize'] - class LocalCache(CacheStatsMixin): """ @@ -679,8 +672,6 @@ class LocalCache(CacheStatsMixin): 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) @@ -718,39 +709,6 @@ class LocalCache(CacheStatsMixin): 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 = [] - 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 - - # 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 - 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 @@ -766,7 +724,6 @@ class LocalCache(CacheStatsMixin): 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): @@ -862,12 +819,7 @@ class LocalCache(CacheStatsMixin): chunk_idx = chunk_idx or ChunkIndex(usable=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: 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', @@ -951,10 +903,10 @@ class LocalCache(CacheStatsMixin): if size is None: raise ValueError("when giving compressed data for a new chunk, the uncompressed size must be given also") data = self.key.encrypt(id, chunk, compress=compress) - csize = len(data) + csize = 0 # len(data) self.repository.put(id, data, wait=wait) self.chunks.add(id, 1, size, csize) - stats.update(size, csize, not refcount) + stats.update(size) return ChunkListEntry(id, size, csize) def seen_chunk(self, id, size=None): @@ -970,7 +922,7 @@ class LocalCache(CacheStatsMixin): if not self.txn_active: self.begin_txn() count, _size, csize = self.chunks.incref(id) - stats.update(_size, csize, False, part=part) + stats.update(_size, part=part) return ChunkListEntry(id, _size, csize) def chunk_decref(self, id, stats, wait=True, part=False): @@ -980,9 +932,9 @@ class LocalCache(CacheStatsMixin): if count == 0: del self.chunks[id] self.repository.delete(id, wait=wait) - stats.update(-size, -csize, True, part=part) + stats.update(-size, part=part) else: - stats.update(-size, -csize, False, part=part) + stats.update(-size, part=part) def file_known_and_unchanged(self, hashed_path, path_hash, st): """ @@ -1122,7 +1074,7 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" csize = len(data) self.repository.put(id, data, wait=wait) self.chunks.add(id, 1, size, csize) - stats.update(size, csize, not refcount) + stats.update(size) return ChunkListEntry(id, size, csize) def seen_chunk(self, id, size=None): @@ -1144,7 +1096,7 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" # size or add_chunk); we can't add references to those (size=0 is invalid) and generally don't try to. size = _size or size assert size - stats.update(size, csize, False, part=part) + stats.update(size, part=part) return ChunkListEntry(id, size, csize) def chunk_decref(self, id, stats, wait=True, part=False): @@ -1154,9 +1106,9 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" if count == 0: del self.chunks[id] self.repository.delete(id, wait=wait) - stats.update(-size, -csize, True, part=part) + stats.update(-size, part=part) else: - stats.update(-size, -csize, False, part=part) + stats.update(-size, part=part) def commit(self): if not self._txn_active: diff --git a/src/borg/constants.py b/src/borg/constants.py index d2fa11919..de28cc2c1 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -12,7 +12,7 @@ ARCHIVE_KEYS = frozenset(['version', 'name', 'items', 'cmdline', 'hostname', 'us 'comment', 'chunker_params', 'recreate_cmdline', 'recreate_source_id', 'recreate_args', 'recreate_partial_chunks', # used in 1.1.0b1 .. b2 - 'size', 'csize', 'nfiles', 'size_parts', 'csize_parts', 'nfiles_parts', ]) + 'size', 'nfiles', 'size_parts', 'nfiles_parts', ]) # this is the set of keys that are always present in archives: REQUIRED_ARCHIVE_KEYS = frozenset(['version', 'name', 'items', 'cmdline', 'time', ]) diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 0e0ecc913..20a2a2fc5 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -375,9 +375,7 @@ cdef class ChunkIndex(IndexBase): assert refcount <= _MAX_VALUE, "invalid reference count" chunks += refcount unique_size += _le32toh(values[1]) - unique_csize += _le32toh(values[2]) size += _le32toh(values[1]) * _le32toh(values[0]) - csize += _le32toh(values[2]) * _le32toh(values[0]) return size, csize, unique_size, unique_csize, unique_chunks, chunks diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index e772f38c4..dc19590ce 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -698,9 +698,6 @@ class ItemFormatter(BaseFormatter): 'source': 'link target for symlinks (identical to linktarget)', 'hlid': 'hard link identity (same if hardlinking same fs object)', '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', 'xxh64': 'XXH64 checksum of this file (note: this is NOT a cryptographic hash!)', @@ -708,7 +705,7 @@ class ItemFormatter(BaseFormatter): } KEY_GROUPS = ( ('type', 'mode', 'uid', 'gid', 'user', 'group', 'path', 'bpath', 'source', 'linktarget', 'hlid', 'flags'), - ('size', 'csize', 'dsize', 'dcsize', 'num_chunks', 'unique_chunks'), + ('size', 'num_chunks', 'unique_chunks'), ('mtime', 'ctime', 'atime', 'isomtime', 'isoctime', 'isoatime'), tuple(sorted(hash_algorithms)), ('archiveid', 'archivename', 'extra'), @@ -716,7 +713,7 @@ class ItemFormatter(BaseFormatter): ) KEYS_REQUIRING_CACHE = ( - 'dsize', 'dcsize', 'unique_chunks', + 'unique_chunks', ) @classmethod @@ -774,9 +771,6 @@ class ItemFormatter(BaseFormatter): self.format_keys = {f[1] for f in Formatter().parse(format)} self.call_keys = { 'size': self.calculate_size, - 'csize': self.calculate_csize, - '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': partial(self.sum_unique_chunks_metadata, lambda chunk: 1), 'isomtime': partial(self.format_iso_time, 'mtime'), @@ -848,10 +842,6 @@ class ItemFormatter(BaseFormatter): # note: does not support hardlink slaves, they will be size 0 return item.get_size(compressed=False) - def calculate_csize(self, item): - # note: does not support hardlink slaves, they will be csize 0 - return item.get_size(compressed=True) - def hash_item(self, hash_function, item): if 'chunks' not in item: return "" diff --git a/src/borg/item.pyx b/src/borg/item.pyx index cd1a3544c..4fc8d6814 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -293,8 +293,9 @@ class Item(PropDict): :param from_chunks: If true, size is computed from chunks even if a precomputed value is available. :param consider_ids: Returns the size of the given ids only. """ - attr = 'csize' if compressed else 'size' - assert not (compressed and memorize), 'Item does not have a csize field.' + if compressed: + return 0 # try to live without csize + attr = 'size' assert not (consider_ids is not None and memorize), "Can't store size when considering only certain ids" try: if from_chunks or consider_ids is not None: diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index 9cdcf5046..fc67aa59e 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -19,47 +19,44 @@ from ..platform import uid2user, gid2group @pytest.fixture() def stats(): stats = Statistics() - stats.update(20, 10, unique=True) + stats.update(20) return stats def test_stats_basic(stats): assert stats.osize == 20 - assert stats.csize == stats.usize == 10 - stats.update(20, 10, unique=False) + stats.update(20) assert stats.osize == 40 - assert stats.csize == 20 - assert stats.usize == 10 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 ' + s = '20 B O 0 N ' buf = ' ' * (columns - len(s)) assert out.getvalue() == s + buf + "\r" out = StringIO() - stats.update(10**3, 0, unique=False) + stats.update(10 ** 3) stats.show_progress(item=Item(path='foo'), final=False, stream=out) - s = '1.02 kB O 10 B C 10 B D 0 N foo' + s = '1.02 kB O 0 N foo' buf = ' ' * (columns - len(s)) assert out.getvalue() == s + buf + "\r" out = StringIO() stats.show_progress(item=Item(path='foo'*40), final=False, stream=out) - s = '1.02 kB O 10 B C 10 B D 0 N foofoofoofoofoofoofoofo...oofoofoofoofoofoofoofoofoo' + s = '1.02 kB O 0 N foofoofoofoofoofoofoofoofoofoo...foofoofoofoofoofoofoofoofoofoofoo' buf = ' ' * (columns - len(s)) assert out.getvalue() == s + buf + "\r" def test_stats_format(stats): assert str(stats) == """\ -This archive: 20 B 10 B 10 B""" +This archive: 20 B""" s = f"{stats.osize_fmt}" assert s == "20 B" # kind of redundant, but id is variable so we can't match reliably - assert repr(stats) == f'' + assert repr(stats) == f'' def test_stats_progress_json(stats): @@ -73,8 +70,6 @@ def test_stats_progress_json(stats): assert result['finished'] is False assert result['path'] == 'foo' assert result['original_size'] == 20 - assert result['compressed_size'] == 10 - assert result['deduplicated_size'] == 10 assert result['nfiles'] == 0 # this counter gets updated elsewhere out = StringIO() @@ -85,8 +80,6 @@ def test_stats_progress_json(stats): assert result['finished'] is True # see #6570 assert 'path' not in result assert 'original_size' not in result - assert 'compressed_size' not in result - assert 'deduplicated_size' not in result assert 'nfiles' not in result diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index a4205ea76..4789f8e47 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1543,7 +1543,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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')) + assert all(key in stats for key in ('total_chunks', 'total_size', 'total_unique_chunks', 'unique_size')) info_archive = json.loads(self.cmd('info', '--json', self.repository_location + '::test')) assert info_repo['repository'] == info_archive['repository'] @@ -2363,12 +2363,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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} {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) + output = self.cmd('list', '--format', '{size} {path}{NL}', test_archive) + size, path = output.split("\n")[1].split(" ") + assert int(size) == 10000 def test_list_json(self): self.create_regular_file('file1', size=1024 * 80) @@ -2441,69 +2438,6 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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 - else: - contents = os.urandom(size) - self.create_regular_file('file', contents=contents) - self.cmd('init', '--encryption=none', self.repository_location) - archive = self.repository_location + '::test' - self.cmd('create', '-C', compression, archive, 'input') - output = self.cmd('list', '--format', '{size} {csize} {path}{NL}', archive) - size, csize, path = output.split("\n")[1].split(" ") - return int(size), int(csize) - - def test_compression_none_compressible(self): - size, csize = self._get_sizes('none', compressible=True) - assert csize == size + 3 - - def test_compression_none_uncompressible(self): - size, csize = self._get_sizes('none', compressible=False) - assert csize == size + 3 - - def test_compression_zlib_compressible(self): - size, csize = self._get_sizes('zlib', compressible=True) - assert csize < size * 0.1 - assert csize == 37 - - def test_compression_zlib_uncompressible(self): - size, csize = self._get_sizes('zlib', compressible=False) - assert csize >= size - - def test_compression_auto_compressible(self): - size, csize = self._get_sizes('auto,zlib', compressible=True) - assert csize < size * 0.1 - assert csize == 37 # same as compression 'zlib' - - def test_compression_auto_uncompressible(self): - size, csize = self._get_sizes('auto,zlib', compressible=False) - assert csize == size + 3 # same as compression 'none' - - def test_compression_lz4_compressible(self): - size, csize = self._get_sizes('lz4', compressible=True) - assert csize < size * 0.1 - - def test_compression_lz4_uncompressible(self): - size, csize = self._get_sizes('lz4', compressible=False) - assert csize == size + 3 # same as compression 'none' - - def test_compression_lzma_compressible(self): - size, csize = self._get_sizes('lzma', compressible=True) - assert csize < size * 0.1 - - def test_compression_lzma_uncompressible(self): - size, csize = self._get_sizes('lzma', compressible=False) - assert csize == size + 3 # same as compression 'none' - - def test_compression_zstd_compressible(self): - size, csize = self._get_sizes('zstd', compressible=True) - assert csize < size * 0.1 - - def test_compression_zstd_uncompressible(self): - size, csize = self._get_sizes('zstd', compressible=False) - assert csize == size + 3 # same as compression 'none' - def test_change_passphrase(self): self.cmd('init', '--encryption=repokey', self.repository_location) os.environ['BORG_NEW_PASSPHRASE'] = 'newpassphrase' @@ -2951,13 +2885,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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] + for id, (refcount, size, _) in correct_chunks.iteritems(): + o_refcount, o_size, _ = 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(): + for id, (refcount, size, _) in original_chunks.iteritems(): assert id in seen def test_check_cache(self): @@ -3051,15 +2984,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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}') - size, csize, sha256_before = file_list.split(' ') - assert int(csize) >= int(size) # >= due to metadata overhead + '--format', '{size} {sha256}') + size, sha256_before = file_list.split(' ') 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}') - size, csize, sha256_after = file_list.split(' ') - assert int(csize) < int(size) + '--format', '{size} {sha256}') + size, sha256_after = file_list.split(' ') assert sha256_before == sha256_after def test_recreate_timestamp(self): diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 89a793a97..f16706ca9 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -147,11 +147,9 @@ class HashIndexTestCase(BaseTestCase): idx[H(2)] = 2, 2000, 200 idx[H(3)] = 3, 3000, 300 - size, csize, unique_size, unique_csize, unique_chunks, chunks = idx.summarize() + size, _, unique_size, _, unique_chunks, chunks = idx.summarize() assert size == 1000 + 2 * 2000 + 3 * 3000 - assert csize == 100 + 2 * 200 + 3 * 300 assert unique_size == 1000 + 2000 + 3000 - assert unique_csize == 100 + 200 + 300 assert chunks == 1 + 2 + 3 assert unique_chunks == 3 diff --git a/src/borg/testsuite/item.py b/src/borg/testsuite/item.py index a0c1d9d13..dc171afa5 100644 --- a/src/borg/testsuite/item.py +++ b/src/borg/testsuite/item.py @@ -143,13 +143,11 @@ def test_unknown_property(): def test_item_file_size(): item = Item(mode=0o100666, chunks=[ - ChunkListEntry(csize=1, size=1000, id=None), - ChunkListEntry(csize=1, size=2000, id=None), + ChunkListEntry(csize=0, size=1000, id=None), + ChunkListEntry(csize=0, 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 + assert item.get_size(compressed=True) == 0 # no csize any more item.get_size(memorize=True) assert item.size == 3000 From ace5957524e0b613f11b6e393bac0db784c9268e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 10 Jun 2022 20:36:58 +0200 Subject: [PATCH 030/160] remove csize from item.chunks elements --- src/borg/archive.py | 16 +++++----- src/borg/archiver.py | 4 +-- src/borg/cache.py | 12 ++++---- src/borg/cache_sync/cache_sync.c | 14 --------- src/borg/cache_sync/unpack.h | 26 +++++----------- src/borg/fuse.py | 4 +-- src/borg/hashindex.pyx | 10 ------ src/borg/item.pyx | 8 ++--- src/borg/testsuite/archive.py | 2 +- src/borg/testsuite/cache.py | 52 ++++++++++++++++---------------- src/borg/testsuite/item.py | 5 ++- 11 files changed, 58 insertions(+), 95 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index b2aabfee6..8992f42d3 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -359,7 +359,7 @@ class CacheChunkBuffer(ChunkBuffer): self.stats = stats def write_chunk(self, chunk): - id_, _, _ = self.cache.add_chunk(self.key.id_hash(chunk), 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_ @@ -921,7 +921,7 @@ Utilization of max. archive size: {csize_max:.0%} item = Item(internal_dict=item) if 'chunks' in item: part = not self.consider_part_files and 'part' in item - for chunk_id, size, _ in item.chunks: + for chunk_id, size in item.chunks: chunk_decref(chunk_id, stats, part=part) except (TypeError, ValueError): # if items metadata spans multiple chunks and one chunk got dropped somehow, @@ -1279,7 +1279,7 @@ class FilesystemObjectProcessors: # this needs to be done early, so that part files also get the patched mode. item.mode = stat.S_IFREG | stat.S_IMODE(item.mode) if 'chunks' in item: # create_helper might have put chunks from a previous hardlink there - [cache.chunk_incref(id_, self.stats) for id_, _, _ in item.chunks] + [cache.chunk_incref(id_, self.stats) for id_, _ in item.chunks] else: # normal case, no "2nd+" hardlink if not is_special_file: hashed_path = safe_encode(os.path.join(self.cwd, path)) @@ -1761,7 +1761,7 @@ class ArchiveChecker: has_chunks_healthy = False chunks_healthy = chunks_current for chunk_current, chunk_healthy in zip(chunks_current, chunks_healthy): - chunk_id, size, csize = chunk_healthy + chunk_id, size = chunk_healthy if chunk_id not in self.chunks: # a chunk of the healthy list is missing if chunk_current == chunk_healthy: @@ -1775,7 +1775,7 @@ class ArchiveChecker: logger.info('{}: {}: Previously missing file chunk is still missing (Byte {}-{}, Chunk {}). ' 'It has an all-zero replacement chunk already.'.format( archive_name, item.path, offset, offset + size, bin_to_hex(chunk_id))) - chunk_id, size, csize = chunk_current + chunk_id, size = chunk_current if chunk_id in self.chunks: add_reference(chunk_id, size) else: @@ -1794,7 +1794,7 @@ class ArchiveChecker: archive_name, item.path, offset, offset + size, bin_to_hex(chunk_id))) add_reference(chunk_id, size) mark_as_possibly_superseded(chunk_current[0]) # maybe orphaned the all-zero replacement chunk - chunk_list.append([chunk_id, size, csize]) # list-typed element as chunks_healthy is list-of-lists + chunk_list.append([chunk_id, size]) # list-typed element as chunks_healthy is list-of-lists offset += size if chunks_replaced and not has_chunks_healthy: # if this is first repair, remember the correct chunk IDs, so we can maybe heal the file later @@ -2046,7 +2046,7 @@ class ArchiveRecreater: def process_chunks(self, archive, target, item): if not self.recompress and not target.recreate_rechunkify: - for chunk_id, size, csize in item.chunks: + for chunk_id, size in item.chunks: self.cache.chunk_incref(chunk_id, target.stats) return item.chunks chunk_iterator = self.iter_chunks(archive, target, list(item.chunks)) @@ -2070,7 +2070,7 @@ class ArchiveRecreater: return chunk_entry def iter_chunks(self, archive, target, chunks): - chunk_iterator = archive.pipeline.fetch_many([chunk_id for chunk_id, _, _ in 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) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index b46bfa3f3..dfc571cf4 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -424,7 +424,7 @@ class Archiver: for item in other_archive.iter_items(): if 'chunks' in item: chunks = [] - for chunk_id, size, _ in item.chunks: + for chunk_id, size in item.chunks: refcount = cache.seen_chunk(chunk_id, size) if refcount == 0: # target repo does not yet have this chunk if not dry_run: @@ -1331,7 +1331,7 @@ class Archiver: """ 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], + chunk_iterator = archive.pipeline.fetch_many([chunk_id for chunk_id, _ in item.chunks], is_preloaded=True) if pi: info = [remove_surrogates(item.path)] diff --git a/src/borg/cache.py b/src/borg/cache.py index 413e83bb4..59b50837a 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -907,7 +907,7 @@ class LocalCache(CacheStatsMixin): self.repository.put(id, data, wait=wait) self.chunks.add(id, 1, size, csize) stats.update(size) - return ChunkListEntry(id, size, csize) + return ChunkListEntry(id, size) def seen_chunk(self, id, size=None): refcount, stored_size, _ = self.chunks.get(id, ChunkIndexEntry(0, None, None)) @@ -921,14 +921,14 @@ class LocalCache(CacheStatsMixin): def chunk_incref(self, id, stats, size=None, part=False): if not self.txn_active: self.begin_txn() - count, _size, csize = self.chunks.incref(id) + count, _size, _ = self.chunks.incref(id) stats.update(_size, part=part) - return ChunkListEntry(id, _size, csize) + return ChunkListEntry(id, _size) def chunk_decref(self, id, stats, wait=True, part=False): if not self.txn_active: self.begin_txn() - count, size, csize = self.chunks.decref(id) + count, size, _ = self.chunks.decref(id) if count == 0: del self.chunks[id] self.repository.delete(id, wait=wait) @@ -1075,7 +1075,7 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" self.repository.put(id, data, wait=wait) self.chunks.add(id, 1, size, csize) stats.update(size) - return ChunkListEntry(id, size, csize) + return ChunkListEntry(id, size) def seen_chunk(self, id, size=None): if not self._txn_active: @@ -1097,7 +1097,7 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" size = _size or size assert size stats.update(size, part=part) - return ChunkListEntry(id, size, csize) + return ChunkListEntry(id, size) def chunk_decref(self, id, stats, wait=True, part=False): if not self._txn_active: diff --git a/src/borg/cache_sync/cache_sync.c b/src/borg/cache_sync/cache_sync.c index 1a2cfb0f2..300faa7a3 100644 --- a/src/borg/cache_sync/cache_sync.c +++ b/src/borg/cache_sync/cache_sync.c @@ -39,10 +39,8 @@ cache_sync_init(HashIndex *chunks) /* needs to be set only once */ ctx->ctx.user.chunks = chunks; ctx->ctx.user.parts.size = 0; - ctx->ctx.user.parts.csize = 0; ctx->ctx.user.parts.num_files = 0; ctx->ctx.user.totals.size = 0; - ctx->ctx.user.totals.csize = 0; ctx->ctx.user.totals.num_files = 0; ctx->buf = NULL; ctx->head = 0; @@ -91,18 +89,6 @@ cache_sync_size_parts(const CacheSyncCtx *ctx) return ctx->ctx.user.parts.size; } -static uint64_t -cache_sync_csize_totals(const CacheSyncCtx *ctx) -{ - return ctx->ctx.user.totals.csize; -} - -static uint64_t -cache_sync_csize_parts(const CacheSyncCtx *ctx) -{ - return ctx->ctx.user.parts.csize; -} - /** * 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 bba566419..73f86d7bd 100644 --- a/src/borg/cache_sync/unpack.h +++ b/src/borg/cache_sync/unpack.h @@ -86,14 +86,12 @@ typedef struct unpack_user { /* * processing ChunkListEntry tuple: - * expect_key, expect_size, expect_csize, expect_entry_end + * expect_key, expect_size, 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, @@ -103,23 +101,22 @@ typedef struct unpack_user { /* collect values here for current chunklist entry */ struct { unsigned char key[32]; - uint32_t csize; uint32_t size; } current; /* summing up chunks sizes here within a single item */ struct { - uint64_t size, csize; + uint64_t size; } item; /* total sizes and files count coming from all files */ struct { - uint64_t size, csize, num_files; + uint64_t size, num_files; } totals; /* total sizes and files count coming from part files */ struct { - uint64_t size, csize, num_files; + uint64_t size, num_files; } parts; } unpack_user; @@ -147,10 +144,6 @@ static inline int unpack_callback_uint64(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: @@ -239,7 +232,7 @@ static inline int unpack_callback_array(unpack_user* u, unsigned int n) case expect_entry_begin_or_chunks_end: /* b'chunks': [ ( * ^ */ - if(n != 3) { + if(n != 2) { SET_LAST_ERROR("Invalid chunk list entry length"); return -1; } @@ -283,18 +276,16 @@ static inline int unpack_callback_array_end(unpack_user* u) refcount += 1; cache_entry[0] = _htole32(MIN(refcount, _MAX_VALUE)); } else { - /* refcount, size, csize */ + /* refcount, size */ cache_values[0] = _htole32(1); cache_values[1] = _htole32(u->current.size); - cache_values[2] = _htole32(u->current.csize); + cache_values[2] = _htole32(0); /* fake csize for now */ if(!hashindex_set(u->chunks, u->current.key, cache_values)) { SET_LAST_ERROR("hashindex_set failed"); return -1; } } u->item.size += u->current.size; - u->item.csize += u->current.csize; - u->expect = expect_entry_begin_or_chunks_end; break; case expect_entry_begin_or_chunks_end: @@ -330,7 +321,6 @@ static inline int unpack_callback_map(unpack_user* u, unsigned int n) u->part = 0; u->has_chunks = 0; u->item.size = 0; - u->item.csize = 0; } if(u->inside_chunks) { @@ -372,11 +362,9 @@ static inline int unpack_callback_map_end(unpack_user* u) if(u->part) { u->parts.num_files += 1; u->parts.size += u->item.size; - u->parts.csize += u->item.csize; } u->totals.num_files += 1; u->totals.size += u->item.size; - u->totals.csize += u->item.csize; } } return 0; diff --git a/src/borg/fuse.py b/src/borg/fuse.py index b81f37f1f..5c9f8b935 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -377,7 +377,7 @@ class FuseBackend: file_id = blake2b_128(path) current_version, previous_id = self.versions_index.get(file_id, (0, None)) - contents_id = blake2b_128(b''.join(chunk_id for chunk_id, _, _ in item.chunks)) + contents_id = blake2b_128(b''.join(chunk_id for chunk_id, _ in item.chunks)) if contents_id != previous_id: current_version += 1 @@ -658,7 +658,7 @@ class FuseOperations(llfuse.Operations, FuseBackend): chunks = item.chunks # note: using index iteration to avoid frequently copying big (sub)lists by slicing for idx in range(chunk_no, len(chunks)): - id, s, csize = chunks[idx] + id, s = chunks[idx] if s < offset: offset -= s chunk_offset += s diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 20a2a2fc5..6ef3a6d55 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -49,8 +49,6 @@ cdef extern from "cache_sync/cache_sync.c": uint64_t cache_sync_num_files_parts(const CacheSyncCtx *ctx) uint64_t cache_sync_size_totals(const CacheSyncCtx *ctx) uint64_t cache_sync_size_parts(const CacheSyncCtx *ctx) - uint64_t cache_sync_csize_totals(const CacheSyncCtx *ctx) - uint64_t cache_sync_csize_parts(const CacheSyncCtx *ctx) int cache_sync_feed(CacheSyncCtx *ctx, void *data, uint32_t length) void cache_sync_free(CacheSyncCtx *ctx) @@ -544,11 +542,3 @@ cdef class CacheSynchronizer: @property def size_parts(self): return cache_sync_size_parts(self.sync) - - @property - def csize_totals(self): - return cache_sync_csize_totals(self.sync) - - @property - def csize_parts(self): - return cache_sync_csize_parts(self.sync) diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 4fc8d6814..3897d2d4e 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -61,10 +61,10 @@ def fix_list_of_chunkentries(v): chunks = [] for ce in v: assert isinstance(ce, (tuple, list)) - assert len(ce) == 3 # id, size, csize + assert len(ce) in (2, 3) # id, size[, csize] assert isinstance(ce[1], int) - assert isinstance(ce[2], int) - ce_fixed = [want_bytes(ce[0]), ce[1], ce[2]] # list! + assert len(ce) == 2 or isinstance(ce[2], int) + ce_fixed = [want_bytes(ce[0]), ce[1]] # list! id, size only, drop csize chunks.append(ce_fixed) # create a list of lists return chunks @@ -227,7 +227,7 @@ class PropDict: return property(_get, _set, _del, doc=doc) -ChunkListEntry = namedtuple('ChunkListEntry', 'id size csize') +ChunkListEntry = namedtuple('ChunkListEntry', 'id size') class Item(PropDict): """ diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index fc67aa59e..38c4aa3d0 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -95,7 +95,7 @@ class MockCache: def add_chunk(self, id, chunk, stats=None, wait=True): self.objects[id] = chunk - return id, len(chunk), len(chunk) + return id, len(chunk) class ArchiveTimestampTestCase(BaseTestCase): diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py index c67fbdf1b..d476492d8 100644 --- a/src/borg/testsuite/cache.py +++ b/src/borg/testsuite/cache.py @@ -43,14 +43,14 @@ class TestCacheSynchronizer: 'bar': 5678, 'user': 'chunks', 'chunks': [ - (H(1), 1, 2), - (H(2), 2, 3), + (H(1), 1), + (H(2), 2), ] }) sync.feed(data) assert len(index) == 2 - assert index[H(1)] == (1, 1, 2) - assert index[H(2)] == (1, 2, 3) + assert index[H(1)] == (1, 1, 0) + assert index[H(2)] == (1, 2, 0) def test_multiple(self, index, sync): data = packb({ @@ -59,8 +59,8 @@ class TestCacheSynchronizer: 'bar': 5678, 'user': 'chunks', 'chunks': [ - (H(1), 1, 2), - (H(2), 2, 3), + (H(1), 1), + (H(2), 2), ] }) data += packb({ @@ -78,8 +78,8 @@ class TestCacheSynchronizer: 'chunks': '123456', }, 'chunks': [ - (H(1), 1, 2), - (H(2), 2, 3), + (H(1), 1), + (H(2), 2), ], 'stuff': [ (1, 2, 3), @@ -87,12 +87,12 @@ class TestCacheSynchronizer: }) data += packb({ 'chunks': [ - (H(3), 1, 2), + (H(3), 1), ], }) data += packb({ 'chunks': [ - (H(1), 1, 2), + (H(1), 1), ], }) @@ -103,9 +103,9 @@ class TestCacheSynchronizer: 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) + assert index[H(1)] == (3, 1, 0) + assert index[H(2)] == (2, 2, 0) + assert index[H(3)] == (1, 1, 0) @pytest.mark.parametrize('elem,error', ( ({1: 2}, 'Unexpected object: map'), @@ -121,7 +121,7 @@ class TestCacheSynchronizer: @pytest.mark.parametrize('structure', ( lambda elem: {'chunks': elem}, lambda elem: {'chunks': [elem]}, - lambda elem: {'chunks': [(elem, 1, 2)]}, + lambda elem: {'chunks': [(elem, 1)]}, )) def test_corrupted(self, sync, structure, elem, error): packed = packb(structure(elem)) @@ -135,11 +135,11 @@ class TestCacheSynchronizer: @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'), + ({'chunks': [(bytes(32), )]}, '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'), + ({'chunks': [(1, 2)]}, 'Unexpected object: integer'), + ({'chunks': [(1, bytes(32))]}, 'Unexpected object: integer'), + ({'chunks': [(bytes(32), 1.0)]}, 'Unexpected object: double'), )) def test_corrupted_ancillary(self, index, sync, data, error): packed = packb(data) @@ -173,7 +173,7 @@ class TestCacheSynchronizer: sync = CacheSynchronizer(index) data = packb({ 'chunks': [ - (H(0), 1, 2), + (H(0), 1), ] }) with pytest.raises(ValueError) as excinfo: @@ -185,7 +185,7 @@ class TestCacheSynchronizer: sync = CacheSynchronizer(index) data = packb({ 'chunks': [ - (H(0), 1, 2), + (H(0), 1), ] }) sync.feed(data) @@ -196,7 +196,7 @@ class TestCacheSynchronizer: sync = CacheSynchronizer(index) data = packb({ 'chunks': [ - (H(0), 1, 2), + (H(0), 1), ] }) sync.feed(data) @@ -244,7 +244,7 @@ class TestAdHocCache: 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) + assert cache.add_chunk(H(1), b'5678', Statistics()) == (H(1), 4) def test_deletes_chunks_during_lifetime(self, cache, repository): """E.g. checkpoint archives""" @@ -270,10 +270,10 @@ class TestAdHocCache: 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) + assert cache.add_chunk(H(3), b'5678', Statistics()) == (H(3), 4) + assert cache.chunk_incref(H(3), Statistics()) == (H(3), 4) 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) + assert cache.add_chunk(H(1), b'5678', Statistics()) == (H(1), 4) + assert cache.chunk_incref(H(1), Statistics()) == (H(1), 4) diff --git a/src/borg/testsuite/item.py b/src/borg/testsuite/item.py index dc171afa5..47f8e1021 100644 --- a/src/borg/testsuite/item.py +++ b/src/borg/testsuite/item.py @@ -143,11 +143,10 @@ def test_unknown_property(): def test_item_file_size(): item = Item(mode=0o100666, chunks=[ - ChunkListEntry(csize=0, size=1000, id=None), - ChunkListEntry(csize=0, size=2000, id=None), + ChunkListEntry(size=1000, id=None), + ChunkListEntry(size=2000, id=None), ]) assert item.get_size() == 3000 - assert item.get_size(compressed=True) == 0 # no csize any more item.get_size(memorize=True) assert item.size == 3000 From b726aa5665eaf57965bbcff99a9fe3e1a912a6a3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 10 Jun 2022 20:54:57 +0200 Subject: [PATCH 031/160] remove csize support from get_size --- src/borg/archive.py | 2 +- src/borg/helpers/parseformat.py | 2 +- src/borg/item.pyx | 9 ++------- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 8992f42d3..710a51d31 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1805,7 +1805,7 @@ class ArchiveChecker: item.chunks = chunk_list if 'size' in item: item_size = item.size - item_chunks_size = item.get_size(compressed=False, from_chunks=True) + item_chunks_size = item.get_size(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( diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index dc19590ce..9b57baebb 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -840,7 +840,7 @@ class ItemFormatter(BaseFormatter): def calculate_size(self, item): # note: does not support hardlink slaves, they will be size 0 - return item.get_size(compressed=False) + return item.get_size() 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 3897d2d4e..0d4c6ba70 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -284,17 +284,14 @@ class Item(PropDict): part = PropDict._make_property('part', int) - def get_size(self, memorize=False, compressed=False, from_chunks=False, consider_ids=None): + def get_size(self, *, memorize=False, from_chunks=False, consider_ids=None): """ - Determine the (uncompressed or compressed) size of this item. + Determine the uncompressed size of this item. :param memorize: Whether the computed size value will be stored into the item. - :param compressed: Whether the compressed or uncompressed size will be returned. :param from_chunks: If true, size is computed from chunks even if a precomputed value is available. :param consider_ids: Returns the size of the given ids only. """ - if compressed: - return 0 # try to live without csize attr = 'size' assert not (consider_ids is not None and memorize), "Can't store size when considering only certain ids" try: @@ -497,10 +494,8 @@ class ArchiveItem(PropDict): 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 size = PropDict._make_property('size', int) - csize = PropDict._make_property('csize', int) nfiles = PropDict._make_property('nfiles', int) size_parts = PropDict._make_property('size_parts', int) - csize_parts = PropDict._make_property('csize_parts', int) nfiles_parts = PropDict._make_property('nfiles_parts', int) def update_internal(self, d): From 0211948cacad12b3bdc8e6ab2f1ae2b093fc7e90 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 10 Jun 2022 20:58:33 +0200 Subject: [PATCH 032/160] remove csize from summarize return tuple --- src/borg/cache.py | 2 +- src/borg/hashindex.pyx | 4 ++-- src/borg/testsuite/hashindex.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 59b50837a..618a61925 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -423,7 +423,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def stats(self): from .archive import Archive # XXX: this should really be moved down to `hashindex.pyx` - total_size, _, unique_size, _, total_unique_chunks, total_chunks = self.chunks.summarize() + total_size, unique_size, total_unique_chunks, total_chunks = self.chunks.summarize() # the above values have the problem that they do not consider part files, # thus the total_size might be too high (chunks referenced # by the part files AND by the complete file). diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 6ef3a6d55..9720a0a49 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -358,7 +358,7 @@ cdef class ChunkIndex(IndexBase): return iter def summarize(self): - cdef uint64_t size = 0, csize = 0, unique_size = 0, unique_csize = 0, chunks = 0, unique_chunks = 0 + cdef uint64_t size = 0, unique_size = 0, chunks = 0, unique_chunks = 0 cdef uint32_t *values cdef uint32_t refcount cdef unsigned char *key = NULL @@ -375,7 +375,7 @@ cdef class ChunkIndex(IndexBase): unique_size += _le32toh(values[1]) size += _le32toh(values[1]) * _le32toh(values[0]) - return size, csize, unique_size, unique_csize, unique_chunks, chunks + return size, unique_size, unique_chunks, chunks def stats_against(self, ChunkIndex master_index): """ diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index f16706ca9..a96c76a6a 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -147,7 +147,7 @@ class HashIndexTestCase(BaseTestCase): idx[H(2)] = 2, 2000, 200 idx[H(3)] = 3, 3000, 300 - size, _, unique_size, _, unique_chunks, chunks = idx.summarize() + size, unique_size, unique_chunks, chunks = idx.summarize() assert size == 1000 + 2 * 2000 + 3 * 3000 assert unique_size == 1000 + 2000 + 3000 assert chunks == 1 + 2 + 3 From b82a39c3b3e204d23407fb73e3c2813adbf49e78 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 10 Jun 2022 21:07:09 +0200 Subject: [PATCH 033/160] remove csize from stats_against() --- src/borg/hashindex.pyx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 9720a0a49..1b978c66f 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -387,9 +387,9 @@ cdef class ChunkIndex(IndexBase): 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. + size, unique_size, unique_chunks, chunks. """ - cdef uint64_t size = 0, csize = 0, unique_size = 0, unique_csize = 0, chunks = 0, unique_chunks = 0 + cdef uint64_t size = 0, unique_size = 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 @@ -406,18 +406,15 @@ cdef class ChunkIndex(IndexBase): 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 + return size, unique_size, unique_chunks, chunks def add(self, key, refs, size, csize): assert len(key) == self.key_size From 2c1f7951c45942a3355533b7bc545d3b87e448f9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 10 Jun 2022 22:23:27 +0200 Subject: [PATCH 034/160] remove csize from ChunkIndexEntry --- src/borg/archive.py | 14 ++-- src/borg/cache.py | 24 ++++--- src/borg/cache_sync/unpack.h | 1 - src/borg/hashindex.pyx | 45 ++++--------- src/borg/testsuite/archiver.py | 6 +- src/borg/testsuite/cache.py | 16 ++--- src/borg/testsuite/hashindex.py | 111 ++++++++++++++++---------------- 7 files changed, 100 insertions(+), 117 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 710a51d31..6fe1db9f3 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -483,6 +483,10 @@ class Archive: def duration_from_meta(self): return format_timedelta(self.ts_end - self.ts) + def _archive_csize(self): + cdata = self.repository.get(self.id) + return len(cdata) + def info(self): if self.create: stats = self.stats @@ -500,7 +504,7 @@ class Archive: 'duration': (end - start).total_seconds(), 'stats': stats.as_dict(), 'limits': { - 'max_archive_size': self.cache.chunks[self.id].csize / MAX_DATA_SIZE, + 'max_archive_size': self._archive_csize() / MAX_DATA_SIZE, }, } if self.create: @@ -529,7 +533,7 @@ Utilization of max. archive size: {csize_max:.0%} self, start=OutputTimestamp(self.start.replace(tzinfo=timezone.utc)), end=OutputTimestamp(self.end.replace(tzinfo=timezone.utc)), - csize_max=self.cache.chunks[self.id].csize / MAX_DATA_SIZE, + csize_max=self._archive_csize() / MAX_DATA_SIZE, location=self.repository._location.canonical_path() ) @@ -1561,7 +1565,7 @@ class ArchiveChecker: if not result: break marker = result[-1] - init_entry = ChunkIndexEntry(refcount=0, size=0, csize=0) + init_entry = ChunkIndexEntry(refcount=0, size=0) for id_ in result: self.chunks[id_] = init_entry @@ -1718,7 +1722,7 @@ class ArchiveChecker: self.chunks.pop(Manifest.MANIFEST_ID, None) def mark_as_possibly_superseded(id_): - if self.chunks.get(id_, ChunkIndexEntry(0, 0, 0)).refcount == 0: + if self.chunks.get(id_, ChunkIndexEntry(0, 0)).refcount == 0: self.possibly_superseded.add(id_) def add_callback(chunk): @@ -1732,7 +1736,7 @@ class ArchiveChecker: self.chunks.incref(id_) except KeyError: assert cdata is not None - self.chunks[id_] = ChunkIndexEntry(refcount=1, size=size, csize=0) # was: csize=csize + self.chunks[id_] = ChunkIndexEntry(refcount=1, size=size) if self.repair: self.repository.put(id_, cdata) diff --git a/src/borg/cache.py b/src/borg/cache.py index 618a61925..ac5c90744 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -713,13 +713,13 @@ class LocalCache(CacheStatsMixin): nonlocal processed_item_metadata_bytes nonlocal processed_item_metadata_chunks csize, data = decrypted_repository.get(archive_id) - chunk_idx.add(archive_id, 1, len(data), csize) + chunk_idx.add(archive_id, 1, len(data)) archive = ArchiveItem(internal_dict=msgpack.unpackb(data)) if archive.version not in (1, 2): # legacy raise Exception('Unknown archive metadata version') 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) + chunk_idx.add(item_id, 1, len(data)) processed_item_metadata_bytes += len(data) processed_item_metadata_chunks += 1 sync.feed(data) @@ -903,14 +903,13 @@ class LocalCache(CacheStatsMixin): if size is None: raise ValueError("when giving compressed data for a new chunk, the uncompressed size must be given also") data = self.key.encrypt(id, chunk, compress=compress) - csize = 0 # len(data) self.repository.put(id, data, wait=wait) - self.chunks.add(id, 1, size, csize) + self.chunks.add(id, 1, size) stats.update(size) return ChunkListEntry(id, size) def seen_chunk(self, id, size=None): - refcount, stored_size, _ = self.chunks.get(id, ChunkIndexEntry(0, None, None)) + refcount, stored_size = self.chunks.get(id, ChunkIndexEntry(0, None)) if size is not None and stored_size is not None and size != stored_size: # we already have a chunk with that id, but different size. # this is either a hash collision (unlikely) or corruption or a bug. @@ -921,14 +920,14 @@ class LocalCache(CacheStatsMixin): def chunk_incref(self, id, stats, size=None, part=False): if not self.txn_active: self.begin_txn() - count, _size, _ = self.chunks.incref(id) + count, _size = self.chunks.incref(id) stats.update(_size, part=part) return ChunkListEntry(id, _size) def chunk_decref(self, id, stats, wait=True, part=False): if not self.txn_active: self.begin_txn() - count, size, _ = self.chunks.decref(id) + count, size = self.chunks.decref(id) if count == 0: del self.chunks[id] self.repository.delete(id, wait=wait) @@ -1071,16 +1070,15 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" if refcount: return self.chunk_incref(id, stats, size=size) data = self.key.encrypt(id, chunk, compress=compress) - csize = len(data) self.repository.put(id, data, wait=wait) - self.chunks.add(id, 1, size, csize) + self.chunks.add(id, 1, size) stats.update(size) return ChunkListEntry(id, size) def seen_chunk(self, id, size=None): if not self._txn_active: self.begin_txn() - entry = self.chunks.get(id, ChunkIndexEntry(0, None, None)) + entry = self.chunks.get(id, ChunkIndexEntry(0, 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. @@ -1091,7 +1089,7 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" def chunk_incref(self, id, stats, size=None, part=False): if not self._txn_active: self.begin_txn() - count, _size, csize = self.chunks.incref(id) + count, _size = self.chunks.incref(id) # 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. size = _size or size @@ -1102,7 +1100,7 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" def chunk_decref(self, id, stats, wait=True, part=False): if not self._txn_active: self.begin_txn() - count, size, csize = self.chunks.decref(id) + count, size = self.chunks.decref(id) if count == 0: del self.chunks[id] self.repository.delete(id, wait=wait) @@ -1142,7 +1140,7 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" # 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) + init_entry = ChunkIndexEntry(refcount=ChunkIndex.MAX_VALUE, size=0) for id_ in result: self.chunks[id_] = init_entry assert len(self.chunks) == num_chunks diff --git a/src/borg/cache_sync/unpack.h b/src/borg/cache_sync/unpack.h index 73f86d7bd..a04e81147 100644 --- a/src/borg/cache_sync/unpack.h +++ b/src/borg/cache_sync/unpack.h @@ -279,7 +279,6 @@ static inline int unpack_callback_array_end(unpack_user* u) /* refcount, size */ cache_values[0] = _htole32(1); cache_values[1] = _htole32(u->current.size); - cache_values[2] = _htole32(0); /* fake csize for now */ if(!hashindex_set(u->chunks, u->current.key, cache_values)) { SET_LAST_ERROR("hashindex_set failed"); return -1; diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 1b978c66f..4b9fe316c 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -269,12 +269,12 @@ cdef class NSKeyIterator: return (self.key)[:self.key_size], (segment, _le32toh(value[1])) -ChunkIndexEntry = namedtuple('ChunkIndexEntry', 'refcount size csize') +ChunkIndexEntry = namedtuple('ChunkIndexEntry', 'refcount size') cdef class ChunkIndex(IndexBase): """ - Mapping of 32 byte keys to (refcount, size, csize), which are all 32-bit unsigned. + Mapping of 32 byte keys to (refcount, size), which are all 32-bit unsigned. The reference count cannot overflow. If an overflow would occur, the refcount is fixed to MAX_VALUE and will neither increase nor decrease by incref(), decref() @@ -289,7 +289,7 @@ cdef class ChunkIndex(IndexBase): Assigning refcounts in this reserved range is an invalid operation and raises AssertionError. """ - value_size = 12 + value_size = 8 def __getitem__(self, key): assert len(key) == self.key_size @@ -298,16 +298,15 @@ cdef class ChunkIndex(IndexBase): raise KeyError(key) cdef uint32_t refcount = _le32toh(data[0]) assert refcount <= _MAX_VALUE, "invalid reference count" - return ChunkIndexEntry(refcount, _le32toh(data[1]), _le32toh(data[2])) + return ChunkIndexEntry(refcount, _le32toh(data[1])) def __setitem__(self, key, value): assert len(key) == self.key_size - cdef uint32_t[3] data + cdef uint32_t[2] data cdef uint32_t refcount = value[0] assert refcount <= _MAX_VALUE, "invalid reference count" data[0] = _htole32(refcount) data[1] = _htole32(value[1]) - data[2] = _htole32(value[2]) if not hashindex_set(self.index, key, data): raise Exception('hashindex_set failed') @@ -319,7 +318,7 @@ cdef class ChunkIndex(IndexBase): return data != NULL def incref(self, key): - """Increase refcount for 'key', return (refcount, size, csize)""" + """Increase refcount for 'key', return (refcount, size)""" assert len(key) == self.key_size data = hashindex_get(self.index, key) if not data: @@ -329,10 +328,10 @@ cdef class ChunkIndex(IndexBase): if refcount != _MAX_VALUE: refcount += 1 data[0] = _htole32(refcount) - return refcount, _le32toh(data[1]), _le32toh(data[2]) + return refcount, _le32toh(data[1]) def decref(self, key): - """Decrease refcount for 'key', return (refcount, size, csize)""" + """Decrease refcount for 'key', return (refcount, size)""" assert len(key) == self.key_size data = hashindex_get(self.index, key) if not data: @@ -343,7 +342,7 @@ cdef class ChunkIndex(IndexBase): if refcount != _MAX_VALUE: refcount -= 1 data[0] = _htole32(refcount) - return refcount, _le32toh(data[1]), _le32toh(data[2]) + return refcount, _le32toh(data[1]) def iteritems(self, marker=None): cdef const unsigned char *key @@ -390,7 +389,7 @@ cdef class ChunkIndex(IndexBase): size, unique_size, unique_chunks, chunks. """ cdef uint64_t size = 0, unique_size = 0, chunks = 0, unique_chunks = 0 - cdef uint32_t our_refcount, chunk_size, chunk_csize + cdef uint32_t our_refcount, chunk_size cdef const uint32_t *our_values cdef const uint32_t *master_values cdef const unsigned char *key = NULL @@ -416,12 +415,11 @@ cdef class ChunkIndex(IndexBase): return size, unique_size, unique_chunks, chunks - def add(self, key, refs, size, csize): + def add(self, key, refs, size): assert len(key) == self.key_size - cdef uint32_t[3] data + cdef uint32_t[2] data data[0] = _htole32(refs) data[1] = _htole32(size) - data[2] = _htole32(csize) self._add( key, data) cdef _add(self, unsigned char *key, uint32_t *data): @@ -435,7 +433,6 @@ cdef class ChunkIndex(IndexBase): result64 = refcount1 + refcount2 values[0] = _htole32(min(result64, _MAX_VALUE)) values[1] = data[1] - values[2] = data[2] else: if not hashindex_set(self.index, key, data): raise Exception('hashindex_set failed') @@ -449,22 +446,6 @@ cdef class ChunkIndex(IndexBase): break self._add(key, (key + self.key_size)) - def zero_csize_ids(self): - cdef unsigned char *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 @@ -491,7 +472,7 @@ cdef class ChunkKeyIterator: cdef uint32_t *value = (self.key + self.key_size) 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])) + return (self.key)[:self.key_size], ChunkIndexEntry(refcount, _le32toh(value[1])) cdef Py_buffer ro_buffer(object data) except *: diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 4789f8e47..e033d6cf7 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2885,12 +2885,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): correct_chunks = cache.chunks assert original_chunks is not correct_chunks seen = set() - for id, (refcount, size, _) in correct_chunks.iteritems(): - o_refcount, o_size, _ = original_chunks[id] + for id, (refcount, size) in correct_chunks.iteritems(): + o_refcount, o_size = original_chunks[id] assert refcount == o_refcount assert size == o_size seen.add(id) - for id, (refcount, size, _) in original_chunks.iteritems(): + for id, (refcount, size) in original_chunks.iteritems(): assert id in seen def test_check_cache(self): diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py index d476492d8..faff9f2b7 100644 --- a/src/borg/testsuite/cache.py +++ b/src/borg/testsuite/cache.py @@ -49,8 +49,8 @@ class TestCacheSynchronizer: }) sync.feed(data) assert len(index) == 2 - assert index[H(1)] == (1, 1, 0) - assert index[H(2)] == (1, 2, 0) + assert index[H(1)] == (1, 1) + assert index[H(2)] == (1, 2) def test_multiple(self, index, sync): data = packb({ @@ -103,9 +103,9 @@ class TestCacheSynchronizer: sync.feed(part2) sync.feed(part3) assert len(index) == 3 - assert index[H(1)] == (3, 1, 0) - assert index[H(2)] == (2, 2, 0) - assert index[H(3)] == (1, 1, 0) + assert index[H(1)] == (3, 1) + assert index[H(2)] == (2, 2) + assert index[H(3)] == (1, 1) @pytest.mark.parametrize('elem,error', ( ({1: 2}, 'Unexpected object: map'), @@ -189,7 +189,7 @@ class TestCacheSynchronizer: ] }) sync.feed(data) - assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234, 5678) + assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234) def test_refcount_one_below_max_value(self): index = self.make_index_with_refcount(ChunkIndex.MAX_VALUE - 1) @@ -201,9 +201,9 @@ class TestCacheSynchronizer: }) sync.feed(data) # Incremented to maximum - assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234, 5678) + assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234) sync.feed(data) - assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234, 5678) + assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234) class TestAdHocCache: diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index a96c76a6a..7a3b53e33 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -91,8 +91,8 @@ class HashIndexTestCase(BaseTestCase): '85f72b036c692c8266e4f51ccf0cff2147204282b5e316ae508d30a448d88fef') def test_chunkindex(self): - self._generic_test(ChunkIndex, lambda x: (x, x, x), - 'c83fdf33755fc37879285f2ecfc5d1f63b97577494902126b6fb6f3e4d852488') + self._generic_test(ChunkIndex, lambda x: (x, x), + '85f72b036c692c8266e4f51ccf0cff2147204282b5e316ae508d30a448d88fef') def test_resize(self): n = 2000 # Must be >= MIN_BUCKETS @@ -126,26 +126,26 @@ class HashIndexTestCase(BaseTestCase): def test_chunkindex_merge(self): idx1 = ChunkIndex() - idx1[H(1)] = 1, 100, 100 - idx1[H(2)] = 2, 200, 200 - idx1[H(3)] = 3, 300, 300 + idx1[H(1)] = 1, 100 + idx1[H(2)] = 2, 200 + idx1[H(3)] = 3, 300 # no H(4) entry idx2 = ChunkIndex() - idx2[H(1)] = 4, 100, 100 - idx2[H(2)] = 5, 200, 200 + idx2[H(1)] = 4, 100 + idx2[H(2)] = 5, 200 # no H(3) entry - idx2[H(4)] = 6, 400, 400 + idx2[H(4)] = 6, 400 idx1.merge(idx2) - assert idx1[H(1)] == (5, 100, 100) - assert idx1[H(2)] == (7, 200, 200) - assert idx1[H(3)] == (3, 300, 300) - assert idx1[H(4)] == (6, 400, 400) + assert idx1[H(1)] == (5, 100) + assert idx1[H(2)] == (7, 200) + assert idx1[H(3)] == (3, 300) + assert idx1[H(4)] == (6, 400) def test_chunkindex_summarize(self): idx = ChunkIndex() - idx[H(1)] = 1, 1000, 100 - idx[H(2)] = 2, 2000, 200 - idx[H(3)] = 3, 3000, 300 + idx[H(1)] = 1, 1000 + idx[H(2)] = 2, 2000 + idx[H(3)] = 3, 3000 size, unique_size, unique_chunks, chunks = idx.summarize() assert size == 1000 + 2 * 2000 + 3 * 3000 @@ -167,14 +167,14 @@ class HashIndexExtraTestCase(BaseTestCase): 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) + index[key] = (i, i) for i, key in enumerate(to_delete_keys): - index[key] = (i, i, i) + index[key] = (i, i) for key in to_delete_keys: del index[key] for i, key in enumerate(keys): - assert index[key] == (i, i, i) + assert index[key] == (i, i) for key in to_delete_keys: assert index.get(key) is None @@ -188,12 +188,12 @@ class HashIndexExtraTestCase(BaseTestCase): class HashIndexSizeTestCase(BaseTestCase): def test_size_on_disk(self): idx = ChunkIndex() - assert idx.size() == 18 + 1031 * (32 + 3 * 4) + assert idx.size() == 18 + 1031 * (32 + 2 * 4) def test_size_on_disk_accurate(self): idx = ChunkIndex() for i in range(1234): - idx[H(i)] = i, i**2, i**3 + idx[H(i)] = i, i**2 with tempfile.NamedTemporaryFile() as file: idx.write(file.name) size = os.path.getsize(file.name) @@ -203,7 +203,7 @@ class HashIndexSizeTestCase(BaseTestCase): class HashIndexRefcountingTestCase(BaseTestCase): def test_chunkindex_limit(self): idx = ChunkIndex() - idx[H(1)] = ChunkIndex.MAX_VALUE - 1, 1, 2 + idx[H(1)] = ChunkIndex.MAX_VALUE - 1, 1 # 5 is arbitrary, any number of incref/decrefs shouldn't move it once it's limited for i in range(5): @@ -217,9 +217,9 @@ class HashIndexRefcountingTestCase(BaseTestCase): def _merge(self, refcounta, refcountb): def merge(refcount1, refcount2): idx1 = ChunkIndex() - idx1[H(1)] = refcount1, 1, 2 + idx1[H(1)] = refcount1, 1 idx2 = ChunkIndex() - idx2[H(1)] = refcount2, 1, 2 + idx2[H(1)] = refcount2, 1 idx1.merge(idx2) refcount, *_ = idx1[H(1)] return refcount @@ -251,44 +251,44 @@ class HashIndexRefcountingTestCase(BaseTestCase): def test_chunkindex_add(self): idx1 = ChunkIndex() - idx1.add(H(1), 5, 6, 7) - assert idx1[H(1)] == (5, 6, 7) - idx1.add(H(1), 1, 2, 3) - assert idx1[H(1)] == (6, 2, 3) + idx1.add(H(1), 5, 6) + assert idx1[H(1)] == (5, 6) + idx1.add(H(1), 1, 2) + assert idx1[H(1)] == (6, 2) def test_incref_limit(self): idx1 = ChunkIndex() - idx1[H(1)] = (ChunkIndex.MAX_VALUE, 6, 7) + idx1[H(1)] = ChunkIndex.MAX_VALUE, 6 idx1.incref(H(1)) refcount, *_ = idx1[H(1)] assert refcount == ChunkIndex.MAX_VALUE def test_decref_limit(self): idx1 = ChunkIndex() - idx1[H(1)] = ChunkIndex.MAX_VALUE, 6, 7 + idx1[H(1)] = ChunkIndex.MAX_VALUE, 6 idx1.decref(H(1)) refcount, *_ = idx1[H(1)] assert refcount == ChunkIndex.MAX_VALUE def test_decref_zero(self): idx1 = ChunkIndex() - idx1[H(1)] = 0, 0, 0 + idx1[H(1)] = 0, 0 with self.assert_raises(AssertionError): idx1.decref(H(1)) def test_incref_decref(self): idx1 = ChunkIndex() - idx1.add(H(1), 5, 6, 7) - assert idx1[H(1)] == (5, 6, 7) + idx1.add(H(1), 5, 6) + assert idx1[H(1)] == (5, 6) idx1.incref(H(1)) - assert idx1[H(1)] == (6, 6, 7) + assert idx1[H(1)] == (6, 6) idx1.decref(H(1)) - assert idx1[H(1)] == (5, 6, 7) + assert idx1[H(1)] == (5, 6) def test_setitem_raises(self): idx1 = ChunkIndex() with self.assert_raises(AssertionError): - idx1[H(1)] = ChunkIndex.MAX_VALUE + 1, 0, 0 + idx1[H(1)] = ChunkIndex.MAX_VALUE + 1, 0 def test_keyerror(self): idx = ChunkIndex() @@ -299,14 +299,15 @@ class HashIndexRefcountingTestCase(BaseTestCase): with self.assert_raises(KeyError): idx[H(1)] with self.assert_raises(OverflowError): - idx.add(H(1), -1, 0, 0) + idx.add(H(1), -1, 0) class HashIndexDataTestCase(BaseTestCase): - # This bytestring was created with 1.0-maint at c2f9533 - HASHINDEX = b'eJzt0L0NgmAUhtHLT0LDEI6AuAEhMVYmVnSuYefC7AB3Aj9KNedJbnfyFne6P67P27w0EdG1Eac+Cm1ZybAsy7Isy7Isy7Isy7I' \ - b'sy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7Isy7LsL9nhc+cqTZ' \ - b'3XlO2Ys++Du5fX+l1/YFmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVn2/+0O2rYccw==' + # This bytestring was created with borg2-pre 2022-06-10 + HASHINDEX = b'eJzt0LEJg1AYhdE/JqBjOEJMNhBBrAQrO9ewc+HsoG+CPMsEz1cfbnHbceqXoZvvEVE+IuoqMu2pnOE4' \ + b'juM4juM4juM4juM4juM4juM4juM4juM4juM4juM4juM4juM4juM4juM4juM4juM4juM4juM4juM4juM4' \ + b'juM4juM4juM4jruie36vuSVT5N0rzW0n9t7r5z9+4TiO4ziO4ziO4ziO4ziO4ziO4ziO4ziO4ziO4ziO' \ + b'4ziO4ziO4ziO4ziO4ziO437LHbSVHGw=' def _serialize_hashindex(self, idx): with tempfile.TemporaryDirectory() as tempdir: @@ -330,23 +331,23 @@ class HashIndexDataTestCase(BaseTestCase): def test_identical_creation(self): idx1 = ChunkIndex() - idx1[H(1)] = 1, 2, 3 - idx1[H(2)] = 2**31 - 1, 0, 0 - idx1[H(3)] = 4294962296, 0, 0 # 4294962296 is -5000 interpreted as an uint32_t + idx1[H(1)] = 1, 2 + idx1[H(2)] = 2**31 - 1, 0 + idx1[H(3)] = 4294962296, 0 # 4294962296 is -5000 interpreted as an uint32_t serialized = self._serialize_hashindex(idx1) assert self._unpack(serialized) == self._unpack(self.HASHINDEX) def test_read_known_good(self): idx1 = self._deserialize_hashindex(self.HASHINDEX) - assert idx1[H(1)] == (1, 2, 3) - assert idx1[H(2)] == (2**31 - 1, 0, 0) - assert idx1[H(3)] == (4294962296, 0, 0) + assert idx1[H(1)] == (1, 2) + assert idx1[H(2)] == (2**31 - 1, 0) + assert idx1[H(3)] == (4294962296, 0) idx2 = ChunkIndex() - idx2[H(3)] = 2**32 - 123456, 6, 7 + idx2[H(3)] = 2**32 - 123456, 6 idx1.merge(idx2) - assert idx1[H(3)] == (ChunkIndex.MAX_VALUE, 6, 7) + assert idx1[H(3)] == (ChunkIndex.MAX_VALUE, 6) class HashIndexIntegrityTestCase(HashIndexDataTestCase): @@ -497,16 +498,16 @@ class HashIndexCompactTestCase(HashIndexDataTestCase): 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[H(1)] = 1, 100 + idx1[H(2)] = 2, 200 + idx1[H(3)] = 3, 300 idx1.compact() - assert idx1.size() == 18 + 3 * (32 + 3 * 4) + assert idx1.size() == 18 + 3 * (32 + 2 * 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) + assert master[H(1)] == (1, 100) + assert master[H(2)] == (2, 200) + assert master[H(3)] == (3, 300) class NSIndexTestCase(BaseTestCase): From 1fd571a4d05318f187dbd8490ec2bd9d88c338b8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 10 Jun 2022 22:39:07 +0200 Subject: [PATCH 035/160] fix comments --- src/borg/archiver.py | 2 +- src/borg/cache.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index dfc571cf4..8a308e330 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -443,7 +443,7 @@ class Archiver: chunks.append(chunk_entry) present_size += size if not dry_run: - item.chunks = chunks # overwrite! IDs and sizes are same, csizes are likely different + item.chunks = chunks # TODO: overwrite? IDs and sizes are same. archive.stats.nfiles += 1 if not dry_run: archive.add_item(upgrade_item(item)) diff --git a/src/borg/cache.py b/src/borg/cache.py index ac5c90744..a964fc605 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -1015,7 +1015,7 @@ class AdHocCache(CacheStatsMixin): 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 + during the current AdHocCache lifetime won't have correct size set (0 bytes) and will have an infinite reference count (MAX_VALUE). """ From 19dfbe5c5c5a7fe4cd529580bd03d94b4c075bfb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 11 Jun 2022 22:29:43 +0200 Subject: [PATCH 036/160] compute the deduplicated size before compression so we do not need csize for it. --- src/borg/archive.py | 23 +++++++++++++++++------ src/borg/archiver.py | 6 +++--- src/borg/cache.py | 20 ++++++++++---------- src/borg/testsuite/archive.py | 18 ++++++++++-------- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 6fe1db9f3..945dc5a8a 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -58,38 +58,45 @@ class Statistics: def __init__(self, output_json=False, iec=False): self.output_json = output_json self.iec = iec - self.osize = self.nfiles = 0 - self.osize_parts = self.nfiles_parts = 0 + self.osize = self.usize = self.nfiles = 0 + self.osize_parts = self.usize_parts = self.nfiles_parts = 0 self.last_progress = 0 # timestamp when last progress was shown - def update(self, size, part=False): + def update(self, size, unique, part=False): if not part: self.osize += size + if unique: + self.usize += size else: self.osize_parts += size + if unique: + self.usize_parts += size def __add__(self, other): if not isinstance(other, Statistics): raise TypeError('can only add Statistics objects') stats = Statistics(self.output_json, self.iec) stats.osize = self.osize + other.osize + stats.usize = self.usize + other.usize stats.nfiles = self.nfiles + other.nfiles stats.osize_parts = self.osize_parts + other.osize_parts + stats.usize_parts = self.usize_parts + other.usize_parts stats.nfiles_parts = self.nfiles_parts + other.nfiles_parts return stats - summary = "{label:15} {stats.osize_fmt:>20s}" + summary = "{label:15} {stats.osize_fmt:>20s} {stats.usize_fmt:>20s}" def __str__(self): return self.summary.format(stats=self, label='This archive:') def __repr__(self): - return "<{cls} object at {hash:#x} ({self.osize})>".format( + return "<{cls} object at {hash:#x} ({self.osize}, {self.usize})>".format( cls=type(self).__name__, hash=id(self), self=self) def as_dict(self): return { 'original_size': FileSize(self.osize, iec=self.iec), + 'deduplicated_size': FileSize(self.usize, iec=self.iec), 'nfiles': self.nfiles, } @@ -114,6 +121,10 @@ class Statistics: def osize_fmt(self): return format_file_size(self.osize, iec=self.iec) + @property + def usize_fmt(self): + return format_file_size(self.usize, iec=self.iec) + def show_progress(self, item=None, final=False, stream=None, dt=None): now = time.monotonic() if dt is None or now - self.last_progress > dt: @@ -134,7 +145,7 @@ class Statistics: else: columns, lines = get_terminal_size() if not final: - msg = '{0.osize_fmt} O {0.nfiles} N '.format(self) + msg = '{0.osize_fmt} O {0.usize_fmt} U {0.nfiles} N '.format(self) path = remove_surrogates(item.path) if item else '' space = columns - swidth(msg) if space < 12: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 8a308e330..3814d1262 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -99,7 +99,7 @@ except BaseException: assert EXIT_ERROR == 2, "EXIT_ERROR is not 2, as expected - fix assert AND exception handler right above this line." -STATS_HEADER = " Original size" +STATS_HEADER = " Original size Deduplicated size" PURE_PYTHON_MSGPACK_WARNING = "Using a pure-python msgpack! This will result in lower performance." @@ -1797,8 +1797,8 @@ class Archiver: Command line: {command_line} Utilization of maximum supported archive size: {limits[max_archive_size]:.0%} ------------------------------------------------------------------------------ - Original size - This archive: {stats[original_size]:>20s} + Original size Deduplicated size + This archive: {stats[original_size]:>20s} {stats[deduplicated_size]:>20s} {cache} """).strip().format(cache=cache, **info)) if self.exit_code: diff --git a/src/borg/cache.py b/src/borg/cache.py index a964fc605..65cbe7712 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -406,7 +406,7 @@ class Cache: class CacheStatsMixin: str_format = """\ -All archives: {0.total_size:>20s} +All archives: {0.total_size:>20s} {0.unique_size:>20s} Unique chunks Total chunks Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" @@ -440,7 +440,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def format_tuple(self): stats = self.stats() - for field in ['total_size', ]: + for field in ['total_size', 'unique_size']: stats[field] = format_file_size(stats[field], iec=self.iec) return self.Summary(**stats) @@ -905,7 +905,7 @@ class LocalCache(CacheStatsMixin): data = self.key.encrypt(id, chunk, compress=compress) self.repository.put(id, data, wait=wait) self.chunks.add(id, 1, size) - stats.update(size) + stats.update(size, not refcount) return ChunkListEntry(id, size) def seen_chunk(self, id, size=None): @@ -921,7 +921,7 @@ class LocalCache(CacheStatsMixin): if not self.txn_active: self.begin_txn() count, _size = self.chunks.incref(id) - stats.update(_size, part=part) + stats.update(_size, False, part=part) return ChunkListEntry(id, _size) def chunk_decref(self, id, stats, wait=True, part=False): @@ -931,9 +931,9 @@ class LocalCache(CacheStatsMixin): if count == 0: del self.chunks[id] self.repository.delete(id, wait=wait) - stats.update(-size, part=part) + stats.update(-size, True, part=part) else: - stats.update(-size, part=part) + stats.update(-size, False, part=part) def file_known_and_unchanged(self, hashed_path, path_hash, st): """ @@ -1072,7 +1072,7 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" data = self.key.encrypt(id, chunk, compress=compress) self.repository.put(id, data, wait=wait) self.chunks.add(id, 1, size) - stats.update(size) + stats.update(size, not refcount) return ChunkListEntry(id, size) def seen_chunk(self, id, size=None): @@ -1094,7 +1094,7 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" # size or add_chunk); we can't add references to those (size=0 is invalid) and generally don't try to. size = _size or size assert size - stats.update(size, part=part) + stats.update(size, False, part=part) return ChunkListEntry(id, size) def chunk_decref(self, id, stats, wait=True, part=False): @@ -1104,9 +1104,9 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" if count == 0: del self.chunks[id] self.repository.delete(id, wait=wait) - stats.update(-size, part=part) + stats.update(-size, True, part=part) else: - stats.update(-size, part=part) + stats.update(-size, False, part=part) def commit(self): if not self._txn_active: diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index 38c4aa3d0..6bfb933ab 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -19,44 +19,46 @@ from ..platform import uid2user, gid2group @pytest.fixture() def stats(): stats = Statistics() - stats.update(20) + stats.update(20, unique=True) return stats def test_stats_basic(stats): assert stats.osize == 20 - stats.update(20) + assert stats.usize == 20 + stats.update(20, unique=False) assert stats.osize == 40 + assert stats.usize == 20 def tests_stats_progress(stats, monkeypatch, columns=80): monkeypatch.setenv('COLUMNS', str(columns)) out = StringIO() stats.show_progress(stream=out) - s = '20 B O 0 N ' + s = '20 B O 20 B U 0 N ' buf = ' ' * (columns - len(s)) assert out.getvalue() == s + buf + "\r" out = StringIO() - stats.update(10 ** 3) + stats.update(10 ** 3, unique=False) stats.show_progress(item=Item(path='foo'), final=False, stream=out) - s = '1.02 kB O 0 N foo' + s = '1.02 kB O 20 B U 0 N foo' buf = ' ' * (columns - len(s)) assert out.getvalue() == s + buf + "\r" out = StringIO() stats.show_progress(item=Item(path='foo'*40), final=False, stream=out) - s = '1.02 kB O 0 N foofoofoofoofoofoofoofoofoofoo...foofoofoofoofoofoofoofoofoofoofoo' + s = '1.02 kB O 20 B U 0 N foofoofoofoofoofoofoofoofo...foofoofoofoofoofoofoofoofoofoo' buf = ' ' * (columns - len(s)) assert out.getvalue() == s + buf + "\r" def test_stats_format(stats): assert str(stats) == """\ -This archive: 20 B""" +This archive: 20 B 20 B""" s = f"{stats.osize_fmt}" assert s == "20 B" # kind of redundant, but id is variable so we can't match reliably - assert repr(stats) == f'' + assert repr(stats) == f'' def test_stats_progress_json(stats): From 49adb7715763b8e46c0fc4d0c418175248d2bea7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 10 Jun 2022 22:27:11 +0200 Subject: [PATCH 037/160] calc_stats: deduplicated size now, was deduplicated csize also: remove pre12_meta cache --- src/borg/archive.py | 33 +++++++++++++++++++++++---------- src/borg/cache.py | 12 ------------ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 945dc5a8a..f9dd2e486 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -629,18 +629,31 @@ Utilization of max. archive size: {csize_max:.0%} self.cache.commit() def calc_stats(self, cache, want_unique=True): - # caching wrapper around _calc_stats which is rather slow for archives made with borg < 1.2 - have_borg12_meta = self.metadata.get('nfiles') is not None - try: - stats = Statistics.from_raw_dict(**cache.pre12_meta[self.fpr]) - except KeyError: # not in pre12_meta cache - stats = self._calc_stats(cache, want_unique=want_unique) - if not have_borg12_meta: - cache.pre12_meta[self.fpr] = stats.as_raw_dict() - return stats + if not want_unique: + unique_size = 0 + else: + def add(id): + entry = cache.chunks[id] + archive_index.add(id, 1, entry.size) + + archive_index = ChunkIndex() + sync = CacheSynchronizer(archive_index) + add(self.id) + # we must escape any % char in the archive name, because we use it in a format string, see #6500 + arch_name_escd = self.name.replace('%', '%%') + pi = ProgressIndicatorPercent(total=len(self.metadata.items), + msg='Calculating statistics for archive %s ... %%3.0f%%%%' % arch_name_escd, + msgid='archive.calc_stats') + 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) + sync.feed(data) + unique_size = archive_index.stats_against(cache.chunks)[1] + pi.finish() - def _calc_stats(self, cache, want_unique=True): stats = Statistics(iec=self.iec) + stats.usize = unique_size # the part files use same chunks as the full file stats.nfiles = self.metadata.nfiles stats.osize = self.metadata.size if self.consider_part_files: diff --git a/src/borg/cache.py b/src/borg/cache.py index 65cbe7712..784910673 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -413,7 +413,6 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def __init__(self, iec=False): self.iec = iec - self.pre12_meta = {} # here we cache archive metadata for borg < 1.2 def __str__(self): return self.str_format.format(self.format_tuple()) @@ -511,8 +510,6 @@ class LocalCache(CacheStatsMixin): os.makedirs(os.path.join(self.path, 'chunks.archive.d')) with SaveFile(os.path.join(self.path, files_cache_name()), binary=True): pass # empty file - with SaveFile(os.path.join(self.path, 'pre12-meta'), binary=False) as fd: - json.dump(self.pre12_meta, fd, indent=4) def _do_open(self): self.cache_config.load() @@ -523,11 +520,6 @@ class LocalCache(CacheStatsMixin): self.files = None else: self._read_files() - try: - with open(os.path.join(self.path, 'pre12-meta')) as fd: - self.pre12_meta = json.load(fd) - except (FileNotFoundError, json.JSONDecodeError): - pass def open(self): if not os.path.isdir(self.path): @@ -536,9 +528,6 @@ class LocalCache(CacheStatsMixin): self.rollback() def close(self): - # save the pre12_meta cache in any case - with open(os.path.join(self.path, 'pre12-meta'), 'w') as fd: - json.dump(self.pre12_meta, fd, indent=4) if self.cache_config is not None: self.cache_config.close() self.cache_config = None @@ -1037,7 +1026,6 @@ Chunk index: {0.total_unique_chunks:20d} unknown""" self.security_manager = SecurityManager(repository) self.security_manager.assert_secure(manifest, key, lock_wait=lock_wait) - self.pre12_meta = {} logger.warning('Note: --no-cache-sync is an experimental feature.') # Public API From 822aefc7b0b4c6a7a9ed30a24e60213a34ec9263 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 12 Jun 2022 00:17:20 +0200 Subject: [PATCH 038/160] re-add dsize placeholder --- src/borg/helpers/parseformat.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 9b57baebb..43e8d05eb 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -698,6 +698,7 @@ class ItemFormatter(BaseFormatter): 'source': 'link target for symlinks (identical to linktarget)', 'hlid': 'hard link identity (same if hardlinking same fs object)', 'extra': 'prepends {source} with " -> " for soft links and " link to " for hard links', + 'dsize': 'deduplicated size', 'num_chunks': 'number of chunks in this file', 'unique_chunks': 'number of unique chunks in this file', 'xxh64': 'XXH64 checksum of this file (note: this is NOT a cryptographic hash!)', @@ -705,7 +706,7 @@ class ItemFormatter(BaseFormatter): } KEY_GROUPS = ( ('type', 'mode', 'uid', 'gid', 'user', 'group', 'path', 'bpath', 'source', 'linktarget', 'hlid', 'flags'), - ('size', 'num_chunks', 'unique_chunks'), + ('size', 'dsize', 'num_chunks', 'unique_chunks'), ('mtime', 'ctime', 'atime', 'isomtime', 'isoctime', 'isoatime'), tuple(sorted(hash_algorithms)), ('archiveid', 'archivename', 'extra'), @@ -713,7 +714,7 @@ class ItemFormatter(BaseFormatter): ) KEYS_REQUIRING_CACHE = ( - 'unique_chunks', + 'dsize', 'unique_chunks', ) @classmethod @@ -771,6 +772,7 @@ class ItemFormatter(BaseFormatter): self.format_keys = {f[1] for f in Formatter().parse(format)} self.call_keys = { 'size': self.calculate_size, + 'dsize': partial(self.sum_unique_chunks_metadata, lambda chunk: chunk.size), 'num_chunks': self.calculate_num_chunks, 'unique_chunks': partial(self.sum_unique_chunks_metadata, lambda chunk: 1), 'isomtime': partial(self.format_iso_time, 'mtime'), From 75ad3b8e35669fa4e703c85ff55a08776245163c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 12 Jun 2022 17:50:12 +0200 Subject: [PATCH 039/160] remove csize expectation from do_transfer upgrade_item the size is already remove via Item._update_internal method. --- 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 3814d1262..1a46e4350 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -361,7 +361,7 @@ class Archiver: chunks, chunks_healthy = hlm.retrieve(id=hlid, default=(None, None)) if chunks is not None: item._dict['chunks'] = chunks - for chunk_id, _, _ in chunks: + for chunk_id, _ in chunks: cache.chunk_incref(chunk_id, archive.stats) if chunks_healthy is not None: item._dict['chunks_healthy'] = chunks From 1393e4f391cc33cc776921996d0180431d74cdca Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 12 Jun 2022 18:01:57 +0200 Subject: [PATCH 040/160] remove csize references from docs --- docs/internals/frontends.rst | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 8ff5ff475..d0ebed48c 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -269,12 +269,8 @@ stats 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 .. highlight: json @@ -285,10 +281,8 @@ Example *borg info* output:: "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 } }, @@ -424,10 +418,8 @@ The same archive with more information (``borg info --last 1 --json``):: "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 } }, @@ -495,26 +487,26 @@ added: removed: See **added** property. - + old_mode: If **type** == '*mode*', then **old_mode** and **new_mode** provide the mode and permissions changes. new_mode: See **old_mode** property. - + old_user: If **type** == '*owner*', then **old_user**, **new_user**, **old_group** and **new_group** provide the user and group ownership changes. old_group: See **old_user** property. - + new_user: See **old_user** property. - + new_group: See **old_user** property. - + Example (excerpt) of ``borg diff --json-lines``:: From 3ce3fbcdff94b640a3cf64c0d95ee0866f9f056e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 12 May 2022 01:45:50 +0200 Subject: [PATCH 041/160] repository index: add payload size (==csize) and flags to NSIndex entries This saves some segment file random IO that was previously necessary just to determine the size of to be deleted data. Keep old one as NSIndex1 for old borg compatibility. Choose NSIndex or NSIndex1 based on repo index layout from HashHeader. for an old repo index repo.get(key) returns segment, offset, None, None --- src/borg/hashindex.pyx | 99 +++++++++++++++++++++++++++++- src/borg/repository.py | 101 ++++++++++++++++++------------- src/borg/testsuite/hashindex.py | 48 +++++++-------- src/borg/testsuite/repository.py | 2 +- 4 files changed, 181 insertions(+), 69 deletions(-) diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 4b9fe316c..ace6c84fa 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -77,6 +77,20 @@ assert UINT32_MAX == 2**32-1 assert _MAX_VALUE % 2 == 1 +def hashindex_variant(fn): + """peek into an index file and find out what it is""" + with open(fn, 'rb') as f: + hh = f.read(18) # len(HashHeader) + magic = hh[0:8] + if magic == b'BORG_IDX': + key_size = hh[16] + value_size = hh[17] + return f'k{key_size}_v{value_size}' + if magic == b'12345678': # used by unit tests + return 'k32_v16' # just return the current variant + raise ValueError(f'unknown hashindex format, magic: {magic!r}') + + @cython.internal cdef class IndexBase: cdef HashIndex *index @@ -196,8 +210,87 @@ cdef class FuseVersionsIndex(IndexBase): return hashindex_get(self.index, key) != NULL +NSIndexEntry = namedtuple('NSIndexEntry', 'segment offset size extra') + + cdef class NSIndex(IndexBase): + value_size = 16 + + def __getitem__(self, key): + assert len(key) == self.key_size + data = hashindex_get(self.index, key) + if not data: + raise KeyError(key) + cdef uint32_t segment = _le32toh(data[0]) + assert segment <= _MAX_VALUE, "maximum number of segments reached" + return NSIndexEntry(segment, _le32toh(data[1]), _le32toh(data[2]), _le32toh(data[3])) + + def __setitem__(self, key, value): + assert len(key) == self.key_size + cdef uint32_t[4] data + cdef uint32_t segment = value[0] + assert segment <= _MAX_VALUE, "maximum number of segments reached" + data[0] = _htole32(segment) + data[1] = _htole32(value[1]) + data[2] = _htole32(value[2]) + data[3] = _htole32(value[3]) + if not hashindex_set(self.index, key, data): + raise Exception('hashindex_set failed') + + def __contains__(self, key): + cdef uint32_t segment + assert len(key) == self.key_size + data = hashindex_get(self.index, key) + if data != NULL: + segment = _le32toh(data[0]) + assert segment <= _MAX_VALUE, "maximum number of segments reached" + return data != NULL + + def iteritems(self, marker=None): + cdef const unsigned char *key + iter = NSKeyIterator(self.key_size) + iter.idx = self + iter.index = self.index + if marker: + key = hashindex_get(self.index, marker) + if marker is None: + raise IndexError + iter.key = key - self.key_size + return iter + + +cdef class NSKeyIterator: + cdef NSIndex idx + cdef HashIndex *index + cdef const unsigned char *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]) + assert segment <= _MAX_VALUE, "maximum number of segments reached" + return ((self.key)[:self.key_size], + NSIndexEntry(segment, _le32toh(value[1]), _le32toh(value[2]), _le32toh(value[3]))) + + +cdef class NSIndex1(IndexBase): # legacy borg 1.x + value_size = 8 def __getitem__(self, key): @@ -230,7 +323,7 @@ cdef class NSIndex(IndexBase): def iteritems(self, marker=None): cdef const unsigned char *key - iter = NSKeyIterator(self.key_size) + iter = NSKeyIterator1(self.key_size) iter.idx = self iter.index = self.index if marker: @@ -241,8 +334,8 @@ cdef class NSIndex(IndexBase): return iter -cdef class NSKeyIterator: - cdef NSIndex idx +cdef class NSKeyIterator1: # legacy borg 1.x + cdef NSIndex1 idx cdef HashIndex *index cdef const unsigned char *key cdef int key_size diff --git a/src/borg/repository.py b/src/borg/repository.py index 3fcc72aad..3e6ec2d29 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -13,7 +13,7 @@ from functools import partial from itertools import islice from .constants import * # NOQA -from .hashindex import NSIndex +from .hashindex import NSIndexEntry, NSIndex, NSIndex1, hashindex_variant from .helpers import Error, ErrorWithTraceback, IntegrityError, format_file_size, parse_file_size from .helpers import Location from .helpers import ProgressIndicatorPercent @@ -52,6 +52,18 @@ MAX_TAG_ID = 15 FreeSpace = partial(defaultdict, int) +def header_size(tag): + if tag == TAG_PUT2: + size = LoggedIO.HEADER_ID_SIZE + LoggedIO.ENTRY_HASH_SIZE + elif tag == TAG_PUT or tag == TAG_DELETE: + size = LoggedIO.HEADER_ID_SIZE + elif tag == TAG_COMMIT: + size = LoggedIO.header_fmt.size + else: + raise ValueError(f"unsupported tag: {tag!r}") + return size + + class Repository: """ Filesystem based transactional key value store @@ -525,10 +537,14 @@ class Repository: if transaction_id is None: return NSIndex() index_path = os.path.join(self.path, 'index.%d' % transaction_id) + variant = hashindex_variant(index_path) integrity_data = self._read_integrity(transaction_id, 'index') try: with IntegrityCheckedFile(index_path, write=False, integrity_data=integrity_data) as fd: - return NSIndex.read(fd) + if variant == 'k32_v16': + return NSIndex.read(fd) + if variant == 'k32_v8': # legacy + return NSIndex1.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) @@ -798,14 +814,14 @@ class Repository: if tag == TAG_COMMIT: continue in_index = self.index.get(key) - is_index_object = in_index == (segment, offset) + is_index_object = in_index and (in_index.segment, in_index.offset) == (segment, offset) if tag in (TAG_PUT2, TAG_PUT) and is_index_object: try: 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) - self.index[key] = new_segment, offset + self.index[key] = NSIndexEntry(new_segment, offset, len(data), in_index.extra) segments.setdefault(new_segment, 0) segments[new_segment] += 1 segments[segment] -= 1 @@ -821,10 +837,7 @@ class Repository: # do not remove entry with empty shadowed_segments list here, # it is needed for shadowed_put_exists code (see below)! pass - if tag == TAG_PUT2: - self.storage_quota_use -= len(data) + self.io.HEADER_ID_SIZE + self.io.ENTRY_HASH_SIZE - elif tag == TAG_PUT: - self.storage_quota_use -= len(data) + self.io.HEADER_ID_SIZE + self.storage_quota_use -= header_size(tag) + len(data) 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. @@ -919,27 +932,26 @@ class Repository: if tag in (TAG_PUT2, TAG_PUT): try: # If this PUT supersedes an older PUT, mark the old segment for compaction and count the free space - s, _ = self.index[key] - self.compact[s] += size - self.segments[s] -= 1 + in_index = self.index[key] + self.compact[in_index.segment] += header_size(tag) + size + self.segments[in_index.segment] -= 1 except KeyError: pass - self.index[key] = segment, offset + self.index[key] = NSIndexEntry(segment, offset, size, 0) self.segments[segment] += 1 - self.storage_quota_use += size # note: size already includes the put_header_fmt overhead + self.storage_quota_use += header_size(tag) + size elif tag == TAG_DELETE: try: # if the deleted PUT is not in the index, there is nothing to clean up - s, offset = self.index.pop(key) + in_index = self.index.pop(key) except KeyError: pass else: - if self.io.segment_exists(s): + if self.io.segment_exists(in_index.segment): # the old index is not necessarily valid for this transaction (e.g. compaction); if the segment # is already gone, then it was already compacted. - self.segments[s] -= 1 - size = self.io.read(s, offset, key, read_data=False) - self.compact[s] += size + self.segments[in_index.segment] -= 1 + self.compact[in_index.segment] += header_size(tag) + in_index.size elif tag == TAG_COMMIT: continue else: @@ -968,12 +980,13 @@ class Repository: self.compact[segment] = 0 for tag, key, offset, size in self.io.iter_objects(segment, read_data=False): if tag in (TAG_PUT2, TAG_PUT): - if self.index.get(key, (-1, -1)) != (segment, offset): + in_index = self.index.get(key) + if not in_index or (in_index.segment, in_index.offset) != (segment, offset): # This PUT is superseded later - self.compact[segment] += size + self.compact[segment] += header_size(tag) + size elif tag == TAG_DELETE: # The outcome of the DELETE has been recorded in the PUT branch already - self.compact[segment] += size + self.compact[segment] += header_size(tag) + size def check(self, repair=False, save_space=False, max_duration=0): """Check repository consistency @@ -1169,7 +1182,7 @@ class Repository: self.index = self.open_index(transaction_id) at_start = marker is None # smallest valid seg is 0, smallest valid offs is 8 - start_segment, start_offset = (0, 0) if at_start else self.index[marker] + start_segment, start_offset, _, _ = (0, 0, 0, 0) if at_start else self.index[marker] result = [] 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) @@ -1186,19 +1199,21 @@ class Repository: # also, for the next segment, we need to start at offset 0. start_offset = 0 continue - if tag in (TAG_PUT2, 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 + if tag in (TAG_PUT2, TAG_PUT): + in_index = self.index.get(id) + if in_index and (in_index.segment, in_index.offset) == (segment, offset): + # 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()) try: - segment, offset = self.index[id] - return self.io.read(segment, offset, id) + in_index = NSIndexEntry(*((self.index[id] + (None, None))[:4])) # legacy: no size/extra + return self.io.read(in_index.segment, in_index.offset, id, expected_size=in_index.size) except KeyError: raise self.ObjectNotFound(id, self.path) from None @@ -1215,7 +1230,7 @@ class Repository: if not self._active_txn: self.prepare_txn(self.get_transaction_id()) try: - segment, offset = self.index[id] + in_index = self.index[id] except KeyError: pass else: @@ -1223,12 +1238,12 @@ class Repository: # we do not want to update the shadow_index here, because # we know already that we will PUT to this id, so it will # be in the repo index (and we won't need it in the shadow_index). - self._delete(id, segment, offset, update_shadow_index=False) + self._delete(id, in_index.segment, in_index.offset, in_index.size, update_shadow_index=False) segment, offset = self.io.write_put(id, data) - self.storage_quota_use += len(data) + self.io.HEADER_ID_SIZE + self.io.ENTRY_HASH_SIZE + self.storage_quota_use += header_size(TAG_PUT2) + len(data) self.segments.setdefault(segment, 0) self.segments[segment] += 1 - self.index[id] = segment, offset + self.index[id] = NSIndexEntry(segment, offset, len(data), 0) 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)) @@ -1243,22 +1258,21 @@ class Repository: if not self._active_txn: self.prepare_txn(self.get_transaction_id()) try: - segment, offset = self.index.pop(id) + in_index = self.index.pop(id) except KeyError: raise self.ObjectNotFound(id, self.path) from None # if we get here, there is an object with this id in the repo, # we write a DEL here that shadows the respective PUT. # after the delete, the object is not in the repo index any more, # for the compaction code, we need to update the shadow_index in this case. - self._delete(id, segment, offset, update_shadow_index=True) + self._delete(id, in_index.segment, in_index.offset, in_index.size, update_shadow_index=True) - def _delete(self, id, segment, offset, *, update_shadow_index): + def _delete(self, id, segment, offset, size, *, update_shadow_index): # common code used by put and delete if update_shadow_index: 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 + self.compact[segment] += header_size(TAG_PUT2) + size segment, size = self.io.write_delete(id) self.compact[segment] += size self.segments.setdefault(segment, 0) @@ -1515,7 +1529,8 @@ class LoggedIO: if include_data: yield tag, key, offset, data else: - yield tag, key, offset, size + yield tag, key, offset, size - header_size(tag) # corresponds to len(data) + assert size >= 0 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 @@ -1580,7 +1595,7 @@ class LoggedIO: h.update(d) return h.digest() - def read(self, segment, offset, id, read_data=True): + def read(self, segment, offset, id, read_data=True, *, expected_size=None): """ Read entry from *segment* at *offset* with *id*. If read_data is False the size of the entry is returned instead. @@ -1596,7 +1611,11 @@ class LoggedIO: if id != key: raise IntegrityError('Invalid segment entry header, is not for wanted id [segment {}, offset {}]'.format( segment, offset)) - return data if read_data else size + data_size_from_header = size - header_size(tag) + if expected_size is not None and expected_size != data_size_from_header: + raise IntegrityError(f'size from repository index: {expected_size} != ' + f'size from entry header: {data_size_from_header}') + return data if read_data else data_size_from_header def _read(self, fd, header, segment, offset, acceptable_tags, read_data=True): """ diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 7a3b53e33..537151e07 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -87,8 +87,8 @@ class HashIndexTestCase(BaseTestCase): del idx def test_nsindex(self): - self._generic_test(NSIndex, lambda x: (x, x), - '85f72b036c692c8266e4f51ccf0cff2147204282b5e316ae508d30a448d88fef') + self._generic_test(NSIndex, lambda x: (x, x, x, x), + 'c9fe5878800d2a0691b667c665a00d4a186e204e891076d6b109016940742bed') def test_chunkindex(self): self._generic_test(ChunkIndex, lambda x: (x, x), @@ -102,7 +102,7 @@ class HashIndexTestCase(BaseTestCase): initial_size = os.path.getsize(filepath) self.assert_equal(len(idx), 0) for x in range(n): - idx[H(x)] = x, x + idx[H(x)] = x, x, x, x idx.write(filepath) assert initial_size < os.path.getsize(filepath) for x in range(n): @@ -114,7 +114,7 @@ class HashIndexTestCase(BaseTestCase): def test_iteritems(self): idx = NSIndex() for x in range(100): - idx[H(x)] = x, x + idx[H(x)] = x, x, x, x iterator = idx.iteritems() all = list(iterator) self.assert_equal(len(all), 100) @@ -514,9 +514,9 @@ class NSIndexTestCase(BaseTestCase): def test_nsindex_segment_limit(self): idx = NSIndex() with self.assert_raises(AssertionError): - idx[H(1)] = NSIndex.MAX_VALUE + 1, 0 + idx[H(1)] = NSIndex.MAX_VALUE + 1, 0, 0, 0 assert H(1) not in idx - idx[H(2)] = NSIndex.MAX_VALUE, 0 + idx[H(2)] = NSIndex.MAX_VALUE, 0, 0, 0 assert H(2) in idx @@ -531,38 +531,38 @@ class IndexCorruptionTestCase(BaseTestCase): from struct import pack - def HH(x, y): - # make some 32byte long thing that depends on x and y. - # same x will mean a collision in the hashtable as bucket index is computed from - # first 4 bytes. giving a specific x targets bucket index x. - # y is to create different keys and does not go into the bucket index calculation. - # so, same x + different y --> collision - return pack(' collision + return pack(' Date: Thu, 12 May 2022 14:07:44 +0200 Subject: [PATCH 042/160] repository: sync write file in get_fd this fixes a strange test failure that did not happen until now: it could not read the MAGIC bytes from a (quite new) segment file, it just returned the empty string. maybe its appearance is related to the removed I/O calls. --- src/borg/repository.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/borg/repository.py b/src/borg/repository.py index 3e6ec2d29..1de071dae 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -1462,6 +1462,9 @@ class LoggedIO: del self.fds[k] clean_old() + if self._write_fd is not None: + # without this, we have a test failure now + self._write_fd.sync() try: ts, fd = self.fds[segment] except KeyError: From e5ea01611560a2113842769ce3e1d08a2e86b2bb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 9 Jun 2022 20:15:25 +0200 Subject: [PATCH 043/160] repository: set/query flags, iteration over flagged items (NSIndex) use this to query or set/clear flags in the "extra" word. also: remove direct access to the "extra" word, adapt tests. --- src/borg/hashindex.pyx | 52 ++++++++++++---- src/borg/repository.py | 10 +-- src/borg/selftest.py | 2 +- src/borg/testsuite/hashindex.py | 102 +++++++++++++++++++++++++------ src/borg/testsuite/repository.py | 2 +- 5 files changed, 130 insertions(+), 38 deletions(-) diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index ace6c84fa..3cb8d5a4d 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -210,7 +210,7 @@ cdef class FuseVersionsIndex(IndexBase): return hashindex_get(self.index, key) != NULL -NSIndexEntry = namedtuple('NSIndexEntry', 'segment offset size extra') +NSIndexEntry = namedtuple('NSIndexEntry', 'segment offset size') cdef class NSIndex(IndexBase): @@ -224,7 +224,7 @@ cdef class NSIndex(IndexBase): raise KeyError(key) cdef uint32_t segment = _le32toh(data[0]) assert segment <= _MAX_VALUE, "maximum number of segments reached" - return NSIndexEntry(segment, _le32toh(data[1]), _le32toh(data[2]), _le32toh(data[3])) + return NSIndexEntry(segment, _le32toh(data[1]), _le32toh(data[2])) def __setitem__(self, key, value): assert len(key) == self.key_size @@ -234,7 +234,7 @@ cdef class NSIndex(IndexBase): data[0] = _htole32(segment) data[1] = _htole32(value[1]) data[2] = _htole32(value[2]) - data[3] = _htole32(value[3]) + data[3] = 0 # init flags to all cleared if not hashindex_set(self.index, key, data): raise Exception('hashindex_set failed') @@ -247,9 +247,12 @@ cdef class NSIndex(IndexBase): assert segment <= _MAX_VALUE, "maximum number of segments reached" return data != NULL - def iteritems(self, marker=None): + def iteritems(self, marker=None, mask=0, value=0): + """iterate over all items or optionally only over items having specific flag values""" cdef const unsigned char *key - iter = NSKeyIterator(self.key_size) + assert isinstance(mask, int) + assert isinstance(value, int) + iter = NSKeyIterator(self.key_size, mask, value) iter.idx = self iter.index = self.index if marker: @@ -259,6 +262,20 @@ cdef class NSIndex(IndexBase): iter.key = key - self.key_size return iter + def flags(self, key, mask=0xFFFFFFFF, value=None): + """query and optionally set flags""" + assert len(key) == self.key_size + assert isinstance(mask, int) + data = hashindex_get(self.index, key) + if not data: + raise KeyError(key) + flags = _le32toh(data[3]) + if isinstance(value, int): + new_flags = flags & ~mask # clear masked bits + new_flags |= value & mask # set value bits + data[3] = _htole32(new_flags) + return flags & mask # always return previous flags value + cdef class NSKeyIterator: cdef NSIndex idx @@ -266,27 +283,38 @@ cdef class NSKeyIterator: cdef const unsigned char *key cdef int key_size cdef int exhausted + cdef int flag_mask + cdef int flag_value - def __cinit__(self, key_size): + def __cinit__(self, key_size, mask, value): self.key = NULL self.key_size = key_size + # note: mask and value both default to 0, so they will match all entries + self.flag_mask = _htole32(mask) + self.flag_value = _htole32(value) self.exhausted = 0 def __iter__(self): return self def __next__(self): + cdef uint32_t *value 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) + while True: + self.key = hashindex_next_key(self.index, self.key) + if not self.key: + self.exhausted = 1 + raise StopIteration + value = (self.key + self.key_size) + if value[3] & self.flag_mask == self.flag_value: + # we found a matching entry! + break + cdef uint32_t segment = _le32toh(value[0]) assert segment <= _MAX_VALUE, "maximum number of segments reached" return ((self.key)[:self.key_size], - NSIndexEntry(segment, _le32toh(value[1]), _le32toh(value[2]), _le32toh(value[3]))) + NSIndexEntry(segment, _le32toh(value[1]), _le32toh(value[2]))) cdef class NSIndex1(IndexBase): # legacy borg 1.x diff --git a/src/borg/repository.py b/src/borg/repository.py index 1de071dae..1020a1eab 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -821,7 +821,7 @@ class Repository: except LoggedIO.SegmentFull: complete_xfer() new_segment, offset = self.io.write_put(key, data) - self.index[key] = NSIndexEntry(new_segment, offset, len(data), in_index.extra) + self.index[key] = NSIndexEntry(new_segment, offset, len(data)) segments.setdefault(new_segment, 0) segments[new_segment] += 1 segments[segment] -= 1 @@ -937,7 +937,7 @@ class Repository: self.segments[in_index.segment] -= 1 except KeyError: pass - self.index[key] = NSIndexEntry(segment, offset, size, 0) + self.index[key] = NSIndexEntry(segment, offset, size) self.segments[segment] += 1 self.storage_quota_use += header_size(tag) + size elif tag == TAG_DELETE: @@ -1182,7 +1182,7 @@ class Repository: self.index = self.open_index(transaction_id) at_start = marker is None # smallest valid seg is 0, smallest valid offs is 8 - start_segment, start_offset, _, _ = (0, 0, 0, 0) if at_start else self.index[marker] + start_segment, start_offset, _ = (0, 0, 0) if at_start else self.index[marker] result = [] 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) @@ -1212,7 +1212,7 @@ class Repository: if not self.index: self.index = self.open_index(self.get_transaction_id()) try: - in_index = NSIndexEntry(*((self.index[id] + (None, None))[:4])) # legacy: no size/extra + in_index = NSIndexEntry(*((self.index[id] + (None, ))[:3])) # legacy: index entriess have no size element return self.io.read(in_index.segment, in_index.offset, id, expected_size=in_index.size) except KeyError: raise self.ObjectNotFound(id, self.path) from None @@ -1243,7 +1243,7 @@ class Repository: self.storage_quota_use += header_size(TAG_PUT2) + len(data) self.segments.setdefault(segment, 0) self.segments[segment] += 1 - self.index[id] = NSIndexEntry(segment, offset, len(data), 0) + self.index[id] = NSIndexEntry(segment, offset, len(data)) 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)) diff --git a/src/borg/selftest.py b/src/borg/selftest.py index 006e85a56..00356cd0e 100644 --- a/src/borg/selftest.py +++ b/src/borg/selftest.py @@ -33,7 +33,7 @@ SELFTEST_CASES = [ ChunkerTestCase, ] -SELFTEST_COUNT = 36 +SELFTEST_COUNT = 37 class SelfTestResult(TestResult): diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 537151e07..b05ff77fd 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -87,8 +87,8 @@ class HashIndexTestCase(BaseTestCase): del idx def test_nsindex(self): - self._generic_test(NSIndex, lambda x: (x, x, x, x), - 'c9fe5878800d2a0691b667c665a00d4a186e204e891076d6b109016940742bed') + self._generic_test(NSIndex, lambda x: (x, x, x), + '7d70671d0b7e9d2f51b2691ecf35184b9f8ecc1202cceb2748c905c8fc04c256') def test_chunkindex(self): self._generic_test(ChunkIndex, lambda x: (x, x), @@ -153,6 +153,70 @@ class HashIndexTestCase(BaseTestCase): assert chunks == 1 + 2 + 3 assert unique_chunks == 3 + def test_flags(self): + idx = NSIndex() + key = H(0) + self.assert_raises(KeyError, idx.flags, key, 0) + idx[key] = 0, 0, 0 # create entry + # check bit 0 and 1, should be both 0 after entry creation + self.assert_equal(idx.flags(key, mask=3), 0) + # set bit 0 + idx.flags(key, mask=1, value=1) + self.assert_equal(idx.flags(key, mask=1), 1) + # set bit 1 + idx.flags(key, mask=2, value=2) + self.assert_equal(idx.flags(key, mask=2), 2) + # check both bit 0 and 1, both should be set + self.assert_equal(idx.flags(key, mask=3), 3) + # clear bit 1 + idx.flags(key, mask=2, value=0) + self.assert_equal(idx.flags(key, mask=2), 0) + # clear bit 0 + idx.flags(key, mask=1, value=0) + self.assert_equal(idx.flags(key, mask=1), 0) + # check both bit 0 and 1, both should be cleared + self.assert_equal(idx.flags(key, mask=3), 0) + + def test_flags_iteritems(self): + idx = NSIndex() + keys_flagged0 = {H(i) for i in (1, 2, 3, 42)} + keys_flagged1 = {H(i) for i in (11, 12, 13, 142)} + keys_flagged2 = {H(i) for i in (21, 22, 23, 242)} + keys_flagged3 = {H(i) for i in (31, 32, 33, 342)} + for key in keys_flagged0: + idx[key] = 0, 0, 0 # create entry + idx.flags(key, mask=3, value=0) # not really necessary, unflagged is default + for key in keys_flagged1: + idx[key] = 0, 0, 0 # create entry + idx.flags(key, mask=3, value=1) + for key in keys_flagged2: + idx[key] = 0, 0, 0 # create entry + idx.flags(key, mask=3, value=2) + for key in keys_flagged3: + idx[key] = 0, 0, 0 # create entry + idx.flags(key, mask=3, value=3) + # check if we can iterate over all items + k_all = {k for k, v in idx.iteritems()} + self.assert_equal(k_all, keys_flagged0 | keys_flagged1 | keys_flagged2 | keys_flagged3) + # check if we can iterate over the flagged0 items + k0 = {k for k, v in idx.iteritems(mask=3, value=0)} + self.assert_equal(k0, keys_flagged0) + # check if we can iterate over the flagged1 items + k1 = {k for k, v in idx.iteritems(mask=3, value=1)} + self.assert_equal(k1, keys_flagged1) + # check if we can iterate over the flagged2 items + k1 = {k for k, v in idx.iteritems(mask=3, value=2)} + self.assert_equal(k1, keys_flagged2) + # check if we can iterate over the flagged3 items + k1 = {k for k, v in idx.iteritems(mask=3, value=3)} + self.assert_equal(k1, keys_flagged3) + # check if we can iterate over the flagged1 + flagged3 items + k1 = {k for k, v in idx.iteritems(mask=1, value=1)} + self.assert_equal(k1, keys_flagged1 | keys_flagged3) + # check if we can iterate over the flagged0 + flagged2 items + k1 = {k for k, v in idx.iteritems(mask=1, value=0)} + self.assert_equal(k1, keys_flagged0 | keys_flagged2) + class HashIndexExtraTestCase(BaseTestCase): """These tests are separate because they should not become part of the selftest. @@ -531,38 +595,38 @@ class IndexCorruptionTestCase(BaseTestCase): from struct import pack - def HH(w, x, y, z): - # make some 32byte long thing that depends on w, x, y, z. - # same w will mean a collision in the hashtable as bucket index is computed from - # first 4 bytes. giving a specific w targets bucket index w. - # x is to create different keys and does not go into the bucket index calculation. - # so, same w + different x --> collision - return pack(' collision + return pack(' Date: Tue, 14 Jun 2022 15:17:30 +0200 Subject: [PATCH 044/160] fix selftest count --- src/borg/selftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/selftest.py b/src/borg/selftest.py index 00356cd0e..1bef011c2 100644 --- a/src/borg/selftest.py +++ b/src/borg/selftest.py @@ -33,7 +33,7 @@ SELFTEST_CASES = [ ChunkerTestCase, ] -SELFTEST_COUNT = 37 +SELFTEST_COUNT = 38 class SelfTestResult(TestResult): From 72994a4432567e0b3f087066c87ac309f9d52540 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 14 Jun 2022 15:40:09 +0200 Subject: [PATCH 045/160] also test pull requests against borg2 branch --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf14f2397..9f91adb7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ on: - 'requirements.d/*' - '!docs/**' pull_request: - branches: [ master ] + branches: [ master, borg2 ] paths: - '**.py' - '**.pyx' From c01f3527e5b7fffbe3e7634c7486a126c99e67e7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 14 Jun 2022 15:42:30 +0200 Subject: [PATCH 046/160] fix linter errors --- src/borg/archiver.py | 2 +- src/borg/cache.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 1a46e4350..c92ba7e05 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1356,7 +1356,7 @@ class Archiver: tarinfo.uid = item.uid tarinfo.gid = item.gid tarinfo.uname = item.get('user', '') - tarinfo.gname = item.get('group', '') + tarinfo.gname = item.get('group', '') # The linkname in tar has 2 uses: # 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 diff --git a/src/borg/cache.py b/src/borg/cache.py index 784910673..d5d87f74d 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -1,5 +1,4 @@ import configparser -import json import os import shutil import stat @@ -655,8 +654,6 @@ class LocalCache(CacheStatsMixin): archive indexes. """ archive_path = os.path.join(self.path, 'chunks.archive.d') - # An index of chunks whose size had to be fetched - chunks_fetched_size_index = ChunkIndex() # Instrumentation processed_item_metadata_bytes = 0 processed_item_metadata_chunks = 0 From 1c707b7da20ab08bb93f637e64eb8af24c0b724c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 14 Jun 2022 00:28:47 +0200 Subject: [PATCH 047/160] cli: use --repo option instead of positional repo parameter currently still with ::archive appended. --- src/borg/archiver.py | 149 +-- src/borg/testsuite/__init__.py | 2 +- src/borg/testsuite/archiver.py | 1643 ++++++++++++++++---------------- 3 files changed, 858 insertions(+), 936 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index c92ba7e05..0e2d07855 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -664,34 +664,37 @@ class Archiver: 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' # measure create perf (without files cache to always have it chunking) t_start = time.monotonic() - rc = self.do_create(self.parse_args(['create', compression, '--files-cache=disabled', archive + '1', path])) + rc = self.do_create(self.parse_args([f'--repo={repo}::borg-benchmark-crud1', 'create', + compression, '--files-cache=disabled', 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'])) + rc1 = self.do_create(self.parse_args([f'--repo={repo}::borg-benchmark-crud2', 'create', + compression, path])) + rc2 = self.do_delete(self.parse_args([f'--repo={repo}::borg-benchmark-crud2', 'delete'])) 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])) + rc1 = self.do_create(self.parse_args([f'--repo={repo}::borg-benchmark-crud3', 'create', + compression, path])) t_end = time.monotonic() dt_update = t_end - t_start - rc2 = self.do_delete(self.parse_args(['delete', archive + '3'])) + rc2 = self.do_delete(self.parse_args([f'--repo={repo}::borg-benchmark-crud3', 'delete'])) 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'])) + rc = self.do_extract(self.parse_args([f'--repo={repo}::borg-benchmark-crud1', 'extract', + '--dry-run'])) 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'])) + rc = self.do_delete(self.parse_args([f'--repo={repo}::borg-benchmark-crud1', 'delete'])) t_end = time.monotonic() dt_delete = t_end - t_start assert rc == 0 @@ -3200,6 +3203,8 @@ class Archiver: 'compatible file can be generated by suffixing FILE with ".pyprof".') add_common_option('--rsh', metavar='RSH', dest='rsh', help="Use this command to connect to the 'borg serve' process (default: 'ssh')") + add_common_option('--repo', metavar='REPO', dest='location', type=location_validator(), + help="repository to use") # XXXYYY def define_exclude_and_patterns(add_option, *, tag_files=False, strip_components=False): add_option('-e', '--exclude', metavar='PATTERN', dest='patterns', @@ -3263,8 +3268,7 @@ class Archiver: def define_borg_mount(parser): parser.set_defaults(func=self.do_mount) - parser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', type=location_validator(), - help='repository or archive to mount') + # archive name parser.add_argument('--consider-checkpoints', action='store_true', dest='consider_checkpoints', help='Show checkpoint archives in the repository contents list (default: hidden).') parser.add_argument('mountpoint', metavar='MOUNTPOINT', type=str, @@ -3427,10 +3431,6 @@ class Archiver: help='benchmarks borg CRUD (create, extract, update, delete).') subparser.set_defaults(func=self.do_benchmark_crud) - subparser.add_argument('location', metavar='REPOSITORY', - type=location_validator(archive=False), - help='repository to use for benchmark (must exist)') - subparser.add_argument('path', metavar='PATH', help='path were to create benchmark input data') bench_cpu_epilog = process_epilog(""" @@ -3460,9 +3460,6 @@ 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', nargs='?', default='', - type=location_validator(archive=False), - help='repository for which to break the locks') # borg check check_epilog = process_epilog(""" @@ -3545,9 +3542,7 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='verify repository') subparser.set_defaults(func=self.do_check) - subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='', - type=location_validator(), - help='repository or archive to check consistency of') + # archive name subparser.add_argument('--repository-only', dest='repo_only', action='store_true', help='only perform repository checks') subparser.add_argument('--archives-only', dest='archives_only', action='store_true', @@ -3595,9 +3590,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='compact segment files / free space in repo') subparser.set_defaults(func=self.do_compact) - subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', - type=location_validator(archive=False), - help='repository to compact') subparser.add_argument('--cleanup-commits', dest='cleanup_commits', action='store_true', help='cleanup commit-only 17-byte segment files') subparser.add_argument('--threshold', metavar='PERCENT', dest='threshold', @@ -3635,9 +3627,6 @@ class Archiver: group.add_argument('-l', '--list', dest='list', action='store_true', help='list the configuration of the repo') - subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', - type=location_validator(archive=False, proto='file'), - help='repository to configure') subparser.add_argument('name', metavar='NAME', nargs='?', help='name of config key') subparser.add_argument('value', metavar='VALUE', nargs='?', @@ -3921,9 +3910,7 @@ class Archiver: help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') - subparser.add_argument('location', metavar='ARCHIVE', - type=location_validator(archive=True), - help='name of archive to create (must be also a valid directory name)') + # archive name subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to archive') @@ -3966,9 +3953,7 @@ class Archiver: 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') + # archive name debug_dump_archive_epilog = process_epilog(""" This command dumps all metadata of an archive in a decoded form to a file. @@ -3979,9 +3964,7 @@ class Archiver: 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') + # archive name subparser.add_argument('path', metavar='PATH', type=str, help='file to dump data into') @@ -3994,9 +3977,6 @@ class Archiver: 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') @@ -4009,9 +3989,6 @@ class Archiver: 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='repository to dump') subparser.add_argument('--ghost', dest='ghost', action='store_true', help='dump all segment file contents, including deleted/uncommitted objects and commits.') @@ -4024,9 +4001,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='search repo objects (debug)') subparser.set_defaults(func=self.do_debug_search_repo_objs) - subparser.add_argument('location', metavar='REPOSITORY', - type=location_validator(archive=False), - help='repository to search') subparser.add_argument('wanted', metavar='WANTED', type=str, help='term to search the repo for, either 0x1234abcd hex term or a string') @@ -4039,9 +4013,6 @@ class Archiver: 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', - 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, @@ -4056,9 +4027,6 @@ class Archiver: 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', - 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') @@ -4071,9 +4039,6 @@ class Archiver: 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', - 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') @@ -4086,9 +4051,6 @@ class Archiver: 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', - 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') @@ -4101,9 +4063,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='dump repo hints (debug)') subparser.set_defaults(func=self.do_debug_dump_hints) - 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') @@ -4171,9 +4130,7 @@ class Archiver: help='keep the local security info when deleting a repository') subparser.add_argument('--save-space', dest='save_space', action='store_true', help='work slower, but using less space') - subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='', - type=location_validator(), - help='repository or archive to delete') + # archive name subparser.add_argument('archives', metavar='ARCHIVE', nargs='*', help='archives to delete') define_archive_filters_group(subparser) @@ -4210,9 +4167,9 @@ class Archiver: subparser.add_argument('other_location', metavar='SRC_REPOSITORY', type=location_validator(archive=False, other=True), help='source repository') - subparser.add_argument('location', metavar='DST_REPOSITORY', - type=location_validator(archive=False, other=False), - help='destination repository') + # subparser.add_argument('-r', '--repo', dest='location', metavar='DST_REPOSITORY', + # type=location_validator(archive=False, other=False), + # help='destination repository') define_archive_filters_group(subparser) # borg diff @@ -4250,12 +4207,12 @@ class Archiver: help='Sort the output lines by file path.') subparser.add_argument('--json-lines', action='store_true', help='Format output as JSON Lines. ') - subparser.add_argument('location', metavar='REPO::ARCHIVE1', - type=location_validator(archive=True), - help='repository location and ARCHIVE1 name') + subparser.add_argument('archive1', metavar='ARCHIVE1', + type=archivename_validator(), + help='ARCHIVE1 name') subparser.add_argument('archive2', metavar='ARCHIVE2', type=archivename_validator(), - help='ARCHIVE2 name (no repository location allowed)') + help='ARCHIVE2 name') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths of items inside the archives to compare; patterns are supported') define_exclusion_group(subparser) @@ -4317,9 +4274,7 @@ class Archiver: subparser.add_argument('--tar-format', metavar='FMT', dest='tar_format', default='GNU', choices=('BORG', 'PAX', 'GNU'), help='select tar format: BORG, PAX or GNU') - subparser.add_argument('location', metavar='ARCHIVE', - type=location_validator(archive=True), - help='archive to export') + # archive name subparser.add_argument('tarfile', metavar='FILE', help='output tar file. "-" to write to stdout instead.') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, @@ -4377,9 +4332,7 @@ class Archiver: help='write all extracted data to stdout') 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), - help='archive to extract') + # archive name subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to extract; patterns are supported') define_exclusion_group(subparser, strip_components=True) @@ -4417,9 +4370,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', nargs='?', default='', - type=location_validator(), - help='repository or archive to display information about') + # archive name subparser.add_argument('--json', action='store_true', help='format output as JSON') define_archive_filters_group(subparser) @@ -4553,9 +4504,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='initialize empty repository') subparser.set_defaults(func=self.do_init) - subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', - type=location_validator(archive=False), - help='repository to create') subparser.add_argument('--other-location', metavar='OTHER_REPOSITORY', dest='other_location', type=location_validator(archive=False, other=True), help='reuse the key material from the other repository') @@ -4626,8 +4574,6 @@ class Archiver: 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', @@ -4657,8 +4603,6 @@ class Archiver: 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 (\'-\' to read from stdin)') subparser.add_argument('--paper', dest='paper', action='store_true', @@ -4679,8 +4623,6 @@ class Archiver: 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)) change_location_epilog = process_epilog(""" Change the location of a borg key. The key can be stored at different locations: @@ -4697,8 +4639,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='change key location') subparser.set_defaults(func=self.do_change_location) - subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', - type=location_validator(archive=False)) subparser.add_argument('key_mode', metavar='KEY_LOCATION', choices=('repokey', 'keyfile'), help='select key location') subparser.add_argument('--keep', dest='keep', action='store_true', @@ -4741,8 +4681,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='change key algorithm') subparser.set_defaults(func=self.do_change_algorithm) - subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', - type=location_validator(archive=False)) subparser.add_argument('algorithm', metavar='ALGORITHM', choices=list(KEY_ALGORITHMS), help='select key algorithm') @@ -4821,9 +4759,7 @@ class Archiver: '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 or archive to list contents of') + # archive name subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to list; patterns are supported') define_archive_filters_group(subparser) @@ -4926,9 +4862,6 @@ class Archiver: 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='', - type=location_validator(archive=False), - help='repository to prune') # borg recreate recreate_epilog = process_epilog(""" @@ -5036,9 +4969,7 @@ class Archiver: 'HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the current defaults. ' 'default: %s,%d,%d,%d,%d' % CHUNKER_PARAMS) - subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='', - type=location_validator(), - help='repository or archive to recreate') + # archive name subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to recreate; patterns are supported') @@ -5054,12 +4985,12 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='rename archive') subparser.set_defaults(func=self.do_rename) - subparser.add_argument('location', metavar='ARCHIVE', - type=location_validator(archive=True), - help='archive to rename') + subparser.add_argument('name_current', metavar='OLDNAME', + type=archivename_validator(), + help='the current archive name') subparser.add_argument('name', metavar='NEWNAME', type=archivename_validator(), - help='the new archive name to use') + help='the new archive name') # borg serve serve_epilog = process_epilog(""" @@ -5176,9 +5107,6 @@ class Archiver: 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') # borg with-lock with_lock_epilog = process_epilog(""" @@ -5202,9 +5130,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='run user command with lock held') subparser.set_defaults(func=self.do_with_lock) - subparser.add_argument('location', metavar='REPOSITORY', - type=location_validator(archive=False), - help='repository to lock') subparser.add_argument('command', metavar='COMMAND', help='command to run') subparser.add_argument('args', metavar='ARGS', nargs=argparse.REMAINDER, @@ -5286,9 +5211,7 @@ class Archiver: help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') - subparser.add_argument('location', metavar='ARCHIVE', - type=location_validator(archive=True), - help='name of archive to create (must be also a valid directory name)') + # archive name subparser.add_argument('tarfile', metavar='TARFILE', help='input tar file. "-" to read from stdin instead.') return parser diff --git a/src/borg/testsuite/__init__.py b/src/borg/testsuite/__init__.py index b1db8aa95..307798e64 100644 --- a/src/borg/testsuite/__init__.py +++ b/src/borg/testsuite/__init__.py @@ -248,7 +248,7 @@ class BaseTestCase(unittest.TestCase): mountpoint = tempfile.mkdtemp() else: os.mkdir(mountpoint) - args = ['mount', location, mountpoint] + list(options) + args = [f'--repo={location}', 'mount', mountpoint] + list(options) if os_fork: # Do not spawn, but actually (OS) fork. if os.fork() == 0: diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index e033d6cf7..1c67a5573 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -147,16 +147,16 @@ def test_return_codes(cmd, tmpdir): input = tmpdir.mkdir('input') output = tmpdir.mkdir('output') input.join('test_file').write('content') - rc, out = cmd('init', '--encryption=none', '%s' % str(repo)) + rc, out = cmd('--repo=%s' % str(repo), 'init', '--encryption=none') assert rc == EXIT_SUCCESS - rc, out = cmd('create', '%s::archive' % repo, str(input)) + rc, out = cmd('--repo=%s::archive' % repo, 'create', str(input)) assert rc == EXIT_SUCCESS with changedir(str(output)): - rc, out = cmd('extract', '%s::archive' % repo) + rc, out = cmd('--repo=%s::archive' % repo, 'extract') assert rc == EXIT_SUCCESS - rc, out = cmd('extract', '%s::archive' % repo, 'does/not/match') + rc, out = cmd('--repo=%s::archive' % repo, 'extract', 'does/not/match') assert rc == EXIT_WARNING # pattern did not match - rc, out = cmd('create', '%s::archive' % repo, str(input)) + rc, out = cmd('--repo=%s::archive' % repo, 'create', str(input)) assert rc == EXIT_ERROR # duplicate archive name @@ -203,7 +203,7 @@ def test_disk_full(cmd): shutil.rmtree(input, ignore_errors=True) # keep some space and some inodes in reserve that we can free up later: make_files(reserve, 80, 100000, rnd=False) - rc, out = cmd('init', repo) + rc, out = cmd(f'--repo={repo}', 'init') if rc != EXIT_SUCCESS: print('init', rc, out) assert rc == EXIT_SUCCESS @@ -219,7 +219,7 @@ def test_disk_full(cmd): break raise try: - rc, out = cmd('create', '%s::test%03d' % (repo, i), input) + rc, out = cmd('--repo=%s::test%03d' % (repo, i), 'create', input) success = rc == EXIT_SUCCESS if not success: print('create', rc, out) @@ -231,10 +231,10 @@ def test_disk_full(cmd): # now some error happened, likely we are out of disk space. # free some space so we can expect borg to be able to work normally: shutil.rmtree(reserve, ignore_errors=True) - rc, out = cmd('list', repo) + rc, out = cmd(f'--repo={repo}', 'list') if rc != EXIT_SUCCESS: print('list', rc, out) - rc, out = cmd('check', '--repair', repo) + rc, out = cmd(f'--repo={repo}', 'check', '--repair') if rc != EXIT_SUCCESS: print('check', rc, out) assert rc == EXIT_SUCCESS @@ -299,7 +299,7 @@ class ArchiverTestCaseBase(BaseTestCase): return output def create_src_archive(self, name): - self.cmd('create', '--compression=lz4', self.repository_location + '::' + name, src_dir) + self.cmd(f'--repo={self.repository_location}::{name}', 'create', '--compression=lz4', src_dir) def open_archive(self, name): repository = Repository(self.repository_path, exclusive=True) @@ -391,16 +391,16 @@ 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', '--encryption=repokey', '--show-version', '--show-rc', self.repository_location, fork=True) + output = self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey', '--show-version', '--show-rc', fork=True) self.assert_in('borgbackup version', output) self.assert_in('terminating with success status, rc 0', output) - self.cmd('create', '--exclude-nodump', self.repository_location + '::test', 'input') - output = self.cmd('create', '--exclude-nodump', '--stats', self.repository_location + '::test.2', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude-nodump', 'input') + output = self.cmd(f'--repo={self.repository_location}::test.2', 'create', '--exclude-nodump', '--stats', 'input') self.assert_in('Archive name: test.2', output) self.assert_in('This archive: ', output) with changedir('output'): - self.cmd('extract', self.repository_location + '::test') - list_output = self.cmd('list', '--short', self.repository_location) + self.cmd(f'--repo={self.repository_location}::test', 'extract') + list_output = self.cmd(f'--repo={self.repository_location}', 'list', '--short') self.assert_in('test', list_output) self.assert_in('test.2', list_output) expected = [ @@ -427,15 +427,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): # remove the file we did not backup, so input and output become equal expected.remove('input/flagfile') # this file is UF_NODUMP os.remove(os.path.join('input', 'flagfile')) - list_output = self.cmd('list', '--short', self.repository_location + '::test') + list_output = self.cmd(f'--repo={self.repository_location}::test', 'list', '--short') for name in expected: self.assert_in(name, list_output) self.assert_dirs_equal('input', 'output/input') - info_output = self.cmd('info', self.repository_location + '::test') + info_output = self.cmd(f'--repo={self.repository_location}::test', 'info') item_count = 5 if has_lchflags else 6 # one file is UF_NODUMP self.assert_in('Number of files: %d' % item_count, info_output) shutil.rmtree(self.cache_path) - info_output2 = self.cmd('info', self.repository_location + '::test') + info_output2 = self.cmd(f'--repo={self.repository_location}::test', 'info') def filter(output): # filter for interesting "info" output, ignore cache rebuilding related stuff @@ -462,10 +462,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): hl_b = os.path.join(path_b, 'hardlink') self.create_regular_file(hl_a, contents=b'123456') os.link(hl_a, hl_b) - self.cmd('init', '--encryption=none', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input', 'input') # give input twice! + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input', 'input') # give input twice! # test if created archive has 'input' contents twice: - archive_list = self.cmd('list', '--json-lines', self.repository_location + '::test') + archive_list = self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines') paths = [json.loads(line)['path'] for line in archive_list.split('\n') if line] # we have all fs items exactly once! assert sorted(paths) == ['input', 'input/a', 'input/a/hardlink', 'input/b', 'input/b/hardlink'] @@ -476,13 +476,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): repository_location = self.prefix + repository_path with pytest.raises(Repository.ParentPathDoesNotExist): # normal borg init does NOT create missing parent dirs - self.cmd('init', '--encryption=none', repository_location) + self.cmd(f'--repo={repository_location}', 'init', '--encryption=none') # but if told so, it does: - self.cmd('init', '--encryption=none', '--make-parent-dirs', repository_location) + self.cmd(f'--repo={repository_location}', 'init', '--encryption=none', '--make-parent-dirs') assert os.path.exists(parent_path) def test_unix_socket(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.bind(os.path.join(self.input_path, 'unix-socket')) @@ -491,19 +491,19 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') sock.close() with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') 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', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') assert os.readlink('input/link1') == 'somewhere' @pytest.mark.skipif(not are_symlinks_supported() or not are_hardlinks_supported(), @@ -513,10 +513,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): with changedir('input'): os.symlink('target', 'symlink1') os.link('symlink1', 'symlink2', follow_symlinks=False) - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with changedir('output'): - output = self.cmd('extract', self.repository_location + '::test') + output = self.cmd(f'--repo={self.repository_location}::test', 'extract') print(output) with changedir('input'): assert os.path.exists('target') @@ -547,10 +547,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): atime, mtime = 123456780, 234567890 have_noatime = has_noatime('input/file1') os.utime('input/file1', (atime, mtime)) - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', '--atime', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--atime', 'input') with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') sti = os.stat('input/file1') sto = os.stat('output/input/file1') assert sti.st_mtime_ns == sto.st_mtime_ns == mtime * 1e9 @@ -567,10 +567,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): birthtime, mtime, atime = 946598400, 946684800, 946771200 os.utime('input/file1', (atime, birthtime)) os.utime('input/file1', (atime, mtime)) - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') sti = os.stat('input/file1') sto = os.stat('output/input/file1') assert int(sti.st_birthtime * 1e9) == int(sto.st_birthtime * 1e9) == birthtime * 1e9 @@ -583,10 +583,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): birthtime, mtime, atime = 946598400, 946684800, 946771200 os.utime('input/file1', (atime, birthtime)) os.utime('input/file1', (atime, mtime)) - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', '--nobirthtime', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--nobirthtime', 'input') with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') sti = os.stat('input/file1') sto = os.stat('output/input/file1') assert int(sti.st_birthtime * 1e9) == birthtime * 1e9 @@ -644,10 +644,10 @@ 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', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with changedir(self.output_path): - self.cmd('extract', '--sparse', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--sparse') self.assert_dirs_equal('input', 'output/input') filename = os.path.join(self.output_path, 'input', 'sparse') with open(filename, 'rb') as fd: @@ -663,158 +663,158 @@ class ArchiverTestCase(ArchiverTestCaseBase): filename = os.path.join(self.input_path, filename) with open(filename, 'wb'): pass - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') for filename in filenames: with changedir('output'): - self.cmd('extract', self.repository_location + '::test', os.path.join('input', filename)) + self.cmd(f'--repo={self.repository_location}::test', 'extract', os.path.join('input', filename)) assert os.path.exists(os.path.join('output', 'input', filename)) def test_repository_swap_detection(self): self.create_test_files() os.environ['BORG_PASSPHRASE'] = 'passphrase' - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') repository_id = self._extract_repository_id(self.repository_path) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') shutil.rmtree(self.repository_path) - self.cmd('init', '--encryption=none', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self._set_repository_id(self.repository_path, repository_id) self.assert_equal(repository_id, self._extract_repository_id(self.repository_path)) if self.FORK_DEFAULT: - self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.EncryptionMethodMismatch): - self.cmd('create', self.repository_location + '::test.2', 'input') + self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') def test_repository_swap_detection2(self): self.create_test_files() - self.cmd('init', '--encryption=none', self.repository_location + '_unencrypted') + self.cmd(f'--repo={self.repository_location}_unencrypted', 'init', '--encryption=none') 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(f'--repo={self.repository_location}_encrypted', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}_encrypted::test', 'create', 'input') 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) + self.cmd(f'--repo={self.repository_location}_encrypted::test.2', 'create', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.RepositoryAccessAborted): - self.cmd('create', self.repository_location + '_encrypted::test.2', 'input') + self.cmd(f'--repo={self.repository_location}_encrypted::test.2', 'create', '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) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') repository_id = self._extract_repository_id(self.repository_path) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') shutil.rmtree(self.repository_path) - self.cmd('init', '--encryption=none', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') 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) + self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') if self.FORK_DEFAULT: - self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.EncryptionMethodMismatch): - self.cmd('create', self.repository_location + '::test.2', 'input') + self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') def test_repository_swap_detection2_no_cache(self): self.create_test_files() - self.cmd('init', '--encryption=none', self.repository_location + '_unencrypted') + self.cmd(f'--repo={self.repository_location}_unencrypted', 'init', '--encryption=none') 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') + self.cmd(f'--repo={self.repository_location}_encrypted', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}_encrypted::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}_unencrypted', 'delete', '--cache-only') + self.cmd(f'--repo={self.repository_location}_encrypted', 'delete', '--cache-only') 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) + self.cmd(f'--repo={self.repository_location}_encrypted::test.2', 'create', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.RepositoryAccessAborted): - self.cmd('create', self.repository_location + '_encrypted::test.2', 'input') + self.cmd(f'--repo={self.repository_location}_encrypted::test.2', 'create', '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') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', '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) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') # Delete cache & security database, AKA switch to user perspective - self.cmd('delete', '--cache-only', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') shutil.rmtree(self.get_security_dir()) 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) + self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.CacheInitAbortedError): - self.cmd('create', self.repository_location + '::test.2', 'input') + self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') def test_repository_move(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') security_dir = self.get_security_dir() 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') + self.cmd(f'--repo={self.repository_location}_new', 'info') 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') + self.cmd(f'--repo={self.repository_location}_new', 'info') shutil.rmtree(self.cache_path) - self.cmd('info', self.repository_location + '_new') + self.cmd(f'--repo={self.repository_location}_new', 'info') shutil.rmtree(security_dir) - self.cmd('info', self.repository_location + '_new') + self.cmd(f'--repo={self.repository_location}_new', 'info') 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', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') with open(os.path.join(self.get_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) + self.cmd(f'--repo={self.repository_location}', 'info') def test_unknown_unencrypted(self): - self.cmd('init', '--encryption=none', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') # Ok: repository is known - self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'info') # Ok: repository is still known (through security_dir) shutil.rmtree(self.cache_path) - self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'info') # Needs confirmation: cache and security dir both gone (eg. another host or rm -rf ~) shutil.rmtree(self.cache_path) shutil.rmtree(self.get_security_dir()) if self.FORK_DEFAULT: - self.cmd('info', self.repository_location, exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'info', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.CacheInitAbortedError): - self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'info') with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='yes'): - self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'info') def test_strip_components(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('dir/file') - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with changedir('output'): - self.cmd('extract', self.repository_location + '::test', '--strip-components', '3') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--strip-components', '3') assert not os.path.exists('file') with self.assert_creates_file('file'): - self.cmd('extract', self.repository_location + '::test', '--strip-components', '2') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--strip-components', '2') with self.assert_creates_file('dir/file'): - self.cmd('extract', self.repository_location + '::test', '--strip-components', '1') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--strip-components', '1') with self.assert_creates_file('input/dir/file'): - self.cmd('extract', self.repository_location + '::test', '--strip-components', '0') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--strip-components', '0') def _extract_hardlinks_setup(self): os.mkdir(os.path.join(self.input_path, 'dir1')) @@ -832,8 +832,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.link(os.path.join(self.input_path, 'dir1/source2'), os.path.join(self.input_path, 'dir1/aaaa')) - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') @requires_hardlinks @unittest.skipUnless(llfuse, 'llfuse not installed') @@ -873,7 +873,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_extract_hardlinks1(self): self._extract_hardlinks_setup() with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') assert os.stat('input/source').st_nlink == 4 assert os.stat('input/abba').st_nlink == 4 assert os.stat('input/dir1/hardlink').st_nlink == 4 @@ -884,14 +884,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_extract_hardlinks2(self): self._extract_hardlinks_setup() with changedir('output'): - self.cmd('extract', self.repository_location + '::test', '--strip-components', '2') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--strip-components', '2') assert os.stat('hardlink').st_nlink == 2 assert os.stat('subdir/hardlink').st_nlink == 2 assert open('subdir/hardlink', 'rb').read() == b'123456' assert os.stat('aaaa').st_nlink == 2 assert os.stat('source2').st_nlink == 2 with changedir('output'): - self.cmd('extract', self.repository_location + '::test', 'input/dir1') + self.cmd(f'--repo={self.repository_location}::test', 'extract', 'input/dir1') assert os.stat('input/dir1/hardlink').st_nlink == 2 assert os.stat('input/dir1/subdir/hardlink').st_nlink == 2 assert open('input/dir1/subdir/hardlink', 'rb').read() == b'123456' @@ -909,11 +909,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): hl_b = os.path.join(path_b, 'hardlink') self.create_regular_file(hl_a, contents=b'123456') os.link(hl_a, hl_b) - self.cmd('init', '--encryption=none', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input', 'input') # give input twice! + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input', 'input') # give input twice! # now test extraction with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') # if issue #5603 happens, extraction gives rc == 1 (triggering AssertionError) and warnings like: # input/a/hardlink: link: [Errno 2] No such file or directory: 'input/a/hardlink' -> 'input/a/hardlink' # input/b/hardlink: link: [Errno 2] No such file or directory: 'input/a/hardlink' -> 'input/b/hardlink' @@ -922,24 +922,24 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert os.stat('input/b/hardlink').st_nlink == 2 def test_extract_include_exclude(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('file3', size=1024 * 80) self.create_regular_file('file4', size=1024 * 80) - self.cmd('create', '--exclude=input/file4', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude=input/file4', 'input') with changedir('output'): - self.cmd('extract', self.repository_location + '::test', 'input/file1', ) + self.cmd(f'--repo={self.repository_location}::test', 'extract', 'input/file1', ) self.assert_equal(sorted(os.listdir('output/input')), ['file1']) with changedir('output'): - self.cmd('extract', '--exclude=input/file2', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude=input/file2') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3']) with changedir('output'): - self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude-from=' + self.exclude_file_path) self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3']) def test_extract_include_exclude_regex(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('file3', size=1024 * 80) @@ -947,32 +947,32 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file333', size=1024 * 80) # Create with regular expression exclusion for file4 - self.cmd('create', '--exclude=re:input/file4$', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude=re:input/file4$', 'input') with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333']) shutil.rmtree('output/input') # Extract with regular expression exclusion with changedir('output'): - self.cmd('extract', '--exclude=re:file3+', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude=re:file3+') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2']) shutil.rmtree('output/input') # Combine --exclude with fnmatch and regular expression with changedir('output'): - self.cmd('extract', '--exclude=input/file2', '--exclude=re:file[01]', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude=input/file2', '--exclude=re:file[01]') self.assert_equal(sorted(os.listdir('output/input')), ['file3', 'file333']) shutil.rmtree('output/input') # Combine --exclude-from and regular expression exclusion with changedir('output'): - self.cmd('extract', '--exclude-from=' + self.exclude_file_path, '--exclude=re:file1', - '--exclude=re:file(\\d)\\1\\1$', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude-from=' + self.exclude_file_path, + '--exclude=re:file1', '--exclude=re:file(\\d)\\1\\1$') self.assert_equal(sorted(os.listdir('output/input')), ['file3']) def test_extract_include_exclude_regex_from_file(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('file3', size=1024 * 80) @@ -985,9 +985,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b're:input/file4$\n') fd.write(b'fm:*aa:*thing\n') - self.cmd('create', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude-from=' + self.exclude_file_path, 'input') with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333']) shutil.rmtree('output/input') @@ -996,7 +996,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b're:file3+\n') with changedir('output'): - self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude-from=' + self.exclude_file_path) self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2']) shutil.rmtree('output/input') @@ -1008,78 +1008,78 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b're:file2$\n') with changedir('output'): - self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude-from=' + self.exclude_file_path) self.assert_equal(sorted(os.listdir('output/input')), ['file3']) def test_extract_with_pattern(self): - self.cmd("init", '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', "init", '--encryption=repokey') self.create_regular_file("file1", size=1024 * 80) self.create_regular_file("file2", size=1024 * 80) self.create_regular_file("file3", size=1024 * 80) self.create_regular_file("file4", size=1024 * 80) self.create_regular_file("file333", size=1024 * 80) - self.cmd("create", self.repository_location + "::test", "input") + self.cmd(f'--repo={self.repository_location}::test', "create", "input") # Extract everything with regular expression with changedir("output"): - self.cmd("extract", self.repository_location + "::test", "re:.*") + self.cmd(f'--repo={self.repository_location}::test', "extract", "re:.*") self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file3", "file333", "file4"]) shutil.rmtree("output/input") # Extract with pattern while also excluding files with changedir("output"): - self.cmd("extract", "--exclude=re:file[34]$", self.repository_location + "::test", r"re:file\d$") + self.cmd(f'--repo={self.repository_location}::test', "extract", "--exclude=re:file[34]$", r"re:file\d$") self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2"]) shutil.rmtree("output/input") # Combine --exclude with pattern for extraction with changedir("output"): - self.cmd("extract", "--exclude=input/file1", self.repository_location + "::test", "re:file[12]$") + self.cmd(f'--repo={self.repository_location}::test', "extract", "--exclude=input/file1", "re:file[12]$") self.assert_equal(sorted(os.listdir("output/input")), ["file2"]) shutil.rmtree("output/input") # Multiple pattern with changedir("output"): - self.cmd("extract", self.repository_location + "::test", "fm:input/file1", "fm:*file33*", "input/file2") + self.cmd(f'--repo={self.repository_location}::test', "extract", "fm:input/file1", "fm:*file33*", "input/file2") self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"]) def test_extract_list_output(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file', size=1024 * 80) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with changedir('output'): - output = self.cmd('extract', self.repository_location + '::test') + output = self.cmd(f'--repo={self.repository_location}::test', 'extract') self.assert_not_in("input/file", output) shutil.rmtree('output/input') with changedir('output'): - output = self.cmd('extract', '--info', self.repository_location + '::test') + output = self.cmd(f'--repo={self.repository_location}::test', 'extract', '--info') self.assert_not_in("input/file", output) shutil.rmtree('output/input') with changedir('output'): - output = self.cmd('extract', '--list', self.repository_location + '::test') + output = self.cmd(f'--repo={self.repository_location}::test', 'extract', '--list') self.assert_in("input/file", output) shutil.rmtree('output/input') with changedir('output'): - output = self.cmd('extract', '--list', '--info', self.repository_location + '::test') + output = self.cmd(f'--repo={self.repository_location}::test', 'extract', '--list', '--info') self.assert_in("input/file", output) def test_extract_progress(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file', size=1024 * 80) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with changedir('output'): - output = self.cmd('extract', self.repository_location + '::test', '--progress') + output = self.cmd(f'--repo={self.repository_location}::test', 'extract', '--progress') assert 'Extracting:' in output def _create_test_caches(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('cache1/%s' % CACHE_TAG_NAME, contents=CACHE_TAG_CONTENTS + b' extra stuff') @@ -1093,122 +1093,122 @@ class ArchiverTestCase(ArchiverTestCaseBase): contents=CACHE_TAG_CONTENTS + b' extra stuff') def test_create_stdin(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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')) + self.cmd(f'--repo={self.repository_location}::test', 'create', '-', input=input_data) + item = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines')) 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) + extracted_data = self.cmd(f'--repo={self.repository_location}::test', 'extract', '--stdout', binary_output=True) assert extracted_data == input_data def test_create_content_from_command(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') input_data = 'some test content' name = 'a/b/c' - self.cmd('create', '--stdin-name', name, '--content-from-command', - self.repository_location + '::test', '--', 'echo', input_data) - item = json.loads(self.cmd('list', '--json-lines', self.repository_location + '::test')) + self.cmd(f'--repo={self.repository_location}::test', 'create', '--stdin-name', name, '--content-from-command', + '--', 'echo', input_data) + item = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines')) assert item['uid'] == 0 assert item['gid'] == 0 assert item['size'] == len(input_data) + 1 # `echo` adds newline assert item['path'] == name - extracted_data = self.cmd('extract', '--stdout', self.repository_location + '::test') + extracted_data = self.cmd(f'--repo={self.repository_location}::test', 'extract', '--stdout') assert extracted_data == input_data + '\n' def test_create_content_from_command_with_failed_command(self): - self.cmd('init', '--encryption=repokey', self.repository_location) - output = self.cmd('create', '--content-from-command', self.repository_location + '::test', + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}::test', 'create', '--content-from-command', '--', 'sh', '-c', 'exit 73;', exit_code=2) assert output.endswith("Command 'sh' exited with status 73\n") - archive_list = json.loads(self.cmd('list', '--json', self.repository_location)) + archive_list = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--json')) assert archive_list['archives'] == [] def test_create_content_from_command_missing_command(self): - self.cmd('init', '--encryption=repokey', self.repository_location) - output = self.cmd('create', '--content-from-command', self.repository_location + '::test', exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}::test', 'create', '--content-from-command', exit_code=2) assert output.endswith('No command given.\n') def test_create_paths_from_stdin(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file("file1", size=1024 * 80) self.create_regular_file("dir1/file2", size=1024 * 80) self.create_regular_file("dir1/file3", size=1024 * 80) self.create_regular_file("file4", size=1024 * 80) input_data = b'input/file1\0input/dir1\0input/file4' - self.cmd('create', '--paths-from-stdin', '--paths-delimiter', '\\0', - self.repository_location + '::test', input=input_data) - archive_list = self.cmd('list', '--json-lines', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'create', + '--paths-from-stdin', '--paths-delimiter', '\\0', input=input_data) + archive_list = self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines') paths = [json.loads(line)['path'] for line in archive_list.split('\n') if line] assert paths == ['input/file1', 'input/dir1', 'input/file4'] def test_create_paths_from_command(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file("file1", size=1024 * 80) self.create_regular_file("file2", size=1024 * 80) self.create_regular_file("file3", size=1024 * 80) self.create_regular_file("file4", size=1024 * 80) input_data = 'input/file1\ninput/file2\ninput/file3' - self.cmd('create', '--paths-from-command', - self.repository_location + '::test', '--', 'echo', input_data) - archive_list = self.cmd('list', '--json-lines', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--paths-from-command', + '--', 'echo', input_data) + archive_list = self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines') paths = [json.loads(line)['path'] for line in archive_list.split('\n') if line] assert paths == ['input/file1', 'input/file2', 'input/file3'] def test_create_paths_from_command_with_failed_command(self): - self.cmd('init', '--encryption=repokey', self.repository_location) - output = self.cmd('create', '--paths-from-command', self.repository_location + '::test', + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}::test', 'create', '--paths-from-command', '--', 'sh', '-c', 'exit 73;', exit_code=2) assert output.endswith("Command 'sh' exited with status 73\n") - archive_list = json.loads(self.cmd('list', '--json', self.repository_location)) + archive_list = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--json')) assert archive_list['archives'] == [] def test_create_paths_from_command_missing_command(self): - self.cmd('init', '--encryption=repokey', self.repository_location) - output = self.cmd('create', '--paths-from-command', self.repository_location + '::test', exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}::test', 'create', '--paths-from-command', exit_code=2) assert output.endswith('No command given.\n') def test_create_without_root(self): """test create without a root""" - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', exit_code=2) def test_create_pattern_root(self): """test create with only a root pattern""" - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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') + output = self.cmd(f'--repo={self.repository_location}::test', 'create', '-v', '--list', '--pattern=R input') 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', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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', + output = self.cmd(f'--repo={self.repository_location}::test', 'create', '-v', '--list', '--pattern=+input/file_important', '--pattern=-input/file*', - self.repository_location + '::test', 'input') + '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.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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', + output = self.cmd(f'--repo={self.repository_location}::test', 'create', '-v', '--list', '--pattern=-input/otherfile', '--patterns-from=' + self.patterns_file_path, - self.repository_location + '::test', 'input') + 'input') self.assert_in("A input/file_important", output) self.assert_in('x input/file1', output) self.assert_in('x input/file2', output) @@ -1220,13 +1220,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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', + output = self.cmd(f'--repo={self.repository_location}::test', 'create', '-v', '--list', '--patterns-from=' + self.patterns_file_path2, - self.repository_location + '::test', 'input') + '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) @@ -1237,13 +1237,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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', + output = self.cmd(f'--repo={self.repository_location}::test', 'create', '-v', '--list', '--patterns-from=' + self.patterns_file_path2, - self.repository_location + '::test', 'input') + '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) @@ -1254,17 +1254,17 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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', '.') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--patterns-from=' + self.patterns_file_path2, + '.') # 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 = self.cmd(f'--repo={self.repository_location}::test', 'list', '--format', '{type} {path}{NL}') out_list = out.splitlines() self.assert_in('d x/a', out_list) @@ -1275,50 +1275,49 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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', + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') + create_json = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'create', '--no-cache-sync', 'input', '--json', '--error')) # ignore experimental warning - info_json = json.loads(self.cmd('info', self.repository_location + '::test', '--json')) + info_json = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'info', '--json')) 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) + self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') + self.cmd(f'--repo={self.repository_location}::test2', 'create', '--no-cache-sync', 'input') + self.cmd(f'--repo={self.repository_location}', 'info') + self.cmd(f'--repo={self.repository_location}', 'check') def test_extract_pattern_opt(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with changedir('output'): - self.cmd('extract', - '--pattern=+input/file_important', '--pattern=-input/file*', - self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract', + '--pattern=+input/file_important', '--pattern=-input/file*') self.assert_equal(sorted(os.listdir('output/input')), ['file_important']) def _assert_test_caches(self): with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') self.assert_equal(sorted(os.listdir('output/input')), ['cache2', 'file1']) self.assert_equal(sorted(os.listdir('output/input/cache2')), [CACHE_TAG_NAME]) def test_exclude_caches(self): self._create_test_caches() - self.cmd('create', '--exclude-caches', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude-caches', 'input') self._assert_test_caches() def test_recreate_exclude_caches(self): self._create_test_caches() - self.cmd('create', self.repository_location + '::test', 'input') - self.cmd('recreate', '--exclude-caches', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'recreate', '--exclude-caches') self._assert_test_caches() def _create_test_tagged(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('tagged1/.NOBACKUP') self.create_regular_file('tagged2/00-NOBACKUP') @@ -1326,23 +1325,24 @@ class ArchiverTestCase(ArchiverTestCaseBase): def _assert_test_tagged(self): with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') self.assert_equal(sorted(os.listdir('output/input')), ['file1']) def test_exclude_tagged(self): self._create_test_tagged() - self.cmd('create', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude-if-present', '.NOBACKUP', + '--exclude-if-present', '00-NOBACKUP', 'input') self._assert_test_tagged() def test_recreate_exclude_tagged(self): self._create_test_tagged() - self.cmd('create', self.repository_location + '::test', 'input') - self.cmd('recreate', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP', - self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'recreate', '--exclude-if-present', '.NOBACKUP', + '--exclude-if-present', '00-NOBACKUP') self._assert_test_tagged() def _create_test_keep_tagged(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file0', size=1024) self.create_regular_file('tagged1/.NOBACKUP1') self.create_regular_file('tagged1/file1', size=1024) @@ -1359,7 +1359,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def _assert_test_keep_tagged(self): with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') self.assert_equal(sorted(os.listdir('output/input')), ['file0', 'tagged1', 'tagged2', 'tagged3', 'taggedall']) self.assert_equal(os.listdir('output/input/tagged1'), ['.NOBACKUP1']) self.assert_equal(os.listdir('output/input/tagged2'), ['.NOBACKUP2']) @@ -1369,29 +1369,29 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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-exclude-tags', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude-if-present', '.NOBACKUP1', + '--exclude-if-present', '.NOBACKUP2', '--exclude-caches', '--keep-exclude-tags', '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-exclude-tags', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'recreate', '--exclude-if-present', '.NOBACKUP1', + '--exclude-if-present', '.NOBACKUP2', '--exclude-caches', '--keep-exclude-tags') self._assert_test_keep_tagged() @pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported') def test_recreate_hardlinked_tags(self): # test for issue #4911 - self.cmd('init', '--encryption=none', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self.create_regular_file('file1', contents=CACHE_TAG_CONTENTS) # "wrong" filename, but correct tag contents os.mkdir(os.path.join(self.input_path, 'subdir')) # to make sure the tag is encountered *after* file1 os.link(os.path.join(self.input_path, 'file1'), os.path.join(self.input_path, 'subdir', CACHE_TAG_NAME)) # correct tag name, hardlink to file1 - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') # in the "test" archive, we now have, in this order: # - a regular file item for "file1" # - a hardlink item for "CACHEDIR.TAG" referring back to file1 for its contents - self.cmd('recreate', '--exclude-caches', '--keep-exclude-tags', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'recreate', '--exclude-caches', '--keep-exclude-tags') # if issue #4911 is present, the recreate will crash with a KeyError for "input/file1" @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='Linux capabilities test, requires fakeroot >= 1.20.2') @@ -1408,11 +1408,11 @@ 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(b'input/file', b'security.capability', capabilities) - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with changedir('output'): with patch.object(os, 'fchown', patched_fchown): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') assert xattr.getxattr(b'input/file', b'security.capability') == capabilities @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='xattr not supported on this system or on this version of' @@ -1429,88 +1429,88 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file') xattr.setxattr(b'input/file', b'user.attribute', b'value') - self.cmd('init', self.repository_location, '-e' 'none') - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '-e' 'none') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with changedir('output'): 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) + out = self.cmd(f'--repo={self.repository_location}::test', 'extract', exit_code=EXIT_WARNING) assert ': when setting extended attribute user.attribute: too big for this filesystem\n' in out os.remove(input_abspath) with patch.object(xattr, 'setxattr', patched_setxattr_ENOTSUP): - out = self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING) + out = self.cmd(f'--repo={self.repository_location}::test', 'extract', exit_code=EXIT_WARNING) assert ': when setting extended attribute user.attribute: xattrs not supported on this filesystem\n' in out os.remove(input_abspath) with patch.object(xattr, 'setxattr', patched_setxattr_EACCES): - out = self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING) + out = self.cmd(f'--repo={self.repository_location}::test', 'extract', exit_code=EXIT_WARNING) assert ': when setting extended attribute user.attribute: Permission denied\n' in out assert os.path.isfile(input_abspath) def test_path_normalization(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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/..') - output = self.cmd('list', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'create', '../../../input/dir1/../dir1/dir2/..') + output = self.cmd(f'--repo={self.repository_location}::test', 'list') self.assert_not_in('..', output) self.assert_in(' input/dir1/dir2/file', output) def test_exclude_normalization(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) with changedir('input'): - self.cmd('create', '--exclude=file1', self.repository_location + '::test1', '.') + self.cmd(f'--repo={self.repository_location}::test1', 'create', '--exclude=file1', '.') with changedir('output'): - self.cmd('extract', self.repository_location + '::test1') + self.cmd(f'--repo={self.repository_location}::test1', 'extract') self.assert_equal(sorted(os.listdir('output')), ['file2']) with changedir('input'): - self.cmd('create', '--exclude=./file1', self.repository_location + '::test2', '.') + self.cmd(f'--repo={self.repository_location}::test2', 'create', '--exclude=./file1', '.') with changedir('output'): - self.cmd('extract', self.repository_location + '::test2') + self.cmd(f'--repo={self.repository_location}::test2', 'extract') self.assert_equal(sorted(os.listdir('output')), ['file2']) - self.cmd('create', '--exclude=input/./file1', self.repository_location + '::test3', 'input') + self.cmd(f'--repo={self.repository_location}::test3', 'create', '--exclude=input/./file1', 'input') with changedir('output'): - self.cmd('extract', self.repository_location + '::test3') + self.cmd(f'--repo={self.repository_location}::test3', 'extract') self.assert_equal(sorted(os.listdir('output/input')), ['file2']) def test_repeated_files(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', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', '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', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') # Overwriting regular files and directories should be supported os.mkdir('output/input') os.mkdir('output/input/file1') os.mkdir('output/input/dir2') with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') self.assert_dirs_equal('input', 'output/input') # But non-empty dirs should fail os.unlink('output/input/file1') os.mkdir('output/input/file1') os.mkdir('output/input/file1/dir') with changedir('output'): - self.cmd('extract', self.repository_location + '::test', exit_code=1) + self.cmd(f'--repo={self.repository_location}::test', 'extract', exit_code=1) def test_rename(self): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('dir2/file2', size=1024 * 80) - 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') - self.cmd('extract', '--dry-run', self.repository_location + '::test.2') - self.cmd('rename', self.repository_location + '::test', 'test.3') - self.cmd('extract', '--dry-run', self.repository_location + '::test.2') - self.cmd('rename', self.repository_location + '::test.2', 'test.4') - self.cmd('extract', '--dry-run', self.repository_location + '::test.3') - self.cmd('extract', '--dry-run', self.repository_location + '::test.4') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--dry-run') + self.cmd(f'--repo={self.repository_location}::test.2', 'extract', '--dry-run') + self.cmd(f'--repo={self.repository_location}::test', 'rename', 'TODO_test', 'test.3') + self.cmd(f'--repo={self.repository_location}::test.2', 'extract', '--dry-run') + self.cmd(f'--repo={self.repository_location}::test.2', 'rename', 'TODO_test.2', 'test.4') + self.cmd(f'--repo={self.repository_location}::test.3', 'extract', '--dry-run') + self.cmd(f'--repo={self.repository_location}::test.4', 'extract', '--dry-run') # Make sure both archives have been renamed with Repository(self.repository_path) as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -1520,20 +1520,20 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_info(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 = self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + info_repo = self.cmd(f'--repo={self.repository_location}', 'info') assert 'All archives:' in info_repo - info_archive = self.cmd('info', self.repository_location + '::test') + info_archive = self.cmd(f'--repo={self.repository_location}::test', 'info') assert 'Archive name: test\n' in info_archive - info_archive = self.cmd('info', '--first', '1', self.repository_location) + info_archive = self.cmd(f'--repo={self.repository_location}', 'info', '--first', '1') 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)) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + info_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--json')) repository = info_repo['repository'] assert len(repository['id']) == 64 assert 'last_modified' in repository @@ -1545,7 +1545,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert all(isinstance(o, int) for o in stats.values()) assert all(key in stats for key in ('total_chunks', 'total_size', 'total_unique_chunks', 'unique_size')) - info_archive = json.loads(self.cmd('info', '--json', self.repository_location + '::test')) + info_archive = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'info', '--json')) assert info_repo['repository'] == info_archive['repository'] assert info_repo['cache'] == info_archive['cache'] archives = info_archive['archives'] @@ -1561,47 +1561,47 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_info_json_of_empty_archive(self): """See https://github.com/borgbackup/borg/issues/6120""" - self.cmd('init', '--encryption=repokey', self.repository_location) - info_repo = json.loads(self.cmd('info', '--json', '--first=1', self.repository_location)) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + info_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--json', '--first=1')) assert info_repo["archives"] == [] - info_repo = json.loads(self.cmd('info', '--json', '--last=1', self.repository_location)) + info_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--json', '--last=1')) assert info_repo["archives"] == [] def test_comment(self): self.create_regular_file('file1', size=1024 * 80) - 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') - self.cmd('create', '--comment', 'preserved comment', self.repository_location + '::test4', 'input') - assert 'Comment: \n' in self.cmd('info', self.repository_location + '::test1') - assert 'Comment: this is the comment' in self.cmd('info', self.repository_location + '::test2') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test1', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test2', 'create', '--comment', 'this is the comment', 'input') + self.cmd(f'--repo={self.repository_location}::test3', 'create', '--comment', '"deleted" comment', 'input') + self.cmd(f'--repo={self.repository_location}::test4', 'create', '--comment', 'preserved comment', 'input') + assert 'Comment: \n' in self.cmd(f'--repo={self.repository_location}::test1', 'info') + assert 'Comment: this is the comment' in self.cmd(f'--repo={self.repository_location}::test2', 'info') - self.cmd('recreate', self.repository_location + '::test1', '--comment', 'added comment') - self.cmd('recreate', self.repository_location + '::test2', '--comment', 'modified comment') - self.cmd('recreate', self.repository_location + '::test3', '--comment', '') - self.cmd('recreate', self.repository_location + '::test4', '12345') - assert 'Comment: added comment' in self.cmd('info', self.repository_location + '::test1') - assert 'Comment: modified comment' in self.cmd('info', self.repository_location + '::test2') - assert 'Comment: \n' in self.cmd('info', self.repository_location + '::test3') - assert 'Comment: preserved comment' in self.cmd('info', self.repository_location + '::test4') + self.cmd(f'--repo={self.repository_location}::test1', 'recreate', '--comment', 'added comment') + self.cmd(f'--repo={self.repository_location}::test2', 'recreate', '--comment', 'modified comment') + self.cmd(f'--repo={self.repository_location}::test3', 'recreate', '--comment', '') + self.cmd(f'--repo={self.repository_location}::test4', 'recreate', '12345') + assert 'Comment: added comment' in self.cmd(f'--repo={self.repository_location}::test1', 'info') + assert 'Comment: modified comment' in self.cmd(f'--repo={self.repository_location}::test2', 'info') + assert 'Comment: \n' in self.cmd(f'--repo={self.repository_location}::test3', 'info') + assert 'Comment: preserved comment' in self.cmd(f'--repo={self.repository_location}::test4', 'info') def test_delete(self): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('dir2/file2', size=1024 * 80) - 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') - 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') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test.3', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::another_test.1', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::another_test.2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--dry-run') + self.cmd(f'--repo={self.repository_location}::test.2', 'extract', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'delete', '--prefix', 'another_') + self.cmd(f'--repo={self.repository_location}', 'delete', '--last', '1') + self.cmd(f'--repo={self.repository_location}::test', 'delete') + self.cmd(f'--repo={self.repository_location}::test.2', 'extract', '--dry-run') + output = self.cmd(f'--repo={self.repository_location}::test.2', 'delete', '--stats') self.assert_in('Deleted data:', output) # Make sure all data except the manifest has been deleted with Repository(self.repository_path) as repository: @@ -1609,31 +1609,31 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_delete_multiple(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test1', 'input') - self.cmd('create', self.repository_location + '::test2', 'input') - self.cmd('create', self.repository_location + '::test3', 'input') - self.cmd('delete', self.repository_location + '::test1', 'test2') - self.cmd('extract', '--dry-run', self.repository_location + '::test3') - self.cmd('delete', self.repository_location, 'test3') - assert not self.cmd('list', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test1', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test3', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test1', 'delete', 'test2') + self.cmd(f'--repo={self.repository_location}::test3', 'extract', '--dry-run') + self.cmd(f'--repo={self.repository_location}::test3', 'delete') + assert not self.cmd(f'--repo={self.repository_location}', 'list') 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', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') - self.cmd('create', self.repository_location + '::test.2', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'no' - self.cmd('delete', self.repository_location, exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'delete', exit_code=2) assert os.path.exists(self.repository_path) os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'YES' - self.cmd('delete', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'delete') # 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.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self.create_src_archive('test') with Repository(self.repository_path, exclusive=True) as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -1645,14 +1645,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): else: assert False # missed the file repository.commit(compact=False) - output = self.cmd('delete', '--force', self.repository_location + '::test') + output = self.cmd(f'--repo={self.repository_location}::test', 'delete', '--force') self.assert_in('deleted archive was corrupted', output) - self.cmd('check', '--repair', self.repository_location) - output = self.cmd('list', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'check', '--repair') + output = self.cmd(f'--repo={self.repository_location}', 'list') self.assert_not_in('test', output) def test_delete_double_force(self): - self.cmd('init', '--encryption=none', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self.create_src_archive('test') with Repository(self.repository_path, exclusive=True) as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -1660,16 +1660,16 @@ class ArchiverTestCase(ArchiverTestCaseBase): id = archive.metadata.items[0] repository.put(id, b'corrupted items metadata stream chunk') repository.commit(compact=False) - self.cmd('delete', '--force', '--force', self.repository_location + '::test') - self.cmd('check', '--repair', self.repository_location) - output = self.cmd('list', self.repository_location) + self.cmd(f'--repo={self.repository_location}::test', 'delete', '--force', '--force') + self.cmd(f'--repo={self.repository_location}', 'check', '--repair') + output = self.cmd(f'--repo={self.repository_location}', 'list') self.assert_not_in('test', output) def test_corrupted_repository(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('test') - self.cmd('extract', '--dry-run', self.repository_location + '::test') - output = self.cmd('check', '--show-version', self.repository_location) + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--dry-run') + output = self.cmd(f'--repo={self.repository_location}', 'check', '--show-version') self.assert_in('borgbackup version', output) # implied output even without --info given self.assert_not_in('Starting repository check', output) # --info not given for root logger @@ -1677,103 +1677,103 @@ class ArchiverTestCase(ArchiverTestCaseBase): with open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+b') as fd: fd.seek(100) fd.write(b'XXXX') - output = self.cmd('check', '--info', self.repository_location, exit_code=1) + output = self.cmd(f'--repo={self.repository_location}', 'check', '--info', exit_code=1) self.assert_in('Starting repository check', output) # --info given for root logger def test_readonly_check(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('test') with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd('check', '--verify-data', self.repository_location, exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'check', '--verify-data', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd('check', '--verify-data', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'check', '--verify-data') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd('check', '--verify-data', self.repository_location, '--bypass-lock') + self.cmd(f'--repo={self.repository_location}', 'check', '--verify-data', '--bypass-lock') def test_readonly_diff(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('a') self.create_src_archive('b') with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd('diff', '%s::a' % self.repository_location, 'b', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}::a', 'diff', 'TODO_a', 'b', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd('diff', '%s::a' % self.repository_location, 'b') + self.cmd(f'--repo={self.repository_location}::a', 'diff', 'TODO_a', 'b') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd('diff', '%s::a' % self.repository_location, 'b', '--bypass-lock') + self.cmd(f'--repo={self.repository_location}::a', 'diff', 'TODO_a', 'b', '--bypass-lock') def test_readonly_export_tar(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('test') with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd('export-tar', '%s::test' % self.repository_location, 'test.tar', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'test.tar', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd('export-tar', '%s::test' % self.repository_location, 'test.tar') + self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'test.tar') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd('export-tar', '%s::test' % self.repository_location, 'test.tar', '--bypass-lock') + self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'test.tar', '--bypass-lock') def test_readonly_extract(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('test') with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd('extract', '%s::test' % self.repository_location, exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}::test', 'extract', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd('extract', '%s::test' % self.repository_location) + self.cmd(f'--repo={self.repository_location}::test', 'extract') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd('extract', '%s::test' % self.repository_location, '--bypass-lock') + self.cmd(f'--repo={self.repository_location}::test', 'extract', '--bypass-lock') def test_readonly_info(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('test') with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd('info', self.repository_location, exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'info', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'info') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd('info', self.repository_location, '--bypass-lock') + self.cmd(f'--repo={self.repository_location}', 'info', '--bypass-lock') def test_readonly_list(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('test') with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd('list', self.repository_location, exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'list', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd('list', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'list') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd('list', self.repository_location, '--bypass-lock') + self.cmd(f'--repo={self.repository_location}', 'list', '--bypass-lock') @unittest.skipUnless(llfuse, 'llfuse not installed') def test_readonly_mount(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('test') with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo @@ -1794,14 +1794,14 @@ 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', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', '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', '--encryption=repokey', self.repository_location) - self.cmd('create', '--dry-run', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--dry-run', 'input') # Make sure no archive has been created with Repository(self.repository_path) as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -1823,56 +1823,56 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert excinfo.value.args == (['unknown-feature'],) def test_unknown_feature_on_create(self): - print(self.cmd('init', '--encryption=repokey', self.repository_location)) + print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) self.add_unknown_feature(Manifest.Operation.WRITE) - self.cmd_raises_unknown_feature(['create', self.repository_location + '::test', 'input']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}::test', 'create', '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.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') self.add_unknown_feature(Manifest.Operation.READ) - self.cmd_raises_unknown_feature(['create', self.repository_location + '::test', 'input']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}::test', 'create', 'input']) def test_unknown_feature_on_change_passphrase(self): - print(self.cmd('init', '--encryption=repokey', self.repository_location)) + print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) self.add_unknown_feature(Manifest.Operation.CHECK) - self.cmd_raises_unknown_feature(['key', 'change-passphrase', self.repository_location]) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'key', 'change-passphrase']) def test_unknown_feature_on_read(self): - print(self.cmd('init', '--encryption=repokey', self.repository_location)) - self.cmd('create', self.repository_location + '::test', 'input') + print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) + self.cmd(f'--repo={self.repository_location}::test', 'create', '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([f'--repo={self.repository_location}::test', 'extract']) - self.cmd_raises_unknown_feature(['list', self.repository_location]) - self.cmd_raises_unknown_feature(['info', self.repository_location + '::test']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'list']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}::test', 'info']) def test_unknown_feature_on_rename(self): - print(self.cmd('init', '--encryption=repokey', self.repository_location)) - self.cmd('create', self.repository_location + '::test', 'input') + print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') self.add_unknown_feature(Manifest.Operation.CHECK) - self.cmd_raises_unknown_feature(['rename', self.repository_location + '::test', 'other']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}::test', 'rename', 'TODO_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') + print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) + self.cmd(f'--repo={self.repository_location}::test', 'create', '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]) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}::test', 'delete']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'prune', '--keep-daily=3']) # delete of the whole repository ignores features - self.cmd('delete', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'delete') @unittest.skipUnless(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.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', '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]) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}::test', 'mount', mountpoint]) @pytest.mark.allow_cache_wipe def test_unknown_mandatory_feature_in_cache(self): @@ -1881,7 +1881,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): else: path_prefix = '' - print(self.cmd('init', '--encryption=repokey', self.repository_location)) + print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) with Repository(self.repository_path, exclusive=True) as repository: if path_prefix: @@ -1893,7 +1893,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): cache.commit() if self.FORK_DEFAULT: - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') else: called = False wipe_cache_safe = LocalCache.wipe_cache @@ -1904,7 +1904,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): wipe_cache_safe(*args) with patch.object(LocalCache, 'wipe_cache', wipe_wrapper): - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') assert called @@ -1917,14 +1917,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_progress_on(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd('init', '--encryption=repokey', self.repository_location) - output = self.cmd('create', '--progress', self.repository_location + '::test4', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}::test4', 'create', '--progress', 'input') self.assert_in("\r", output) def test_progress_off(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd('init', '--encryption=repokey', self.repository_location) - output = self.cmd('create', self.repository_location + '::test5', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}::test5', 'create', 'input') self.assert_not_in("\r", output) def test_file_status(self): @@ -1934,12 +1934,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=1024 * 80) - self.cmd('init', '--encryption=repokey', self.repository_location) - output = self.cmd('create', '--list', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}::test', 'create', '--list', 'input') self.assert_in("A input/file1", output) self.assert_in("A input/file2", output) # should find first file as unmodified - output = self.cmd('create', '--list', self.repository_location + '::test1', 'input') + output = self.cmd(f'--repo={self.repository_location}::test1', 'create', '--list', 'input') self.assert_in("U input/file1", output) # this is expected, although surprising, for why, see: # https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file @@ -1950,14 +1950,16 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', contents=b'123') time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=10) - self.cmd('init', '--encryption=repokey', self.repository_location) - output = self.cmd('create', '--list', '--files-cache=ctime,size', self.repository_location + '::test1', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}::test1', 'create', + '--list', '--files-cache=ctime,size', 'input') # modify file1, but cheat with the mtime (and atime) and also keep same size: st = os.stat('input/file1') self.create_regular_file('file1', contents=b'321') os.utime('input/file1', ns=(st.st_atime_ns, st.st_mtime_ns)) # this mode uses ctime for change detection, so it should find file1 as modified - output = self.cmd('create', '--list', '--files-cache=ctime,size', self.repository_location + '::test2', 'input') + output = self.cmd(f'--repo={self.repository_location}::test2', 'create', + '--list', '--files-cache=ctime,size', 'input') self.assert_in("M input/file1", output) def test_file_status_ms_cache_mode(self): @@ -1965,13 +1967,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=10) time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=10) - self.cmd('init', '--encryption=repokey', self.repository_location) - output = self.cmd('create', '--list', '--files-cache=mtime,size', self.repository_location + '::test1', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}::test1', 'create', + '--list', '--files-cache=mtime,size', 'input') # change mode of file1, no content change: st = os.stat('input/file1') os.chmod('input/file1', st.st_mode ^ stat.S_IRWXO) # this triggers a ctime change, but mtime is unchanged # this mode uses mtime for change detection, so it should find file1 as unmodified - output = self.cmd('create', '--list', '--files-cache=mtime,size', self.repository_location + '::test2', 'input') + output = self.cmd(f'--repo={self.repository_location}::test2', 'create', + '--list', '--files-cache=mtime,size', 'input') self.assert_in("U input/file1", output) def test_file_status_rc_cache_mode(self): @@ -1979,10 +1983,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=10) time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=10) - self.cmd('init', '--encryption=repokey', self.repository_location) - output = self.cmd('create', '--list', '--files-cache=rechunk,ctime', self.repository_location + '::test1', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}::test1', 'create', + '--list', '--files-cache=rechunk,ctime', 'input') # no changes here, but this mode rechunks unconditionally - output = self.cmd('create', '--list', '--files-cache=rechunk,ctime', self.repository_location + '::test2', 'input') + output = self.cmd(f'--repo={self.repository_location}::test2', 'create', + '--list', '--files-cache=rechunk,ctime', 'input') self.assert_in("A input/file1", output) def test_file_status_excluded(self): @@ -1994,14 +2000,15 @@ 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', '--encryption=repokey', self.repository_location) - output = self.cmd('create', '--list', '--exclude-nodump', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}::test', 'create', '--list', '--exclude-nodump', 'input') self.assert_in("A input/file1", output) self.assert_in("A input/file2", output) if has_lchflags: self.assert_in("x input/file3", output) # should find second file as excluded - output = self.cmd('create', '--list', '--exclude-nodump', self.repository_location + '::test1', 'input', '--exclude', '*/file2') + output = self.cmd(f'--repo={self.repository_location}::test1', 'create', + '--list', '--exclude-nodump', 'input', '--exclude', '*/file2') self.assert_in("U input/file1", output) self.assert_in("x input/file2", output) if has_lchflags: @@ -2009,8 +2016,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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')) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + create_info = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'create', + '--json', 'input')) # The usual keys assert 'encryption' in create_info assert 'repository' in create_info @@ -2028,23 +2036,23 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=1024 * 80) - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') # no listing by default - output = self.cmd('create', self.repository_location + '::test', 'input') + output = self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') self.assert_not_in('file1', output) # shouldn't be listed even if unchanged - output = self.cmd('create', self.repository_location + '::test0', 'input') + output = self.cmd(f'--repo={self.repository_location}::test0', 'create', 'input') self.assert_not_in('file1', output) # should list the file as unchanged - output = self.cmd('create', '--list', '--filter=U', self.repository_location + '::test1', 'input') + output = self.cmd(f'--repo={self.repository_location}::test1', 'create', '--list', '--filter=U', 'input') self.assert_in('file1', output) # should *not* list the file as changed - output = self.cmd('create', '--list', '--filter=AM', self.repository_location + '::test2', 'input') + output = self.cmd(f'--repo={self.repository_location}::test2', 'create', '--list', '--filter=AM', 'input') self.assert_not_in('file1', output) # change the file self.create_regular_file('file1', size=1024 * 100) # should list the file as changed - output = self.cmd('create', '--list', '--filter=AM', self.repository_location + '::test3', 'input') + output = self.cmd(f'--repo={self.repository_location}::test3', 'create', '--list', '--filter=AM', 'input') self.assert_in('file1', output) @pytest.mark.skipif(not are_fifos_supported(), reason='FIFOs not supported') @@ -2058,8 +2066,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): finally: os.close(fd) - self.cmd('init', '--encryption=repokey', self.repository_location) - archive = self.repository_location + '::test' + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') data = b'foobar' * 1000 fifo_fn = os.path.join(self.input_path, 'fifo') @@ -2070,11 +2077,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): t = Thread(target=fifo_feeder, args=(fifo_fn, data)) t.start() try: - self.cmd('create', '--read-special', archive, 'input/link_fifo') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--read-special', 'input/link_fifo') finally: t.join() with changedir('output'): - self.cmd('extract', archive) + self.cmd(f'--repo={self.repository_location}::test', 'extract') fifo_fn = 'input/link_fifo' with open(fifo_fn, 'rb') as f: extracted_data = f.read() @@ -2082,41 +2089,40 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_read_special_broken_symlink(self): os.symlink('somewhere does not exist', os.path.join(self.input_path, 'link')) - 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) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', '--read-special', 'input') + output = self.cmd(f'--repo={self.repository_location}::test', 'list') assert 'input/link -> somewhere does not exist' in output # def test_cmdline_compatibility(self): # self.create_regular_file('file1', size=1024 * 80) - # self.cmd('init', '--encryption=repokey', self.repository_location) + # self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') # 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', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test1', src_dir) - self.cmd('create', self.repository_location + '::test2', src_dir) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test1', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}::test2', 'create', src_dir) # these are not really a checkpoints, but they look like some: - self.cmd('create', self.repository_location + '::test3.checkpoint', src_dir) - self.cmd('create', self.repository_location + '::test3.checkpoint.1', src_dir) - self.cmd('create', self.repository_location + '::test4.checkpoint', src_dir) - output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=1') + self.cmd(f'--repo={self.repository_location}::test3.checkpoint', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}::test3.checkpoint.1', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}::test4.checkpoint', 'create', src_dir) + output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1') assert re.search(r'Would prune:\s+test1', output) # must keep the latest non-checkpoint archive: assert re.search(r'Keeping archive \(rule: daily #1\):\s+test2', output) # must keep the latest checkpoint archive: assert re.search(r'Keeping checkpoint archive:\s+test4.checkpoint', output) - output = self.cmd('list', '--consider-checkpoints', self.repository_location) + output = self.cmd(f'--repo={self.repository_location}', 'list', '--consider-checkpoints') self.assert_in('test1', output) self.assert_in('test2', output) self.assert_in('test3.checkpoint', output) self.assert_in('test3.checkpoint.1', output) self.assert_in('test4.checkpoint', output) - self.cmd('prune', self.repository_location, '--keep-daily=1') - output = self.cmd('list', '--consider-checkpoints', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=1') + output = self.cmd(f'--repo={self.repository_location}', 'list', '--consider-checkpoints') self.assert_not_in('test1', output) # the latest non-checkpoint archive must be still there: self.assert_in('test2', output) @@ -2125,9 +2131,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_not_in('test3.checkpoint.1', output) self.assert_in('test4.checkpoint', output) # now we supersede the latest checkpoint by a successful backup: - self.cmd('create', self.repository_location + '::test5', src_dir) - self.cmd('prune', self.repository_location, '--keep-daily=2') - output = self.cmd('list', '--consider-checkpoints', self.repository_location) + self.cmd(f'--repo={self.repository_location}::test5', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=2') + output = self.cmd(f'--repo={self.repository_location}', 'list', '--consider-checkpoints') # all checkpoints should be gone now: self.assert_not_in('checkpoint', output) # the latest archive must be still there @@ -2140,12 +2146,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): return dtime.astimezone(dateutil.tz.UTC).strftime("%Y-%m-%dT%H:%M:%S") def _create_archive_ts(self, name, y, m, d, H=0, M=0, S=0): - loc = self.repository_location + '::' + name - self.cmd('create', '--timestamp', self._to_utc_timestamp(y, m, d, H, M, S), loc, src_dir) + self.cmd(f'--repo={self.repository_location}::{name}', 'create', + '--timestamp', self._to_utc_timestamp(y, m, d, H, M, S), src_dir) # This test must match docs/misc/prune-example.txt def test_prune_repository_example(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') # Archives that will be kept, per the example # Oldest archive self._create_archive_ts('test01', 2015, 1, 1) @@ -2178,7 +2184,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self._create_archive_ts('test23', 2015, 5, 31) # The next older daily backup self._create_archive_ts('test24', 2015, 12, 16) - output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=14', '--keep-monthly=6', '--keep-yearly=1') + output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=14', '--keep-monthly=6', '--keep-yearly=1') # Prune second backup of the year assert re.search(r'Would prune:\s+test22', output) # Prune next older monthly and daily backups @@ -2191,12 +2197,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert re.search(r'Keeping archive \(rule: monthly #' + str(i) + r'\):\s+test' + ("%02d" % (8-i)), output) for i in range(1, 15): assert re.search(r'Keeping archive \(rule: daily #' + str(i) + r'\):\s+test' + ("%02d" % (22-i)), output) - output = self.cmd('list', self.repository_location) + output = self.cmd(f'--repo={self.repository_location}', 'list') # Nothing pruned after dry run for i in range(1, 25): self.assert_in('test%02d' % i, output) - self.cmd('prune', self.repository_location, '--keep-daily=14', '--keep-monthly=6', '--keep-yearly=1') - output = self.cmd('list', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=14', '--keep-monthly=6', '--keep-yearly=1') + output = self.cmd(f'--repo={self.repository_location}', 'list') # All matching backups plus oldest kept for i in range(1, 22): self.assert_in('test%02d' % i, output) @@ -2206,141 +2212,139 @@ class ArchiverTestCase(ArchiverTestCaseBase): # With an initial and daily backup, prune daily until oldest is replaced by a monthly backup def test_prune_retain_and_expire_oldest(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') # Initial backup self._create_archive_ts('original_archive', 2020, 9, 1, 11, 15) # Archive and prune daily for 30 days for i in range(1, 31): self._create_archive_ts('september%02d' % i, 2020, 9, i, 12) - self.cmd('prune', self.repository_location, '--keep-daily=7', '--keep-monthly=1') + self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=7', '--keep-monthly=1') # Archive and prune 6 days into the next month for i in range(1, 7): self._create_archive_ts('october%02d' % i, 2020, 10, i, 12) - self.cmd('prune', self.repository_location, '--keep-daily=7', '--keep-monthly=1') + self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=7', '--keep-monthly=1') # Oldest backup is still retained - output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=7', '--keep-monthly=1') + output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=7', '--keep-monthly=1') assert re.search(r'Keeping archive \(rule: monthly\[oldest\] #1' + r'\):\s+original_archive', output) # Archive one more day and prune. self._create_archive_ts('october07', 2020, 10, 7, 12) - self.cmd('prune', self.repository_location, '--keep-daily=7', '--keep-monthly=1') + self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=7', '--keep-monthly=1') # Last day of previous month is retained as monthly, and oldest is expired. - output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=7', '--keep-monthly=1') + output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=7', '--keep-monthly=1') assert re.search(r'Keeping archive \(rule: monthly #1\):\s+september30', output) self.assert_not_in('original_archive', output) def test_prune_repository_save_space(self): - 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', '--dry-run', self.repository_location, '--keep-daily=1') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test1', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}::test2', 'create', src_dir) + output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1') assert re.search(r'Keeping archive \(rule: daily #1\):\s+test2', output) assert re.search(r'Would prune:\s+test1', output) - output = self.cmd('list', self.repository_location) + output = self.cmd(f'--repo={self.repository_location}', 'list') self.assert_in('test1', output) self.assert_in('test2', output) - self.cmd('prune', '--save-space', self.repository_location, '--keep-daily=1') - output = self.cmd('list', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'prune', '--save-space', '--keep-daily=1') + output = self.cmd(f'--repo={self.repository_location}', 'list') self.assert_not_in('test1', output) self.assert_in('test2', output) def test_prune_repository_prefix(self): - 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) - self.cmd('create', self.repository_location + '::bar-2015-08-12-20:00', src_dir) - output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=1', '--prefix=foo-') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::foo-2015-08-12-10:00', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}::foo-2015-08-12-20:00', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}::bar-2015-08-12-10:00', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}::bar-2015-08-12-20:00', 'create', src_dir) + output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1', '--prefix=foo-') assert re.search(r'Keeping archive \(rule: daily #1\):\s+foo-2015-08-12-20:00', output) assert re.search(r'Would prune:\s+foo-2015-08-12-10:00', output) - output = self.cmd('list', self.repository_location) + output = self.cmd(f'--repo={self.repository_location}', 'list') self.assert_in('foo-2015-08-12-10:00', output) self.assert_in('foo-2015-08-12-20:00', output) self.assert_in('bar-2015-08-12-10:00', output) self.assert_in('bar-2015-08-12-20:00', output) - self.cmd('prune', self.repository_location, '--keep-daily=1', '--prefix=foo-') - output = self.cmd('list', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=1', '--prefix=foo-') + output = self.cmd(f'--repo={self.repository_location}', 'list') self.assert_not_in('foo-2015-08-12-10:00', output) self.assert_in('foo-2015-08-12-20:00', output) 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=1', '--glob-archives=2015-*-foo') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::2015-08-12-10:00-foo', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}::2015-08-12-20:00-foo', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}::2015-08-12-10:00-bar', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}::2015-08-12-20:00-bar', 'create', src_dir) + output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1', '--glob-archives=2015-*-foo') assert re.search(r'Keeping archive \(rule: daily #1\):\s+2015-08-12-20:00-foo', output) assert re.search(r'Would prune:\s+2015-08-12-10:00-foo', output) - output = self.cmd('list', self.repository_location) + output = self.cmd(f'--repo={self.repository_location}', 'list') 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=1', '--glob-archives=2015-*-foo') - output = self.cmd('list', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=1', '--glob-archives=2015-*-foo') + output = self.cmd(f'--repo={self.repository_location}', 'list') 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) - self.cmd('create', self.repository_location + '::something-else-than-test-1', src_dir) - self.cmd('create', self.repository_location + '::test-2', src_dir) - output = self.cmd('list', '--prefix=test-', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test-1', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}::something-else-than-test-1', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}::test-2', 'create', src_dir) + output = self.cmd(f'--repo={self.repository_location}', 'list', '--prefix=test-') self.assert_in('test-1', output) self.assert_in('test-2', output) self.assert_not_in('something-else', output) def test_list_format(self): - self.cmd('init', '--encryption=repokey', self.repository_location) - test_archive = self.repository_location + '::test' - self.cmd('create', test_archive, src_dir) - output_1 = self.cmd('list', test_archive) - output_2 = self.cmd('list', '--format', '{mode} {user:6} {group:6} {size:8d} {mtime} {path}{extra}{NEWLINE}', test_archive) - output_3 = self.cmd('list', '--format', '{mtime:%s} {path}{NL}', test_archive) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', src_dir) + output_1 = self.cmd(f'--repo={self.repository_location}::test', 'list') + output_2 = self.cmd(f'--repo={self.repository_location}::test', 'list', '--format', '{mode} {user:6} {group:6} {size:8d} {mtime} {path}{extra}{NEWLINE}') + output_3 = self.cmd(f'--repo={self.repository_location}::test', 'list', '--format', '{mtime:%s} {path}{NL}') self.assertEqual(output_1, output_2) self.assertNotEqual(output_1, output_3) def test_list_repository_format(self): - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', '--comment', 'comment 1', self.repository_location + '::test-1', src_dir) - self.cmd('create', '--comment', 'comment 2', self.repository_location + '::test-2', src_dir) - output_1 = self.cmd('list', self.repository_location) - output_2 = self.cmd('list', '--format', '{archive:<36} {time} [{id}]{NL}', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test-1', 'create', '--comment', 'comment 1', src_dir) + self.cmd(f'--repo={self.repository_location}::test-2', 'create', '--comment', 'comment 2', src_dir) + output_1 = self.cmd(f'--repo={self.repository_location}', 'list') + output_2 = self.cmd(f'--repo={self.repository_location}', 'list', '--format', '{archive:<36} {time} [{id}]{NL}') self.assertEqual(output_1, output_2) - output_1 = self.cmd('list', '--short', self.repository_location) + output_1 = self.cmd(f'--repo={self.repository_location}', 'list', '--short') self.assertEqual(output_1, 'test-1\ntest-2\n') - output_1 = self.cmd('list', '--format', '{barchive}/', self.repository_location) + output_1 = self.cmd(f'--repo={self.repository_location}', 'list', '--format', '{barchive}/') self.assertEqual(output_1, 'test-1/test-2/') - output_3 = self.cmd('list', '--format', '{name} {comment}{NL}', self.repository_location) + output_3 = self.cmd(f'--repo={self.repository_location}', 'list', '--format', '{name} {comment}{NL}') self.assert_in('test-1 comment 1\n', output_3) self.assert_in('test-2 comment 2\n', output_3) 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', '--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) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + output = self.cmd(f'--repo={self.repository_location}::test', 'list', '--format', '{sha256} {path}{NL}') assert "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0 input/amb" in output assert "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 input/empty_file" in output def test_list_consider_checkpoints(self): - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test1', 'create', src_dir) # these are not really a checkpoints, but they look like some: - self.cmd('create', self.repository_location + '::test2.checkpoint', src_dir) - self.cmd('create', self.repository_location + '::test3.checkpoint.1', src_dir) - output = self.cmd('list', self.repository_location) + self.cmd(f'--repo={self.repository_location}::test2.checkpoint', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}::test3.checkpoint.1', 'create', src_dir) + output = self.cmd(f'--repo={self.repository_location}', 'list') assert "test1" in output assert "test2.checkpoint" not in output assert "test3.checkpoint.1" not in output - output = self.cmd('list', '--consider-checkpoints', self.repository_location) + output = self.cmd(f'--repo={self.repository_location}', 'list', '--consider-checkpoints') assert "test1" in output assert "test2.checkpoint" in output assert "test3.checkpoint.1" in output @@ -2351,27 +2355,25 @@ 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', '--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) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + output = self.cmd(f'--repo={self.repository_location}::test', 'list', '--format', '{num_chunks} {unique_chunks} {path}{NL}') assert "0 0 input/empty_file" in output assert "2 2 input/two_chunks" in output def test_list_size(self): self.create_regular_file('compressible_file', size=10000) - 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} {path}{NL}', test_archive) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', '-C', 'lz4', 'input') + output = self.cmd(f'--repo={self.repository_location}::test', 'list', '--format', '{size} {path}{NL}') size, path = output.split("\n")[1].split(" ") assert int(size) == 10000 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)) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + list_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--json')) repository = list_repo['repository'] assert len(repository['id']) == 64 assert datetime.strptime(repository['last_modified'], ISO_FORMAT) # must not raise @@ -2380,7 +2382,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): archive0 = list_repo['archives'][0] assert datetime.strptime(archive0['time'], ISO_FORMAT) # must not raise - list_archive = self.cmd('list', '--json-lines', self.repository_location + '::test') + list_archive = self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines') items = [json.loads(s) for s in list_archive.splitlines()] assert len(items) == 2 file1 = items[1] @@ -2388,7 +2390,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert file1['size'] == 81920 assert datetime.strptime(file1['mtime'], ISO_FORMAT) # must not raise - list_archive = self.cmd('list', '--json-lines', '--format={sha256}', self.repository_location + '::test') + list_archive = self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines', '--format={sha256}') items = [json.loads(s) for s in list_archive.splitlines()] assert len(items) == 2 file1 = items[1] @@ -2396,14 +2398,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'list', '--json-lines', exit_code=2) + self.cmd(f'--repo={self.repository_location}::archive', 'list', '--json', 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') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + log = self.cmd(f'--repo={self.repository_location}::test', 'create', '--log-json', 'input', '--list', '--debug') messages = {} # type -> message, one of each kind for line in log.splitlines(): msg = json.loads(line) @@ -2420,67 +2422,67 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input', '--debug-profile=create.prof') self.cmd('debug', 'convert-profile', 'create.prof', 'create.pyprof') stats = pstats.Stats('create.pyprof') stats.strip_dirs() stats.sort_stats('cumtime') - self.cmd('create', self.repository_location + '::test2', 'input', '--debug-profile=create.pyprof') + self.cmd(f'--repo={self.repository_location}::test2', 'create', 'input', '--debug-profile=create.pyprof') stats = pstats.Stats('create.pyprof') # Only do this on trusted data! stats.strip_dirs() stats.sort_stats('cumtime') 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') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + log = self.cmd(f'--repo={self.repository_location}::test', '--debug', 'create', 'input') assert 'security: read previous location' in log def test_change_passphrase(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') os.environ['BORG_NEW_PASSPHRASE'] = 'newpassphrase' # here we have both BORG_PASSPHRASE and BORG_NEW_PASSPHRASE set: - self.cmd('key', 'change-passphrase', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'key', 'change-passphrase') os.environ['BORG_PASSPHRASE'] = 'newpassphrase' - self.cmd('list', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'list') def test_change_location_to_keyfile(self): - self.cmd('init', '--encryption=repokey', self.repository_location) - log = self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + log = self.cmd(f'--repo={self.repository_location}', 'info') assert '(repokey)' in log - self.cmd('key', 'change-location', self.repository_location, 'keyfile') - log = self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'keyfile') + log = self.cmd(f'--repo={self.repository_location}', 'info') assert '(key file)' in log def test_change_location_to_b2keyfile(self): - self.cmd('init', '--encryption=repokey-blake2', self.repository_location) - log = self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey-blake2') + log = self.cmd(f'--repo={self.repository_location}', 'info') assert '(repokey BLAKE2b)' in log - self.cmd('key', 'change-location', self.repository_location, 'keyfile') - log = self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'keyfile') + log = self.cmd(f'--repo={self.repository_location}', 'info') assert '(key file BLAKE2b)' in log def test_change_location_to_repokey(self): - self.cmd('init', '--encryption=keyfile', self.repository_location) - log = self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=keyfile') + log = self.cmd(f'--repo={self.repository_location}', 'info') assert '(key file)' in log - self.cmd('key', 'change-location', self.repository_location, 'repokey') - log = self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'repokey') + log = self.cmd(f'--repo={self.repository_location}', 'info') assert '(repokey)' in log def test_change_location_to_b2repokey(self): - self.cmd('init', '--encryption=keyfile-blake2', self.repository_location) - log = self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=keyfile-blake2') + log = self.cmd(f'--repo={self.repository_location}', 'info') assert '(key file BLAKE2b)' in log - self.cmd('key', 'change-location', self.repository_location, 'repokey') - log = self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'repokey') + log = self.cmd(f'--repo={self.repository_location}', 'info') assert '(repokey BLAKE2b)' in log def test_break_lock(self): - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('break-lock', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'break-lock') def test_usage(self): self.cmd() @@ -2506,11 +2508,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): noatime_used = flags_noatime != flags_normal return noatime_used and atime_before == atime_after - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_test_files() have_noatime = has_noatime('input/file1') - self.cmd('create', '--exclude-nodump', '--atime', self.repository_location + '::archive', 'input') - self.cmd('create', '--exclude-nodump', '--atime', self.repository_location + '::archive2', 'input') + self.cmd(f'--repo={self.repository_location}::archive', 'create', '--exclude-nodump', '--atime', 'input') + self.cmd(f'--repo={self.repository_location}::archive2', 'create', '--exclude-nodump', '--atime', 'input') if has_lchflags: # remove the file we did not backup, so input and output become equal os.remove(os.path.join('input', 'flagfile')) @@ -2596,15 +2598,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(llfuse, 'llfuse not installed') def test_fuse_versions_view(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('test', contents=b'first') if are_hardlinks_supported(): self.create_regular_file('hardlink1', contents=b'123456') os.link('input/hardlink1', 'input/hardlink2') os.link('input/hardlink1', 'input/hardlink3') - self.cmd('create', self.repository_location + '::archive1', 'input') + self.cmd(f'--repo={self.repository_location}::archive1', 'create', 'input') self.create_regular_file('test', contents=b'second') - self.cmd('create', self.repository_location + '::archive2', 'input') + self.cmd(f'--repo={self.repository_location}::archive2', 'create', '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, '-o', 'versions'): @@ -2628,7 +2630,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(llfuse, 'llfuse not installed') def test_fuse_allow_damaged_files(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('archive') # Get rid of a chunk and repair it archive, repository = self.open_archive('archive') @@ -2641,7 +2643,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): else: assert False # missed the file repository.commit(compact=False) - self.cmd('check', '--repair', self.repository_location, exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', '--repair', exit_code=0) mountpoint = os.path.join(self.tmpdir, 'mountpoint') with self.fuse_mount(self.repository_location + '::archive', mountpoint): @@ -2653,7 +2655,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(llfuse, 'llfuse not installed') def test_fuse_mount_options(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('arch11') self.create_src_archive('arch12') self.create_src_archive('arch21') @@ -2727,7 +2729,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # Decorate borg.locking.Lock.migrate_lock = write_assert_data(borg.locking.Lock.migrate_lock) try: - self.cmd('init', '--encryption=none', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self.create_src_archive('arch') mountpoint = os.path.join(self.tmpdir, 'mountpoint') # In order that the decoration is kept for the borg mount process, we must not spawn, but actually fork; @@ -2780,13 +2782,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_test_files() os.environ['BORG_PASSPHRASE'] = 'passphrase' - self.cmd('init', '--encryption=' + method, self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=' + method) verify_uniqueness() - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') verify_uniqueness() - self.cmd('create', self.repository_location + '::test.2', 'input') + self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') verify_uniqueness() - self.cmd('delete', self.repository_location + '::test.2') + self.cmd(f'--repo={self.repository_location}::test.2', 'delete') verify_uniqueness() def test_aes_counter_uniqueness_keyfile(self): @@ -2797,41 +2799,41 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_debug_dump_archive_items(self): self.create_test_files() - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with changedir('output'): - output = self.cmd('debug', 'dump-archive-items', self.repository_location + '::test') + output = self.cmd(f'--repo={self.repository_location}::test', 'debug', 'dump-archive-items') output_dir = sorted(os.listdir('output')) 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', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with changedir('output'): - output = self.cmd('debug', 'dump-repo-objs', self.repository_location) + output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-repo-objs') output_dir = sorted(os.listdir('output')) assert len(output_dir) > 0 and output_dir[0].startswith('00000000_') assert 'Done.' in output def test_debug_put_get_delete_obj(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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(f'--repo={self.repository_location}', 'debug', 'put-obj', 'input/file') assert hexkey in output - output = self.cmd('debug', 'get-obj', self.repository_location, hexkey, 'output/file') + output = self.cmd(f'--repo={self.repository_location}', 'debug', 'get-obj', 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(f'--repo={self.repository_location}', 'debug', 'delete-obj', hexkey) assert "deleted" in output - output = self.cmd('debug', 'delete-obj', self.repository_location, hexkey) + output = self.cmd(f'--repo={self.repository_location}', 'debug', 'delete-obj', hexkey) assert "not found" in output - output = self.cmd('debug', 'delete-obj', self.repository_location, 'invalid') + output = self.cmd(f'--repo={self.repository_location}', 'debug', 'delete-obj', 'invalid') assert "is invalid" in output def test_init_interrupt(self): @@ -2839,19 +2841,19 @@ class ArchiverTestCase(ArchiverTestCaseBase): raise EOFError with patch.object(FlexiKey, 'create', raise_eof): - self.cmd('init', '--encryption=repokey', self.repository_location, exit_code=1) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey', 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) + self.cmd(f'--repo={self.repository_location}', 'init', exit_code=2) def test_init_nested_repositories(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') if self.FORK_DEFAULT: - self.cmd('init', '--encryption=repokey', self.repository_location + '/nested', exit_code=2) + self.cmd(f'--repo={self.repository_location}/nested', 'init', '--encryption=repokey', exit_code=2) else: with pytest.raises(Repository.AlreadyExists): - self.cmd('init', '--encryption=repokey', self.repository_location + '/nested') + self.cmd(f'--repo={self.repository_location}/nested', 'init', '--encryption=repokey') def test_init_refuse_to_overwrite_keyfile(self): """BORG_KEY_FILE=something borg init should quit if "something" already exists. @@ -2859,10 +2861,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): See https://github.com/borgbackup/borg/pull/6046""" keyfile = os.path.join(self.tmpdir, 'keyfile') with environment_variable(BORG_KEY_FILE=keyfile): - self.cmd('init', '--encryption=keyfile', self.repository_location + '0') + self.cmd(f'--repo={self.repository_location}0', 'init', '--encryption=keyfile') with open(keyfile) as file: before = file.read() - arg = ('init', '--encryption=keyfile', self.repository_location + '1') + arg = (f'--repo={self.repository_location}1', 'init', '--encryption=keyfile') if self.FORK_DEFAULT: self.cmd(*arg, exit_code=2) else: @@ -2874,7 +2876,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def check_cache(self): # First run a regular borg check - self.cmd('check', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'check') # 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.NO_OPERATION_CHECK) @@ -2894,8 +2896,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert id in seen def test_check_cache(self): - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with self.open_repository() as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) with Cache(repository, key, manifest, sync=False) as cache: @@ -2906,26 +2908,25 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.check_cache() def test_recreate_target_rc(self): - self.cmd('init', '--encryption=repokey', self.repository_location) - output = self.cmd('recreate', self.repository_location, '--target=asdf', exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}', 'recreate', '--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', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.check_cache() - archive = self.repository_location + '::test0' - self.cmd('create', archive, 'input') + self.cmd(f'--repo={self.repository_location}::test0', 'create', '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') + original_archive = self.cmd(f'--repo={self.repository_location}', 'list') + self.cmd(f'--repo={self.repository_location}::test0', 'recreate', 'input/dir2', + '-e', 'input/dir2/file3', '--target=new-archive') self.check_cache() - archives = self.cmd('list', self.repository_location) + archives = self.cmd(f'--repo={self.repository_location}', 'list') assert original_archive in archives assert 'new-archive' in archives - archive = self.repository_location + '::new-archive' - listing = self.cmd('list', '--short', archive) + listing = self.cmd(f'--repo={self.repository_location}::new-archive', 'list', '--short') assert 'file1' not in listing assert 'dir2/file2' in listing assert 'dir2/file3' not in listing @@ -2933,12 +2934,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_basic(self): self.create_test_files() self.create_regular_file('dir2/file3', size=1024 * 80) - 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') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test0', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test0', 'recreate', 'input/dir2', '-e', 'input/dir2/file3') self.check_cache() - listing = self.cmd('list', '--short', archive) + listing = self.cmd(f'--repo={self.repository_location}::test0', 'list', '--short') assert 'file1' not in listing assert 'dir2/file2' in listing assert 'dir2/file3' not in listing @@ -2947,48 +2947,48 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_subtree_hardlinks(self): # This is essentially the same problem set as in test_extract_hardlinks self._extract_hardlinks_setup() - self.cmd('create', self.repository_location + '::test2', 'input') - self.cmd('recreate', self.repository_location + '::test', 'input/dir1') + self.cmd(f'--repo={self.repository_location}::test2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'recreate', 'input/dir1') self.check_cache() with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') 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 with changedir('output'): - self.cmd('extract', self.repository_location + '::test2') + self.cmd(f'--repo={self.repository_location}::test2', 'extract') assert os.stat('input/dir1/hardlink').st_nlink == 4 def test_recreate_rechunkify(self): 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', '--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', '--files-cache=disabled') - list = self.cmd('list', self.repository_location + '::test1', 'input/large_file', + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test1', 'create', '--chunker-params', '7,9,8,128', 'input') + self.cmd(f'--repo={self.repository_location}::test2', 'create', 'input', '--files-cache=disabled') + list = self.cmd(f'--repo={self.repository_location}::test1', 'list', 'input/large_file', '--format', '{num_chunks} {unique_chunks}') num_chunks, unique_chunks = map(int, list.split(' ')) # test1 and test2 do not deduplicate assert num_chunks == unique_chunks - self.cmd('recreate', self.repository_location, '--chunker-params', 'default') + self.cmd(f'--repo={self.repository_location}', 'recreate', '--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', + assert int(self.cmd(f'--repo={self.repository_location}::test1', 'list', 'input/large_file', '--format={size}')) + assert not int(self.cmd(f'--repo={self.repository_location}::test1', 'list', 'input/large_file', '--format', '{unique_chunks}')) def test_recreate_recompress(self): self.create_regular_file('compressible', size=10000) - 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', + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input', '-C', 'none') + file_list = self.cmd(f'--repo={self.repository_location}::test', 'list', 'input/compressible', '--format', '{size} {sha256}') size, sha256_before = file_list.split(' ') - self.cmd('recreate', self.repository_location, '-C', 'lz4', '--recompress') + self.cmd(f'--repo={self.repository_location}', 'recreate', '-C', 'lz4', '--recompress') self.check_cache() - file_list = self.cmd('list', self.repository_location + '::test', 'input/compressible', + file_list = self.cmd(f'--repo={self.repository_location}::test', 'list', 'input/compressible', '--format', '{size} {sha256}') size, sha256_after = file_list.split(' ') assert sha256_before == sha256_after @@ -2996,12 +2996,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_timestamp(self): local_timezone = datetime.now(timezone(timedelta(0))).astimezone().tzinfo self.create_test_files() - self.cmd('init', '--encryption=repokey', self.repository_location) - archive = self.repository_location + '::test0' - self.cmd('create', archive, 'input') - self.cmd('recreate', '--timestamp', "1970-01-02T00:00:00", '--comment', - 'test', archive) - info = self.cmd('info', archive).splitlines() + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test0', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test0', 'recreate', '--timestamp', "1970-01-02T00:00:00", + '--comment', 'test') + info = self.cmd(f'--repo={self.repository_location}::test0', 'info').splitlines() dtime = datetime(1970, 1, 2) + local_timezone.utcoffset(None) s_time = dtime.strftime("%Y-%m-%d") assert any([re.search(r'Time \(start\).+ %s' % s_time, item) for item in info]) @@ -3009,70 +3008,70 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_dry_run(self): self.create_regular_file('compressible', size=10000) - 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') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + archives_before = self.cmd(f'--repo={self.repository_location}::test', 'list') + self.cmd(f'--repo={self.repository_location}', 'recreate', '-n', '-e', 'input/compressible') self.check_cache() - archives_after = self.cmd('list', self.repository_location + '::test') + archives_after = self.cmd(f'--repo={self.repository_location}::test', 'list') assert archives_after == archives_before def test_recreate_skips_nothing_to_do(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_before = self.cmd('info', self.repository_location + '::test') - self.cmd('recreate', self.repository_location, '--chunker-params', 'default') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + info_before = self.cmd(f'--repo={self.repository_location}::test', 'info') + self.cmd(f'--repo={self.repository_location}', 'recreate', '--chunker-params', 'default') self.check_cache() - info_after = self.cmd('info', self.repository_location + '::test') + info_after = self.cmd(f'--repo={self.repository_location}::test', 'info') assert info_before == info_after # includes archive ID def test_with_lock(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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) + self.cmd(f'--repo={self.repository_location}', 'with-lock', *cmd, fork=True, exit_code=42) def test_recreate_list_output(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file1', size=0) self.create_regular_file('file2', size=0) self.create_regular_file('file3', size=0) self.create_regular_file('file4', size=0) self.create_regular_file('file5', size=0) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - output = self.cmd('recreate', '--list', '--info', self.repository_location + '::test', '-e', 'input/file2') + output = self.cmd(f'--repo={self.repository_location}::test', 'recreate', '--list', '--info', '-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') + output = self.cmd(f'--repo={self.repository_location}::test', 'recreate', '--list', '-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') + output = self.cmd(f'--repo={self.repository_location}::test', 'recreate', '-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') + output = self.cmd(f'--repo={self.repository_location}::test', 'recreate', '--info', '-e', 'input/file5') self.check_cache() self.assert_not_in("input/file1", output) self.assert_not_in("x input/file5", output) def test_bad_filters(self): - 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) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'delete', '--first', '1', '--last', '1', 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') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'keyfile') repo_id = self._extract_repository_id(self.repository_path) - self.cmd('key', 'export', self.repository_location, export_file) + self.cmd(f'--repo={self.repository_location}', 'key', 'export', export_file) with open(export_file) as fd: export_contents = fd.read() @@ -3088,7 +3087,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.unlink(key_file) - self.cmd('key', 'import', self.repository_location, export_file) + self.cmd(f'--repo={self.repository_location}', 'key', 'import', export_file) with open(key_file) as fd: key_contents2 = fd.read() @@ -3096,10 +3095,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert key_contents2 == key_contents def test_key_import_keyfile_with_borg_key_file(self): - self.cmd('init', self.repository_location, '--encryption', 'keyfile') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'keyfile') exported_key_file = os.path.join(self.output_path, 'exported') - self.cmd('key', 'export', self.repository_location, exported_key_file) + self.cmd(f'--repo={self.repository_location}', 'key', 'export', exported_key_file) key_file = os.path.join(self.keys_path, os.listdir(self.keys_path)[0]) with open(key_file) as fd: @@ -3108,7 +3107,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): imported_key_file = os.path.join(self.output_path, 'imported') with environment_variable(BORG_KEY_FILE=imported_key_file): - self.cmd('key', 'import', self.repository_location, exported_key_file) + self.cmd(f'--repo={self.repository_location}', 'key', 'import', exported_key_file) assert not os.path.isfile(key_file), '"borg key import" should respect BORG_KEY_FILE' with open(imported_key_file) as fd: @@ -3117,9 +3116,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_key_export_repokey(self): export_file = self.output_path + '/exported' - self.cmd('init', self.repository_location, '--encryption', 'repokey') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'repokey') repo_id = self._extract_repository_id(self.repository_path) - self.cmd('key', 'export', self.repository_location, export_file) + self.cmd(f'--repo={self.repository_location}', 'key', 'export', export_file) with open(export_file) as fd: export_contents = fd.read() @@ -3138,7 +3137,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(f'--repo={self.repository_location}', 'key', 'import', export_file) with Repository(self.repository_path) as repository: repo_key2 = RepoKey(repository) @@ -3148,9 +3147,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_key_export_qr(self): export_file = self.output_path + '/exported.html' - self.cmd('init', self.repository_location, '--encryption', 'repokey') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'repokey') repo_id = self._extract_repository_id(self.repository_path) - self.cmd('key', 'export', '--qr-html', self.repository_location, export_file) + self.cmd(f'--repo={self.repository_location}', 'key', 'export', '--qr-html', export_file) with open(export_file, encoding='utf-8') as fd: export_contents = fd.read() @@ -3163,39 +3162,39 @@ class ArchiverTestCase(ArchiverTestCaseBase): export_directory = self.output_path + '/exported' os.mkdir(export_directory) - self.cmd('init', self.repository_location, '--encryption', 'repokey') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'repokey') - self.cmd('key', 'export', self.repository_location, export_directory, exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'key', 'export', export_directory, exit_code=EXIT_ERROR) def test_key_import_errors(self): export_file = self.output_path + '/exported' - self.cmd('init', self.repository_location, '--encryption', 'keyfile') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'keyfile') - self.cmd('key', 'import', self.repository_location, export_file, exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'key', 'import', export_file, exit_code=EXIT_ERROR) with open(export_file, 'w') as fd: fd.write('something not a key\n') if self.FORK_DEFAULT: - self.cmd('key', 'import', self.repository_location, export_file, exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'key', 'import', export_file, exit_code=2) else: with pytest.raises(NotABorgKeyFile): - self.cmd('key', 'import', self.repository_location, export_file) + self.cmd(f'--repo={self.repository_location}', 'key', 'import', export_file) with open(export_file, 'w') as fd: fd.write('BORG_KEY a0a0a0\n') if self.FORK_DEFAULT: - self.cmd('key', 'import', self.repository_location, export_file, exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'key', 'import', export_file, exit_code=2) else: with pytest.raises(RepoIdMismatch): - self.cmd('key', 'import', self.repository_location, export_file) + self.cmd(f'--repo={self.repository_location}', 'key', 'import', 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.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'keyfile') self._set_repository_id(self.repository_path, unhexlify(repo_id)) key_file = self.keys_path + '/' + os.listdir(self.keys_path)[0] @@ -3204,7 +3203,7 @@ 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(f'--repo={self.repository_location}', 'key', 'export', '--paper', export_file) with open(export_file) as fd: export_contents = fd.read() @@ -3219,7 +3218,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_key_import_paperkey(self): repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239' - self.cmd('init', self.repository_location, '--encryption', 'keyfile') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'keyfile') self._set_repository_id(self.repository_path, unhexlify(repo_id)) key_file = self.keys_path + '/' + os.listdir(self.keys_path)[0] @@ -3254,20 +3253,20 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 # print(i.to_bytes(2, 'big')) # break - self.cmd('key', 'import', '--paper', self.repository_location, input=typed_input) + self.cmd(f'--repo={self.repository_location}', 'key', 'import', '--paper', input=typed_input) # Test abort paths typed_input = b'\ny\n' - self.cmd('key', 'import', '--paper', self.repository_location, input=typed_input) + self.cmd(f'--repo={self.repository_location}', 'key', 'import', '--paper', 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) + self.cmd(f'--repo={self.repository_location}', 'key', 'import', '--paper', 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) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') dump_file = self.output_path + '/dump' - output = self.cmd('debug', 'dump-manifest', self.repository_location, dump_file) + output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-manifest', dump_file) assert output == "" with open(dump_file) as f: result = json.load(f) @@ -3279,10 +3278,10 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 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') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') dump_file = self.output_path + '/dump' - output = self.cmd('debug', 'dump-archive', self.repository_location + "::test", dump_file) + output = self.cmd(f'--repo={self.repository_location}::test', 'debug', 'dump-archive', dump_file) assert output == "" with open(dump_file) as f: result = json.load(f) @@ -3292,17 +3291,17 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 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() + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}', 'debug', 'refcount-obj', '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')) + create_json = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'create', '--json', 'input')) archive_id = create_json['archive']['id'] - output = self.cmd('debug', 'refcount-obj', self.repository_location, archive_id).strip() + output = self.cmd(f'--repo={self.repository_location}', 'debug', 'refcount-obj', 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() + output = self.cmd(f'--repo={self.repository_location}', 'debug', 'refcount-obj', '124', 'xyza').strip() assert output == 'object id 124 is invalid.\nobject id xyza is invalid.' def test_debug_info(self): @@ -3311,15 +3310,15 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 assert 'Python' in output def test_benchmark_crud(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') with environment_variable(_BORG_BENCHMARK_CRUD_TEST='YES'): - self.cmd('benchmark', 'crud', self.repository_location, self.input_path) + self.cmd(f'--repo={self.repository_location}', 'benchmark', 'crud', self.input_path) def test_config(self): self.create_test_files() os.unlink('input/flagfile') - self.cmd('init', '--encryption=repokey', self.repository_location) - output = self.cmd('config', '--list', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + output = self.cmd(f'--repo={self.repository_location}', 'config', '--list') self.assert_in('[repository]', output) self.assert_in('version', output) self.assert_in('segments_per_dir', output) @@ -3329,30 +3328,30 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.assert_in('id', output) self.assert_not_in('last_segment_checked', output) - output = self.cmd('config', self.repository_location, 'last_segment_checked', exit_code=1) + output = self.cmd(f'--repo={self.repository_location}', 'config', 'last_segment_checked', exit_code=1) self.assert_in('No option ', output) - self.cmd('config', self.repository_location, 'last_segment_checked', '123') - output = self.cmd('config', self.repository_location, 'last_segment_checked') + self.cmd(f'--repo={self.repository_location}', 'config', 'last_segment_checked', '123') + output = self.cmd(f'--repo={self.repository_location}', 'config', 'last_segment_checked') assert output == '123' + '\n' - output = self.cmd('config', '--list', self.repository_location) + output = self.cmd(f'--repo={self.repository_location}', 'config', '--list') self.assert_in('last_segment_checked', output) - self.cmd('config', '--delete', self.repository_location, 'last_segment_checked') + self.cmd(f'--repo={self.repository_location}', 'config', '--delete', 'last_segment_checked') for cfg_key, cfg_value in [ ('additional_free_space', '2G'), ('repository.append_only', '1'), ]: - output = self.cmd('config', self.repository_location, cfg_key) + output = self.cmd(f'--repo={self.repository_location}', 'config', cfg_key) assert output == '0' + '\n' - self.cmd('config', self.repository_location, cfg_key, cfg_value) - output = self.cmd('config', self.repository_location, cfg_key) + self.cmd(f'--repo={self.repository_location}', 'config', cfg_key, cfg_value) + output = self.cmd(f'--repo={self.repository_location}', 'config', cfg_key) assert output == cfg_value + '\n' - self.cmd('config', '--delete', self.repository_location, cfg_key) - self.cmd('config', self.repository_location, cfg_key, exit_code=1) + self.cmd(f'--repo={self.repository_location}', 'config', '--delete', cfg_key) + self.cmd(f'--repo={self.repository_location}', 'config', cfg_key, exit_code=1) - self.cmd('config', '--list', '--delete', self.repository_location, exit_code=2) - self.cmd('config', self.repository_location, exit_code=2) - self.cmd('config', self.repository_location, 'invalid-option', exit_code=1) + self.cmd(f'--repo={self.repository_location}', 'config', '--list', '--delete', exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'config', exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'config', 'invalid-option', exit_code=1) 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.') @@ -3361,9 +3360,9 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 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', '--progress', '--tar-format=GNU') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'simple.tar', '--progress', '--tar-format=GNU') with changedir('output'): # This probably assumes GNU tar. Note -p switch to extract permissions regardless of umask. subprocess.check_call(['tar', 'xpf', '../simple.tar', '--warning=no-timestamp']) @@ -3376,9 +3375,9 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 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.gz', + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + list = self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'simple.tar.gz', '--list', '--tar-format=GNU') assert 'input/file1\n' in list assert 'input/dir2\n' in list @@ -3392,9 +3391,9 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 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', + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + list = self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'simple.tar', '--strip-components=1', '--list', '--tar-format=GNU') # --list's path are those before processing with --strip-components assert 'input/file1\n' in list @@ -3407,7 +3406,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 @requires_gnutar def test_export_tar_strip_components_links(self): self._extract_hardlinks_setup() - self.cmd('export-tar', self.repository_location + '::test', 'output.tar', + self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'output.tar', '--strip-components=2', '--tar-format=GNU') with changedir('output'): subprocess.check_call(['tar', 'xpf', '../output.tar', '--warning=no-timestamp']) @@ -3420,7 +3419,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 @requires_gnutar def test_extract_hardlinks_tar(self): self._extract_hardlinks_setup() - self.cmd('export-tar', self.repository_location + '::test', 'output.tar', 'input/dir1', '--tar-format=GNU') + self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'output.tar', 'input/dir1', '--tar-format=GNU') with changedir('output'): subprocess.check_call(['tar', 'xpf', '../output.tar', '--warning=no-timestamp']) assert os.stat('input/dir1/hardlink').st_nlink == 2 @@ -3431,12 +3430,12 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_import_tar(self, tar_format='PAX'): self.create_test_files(create_hardlinks=False) # hardlinks become separate files os.unlink('input/flagfile') - self.cmd('init', '--encryption=none', self.repository_location) - self.cmd('create', self.repository_location + '::src', 'input') - self.cmd('export-tar', self.repository_location + '::src', 'simple.tar', f'--tar-format={tar_format}') - self.cmd('import-tar', self.repository_location + '::dst', 'simple.tar') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}::src', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::src', 'export-tar', 'simple.tar', f'--tar-format={tar_format}') + self.cmd(f'--repo={self.repository_location}::dst', 'import-tar', 'simple.tar') with changedir(self.output_path): - self.cmd('extract', self.repository_location + '::dst') + self.cmd(f'--repo={self.repository_location}::dst', 'extract') self.assert_dirs_equal('input', 'output/input', ignore_ns=True, ignore_xattrs=True) @requires_gzip @@ -3445,22 +3444,22 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 pytest.skip('gzip is not installed') self.create_test_files(create_hardlinks=False) # hardlinks become separate files os.unlink('input/flagfile') - self.cmd('init', '--encryption=none', self.repository_location) - self.cmd('create', self.repository_location + '::src', 'input') - self.cmd('export-tar', self.repository_location + '::src', 'simple.tgz', f'--tar-format={tar_format}') - self.cmd('import-tar', self.repository_location + '::dst', 'simple.tgz') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}::src', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::src', 'export-tar', 'simple.tgz', f'--tar-format={tar_format}') + self.cmd(f'--repo={self.repository_location}::dst', 'import-tar', 'simple.tgz') with changedir(self.output_path): - self.cmd('extract', self.repository_location + '::dst') + self.cmd(f'--repo={self.repository_location}::dst', 'extract') self.assert_dirs_equal('input', 'output/input', ignore_ns=True, ignore_xattrs=True) def test_roundtrip_pax_borg(self): self.create_test_files() - self.cmd('init', '--encryption=none', self.repository_location) - self.cmd('create', self.repository_location + '::src', 'input') - self.cmd('export-tar', self.repository_location + '::src', 'simple.tar', '--tar-format=BORG') - self.cmd('import-tar', self.repository_location + '::dst', 'simple.tar') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}::src', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::src', 'export-tar', 'simple.tar', '--tar-format=BORG') + self.cmd(f'--repo={self.repository_location}::dst', 'import-tar', 'simple.tar') with changedir(self.output_path): - self.cmd('extract', self.repository_location + '::dst') + self.cmd(f'--repo={self.repository_location}::dst', 'extract') self.assert_dirs_equal('input', 'output/input') # derived from test_extract_xattrs_errors() @@ -3489,16 +3488,16 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 os.makedirs(os.path.join(self.input_path, 'dir%p')) xattr.setxattr(b'input/dir%p', b'user.attribute', b'value') - self.cmd('init', self.repository_location, '-e' 'none') - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '-e' 'none') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') with changedir('output'): with patch.object(xattr, 'setxattr', patched_setxattr_EACCES): - self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING) + self.cmd(f'--repo={self.repository_location}::test', 'extract', exit_code=EXIT_WARNING) def test_do_not_mention_archive_if_you_can_not_find_repo(self): """https://github.com/borgbackup/borg/issues/6014""" - archive = self.repository_location + '-this-repository-does-not-exist' + '::test' - output = self.cmd('info', archive, exit_code=2, fork=True) + output = self.cmd(f'--repo={self.repository_location}-this-repository-does-not-exist::test', 'info', + exit_code=2, fork=True) self.assert_in('this-repository-does-not-exist', output) self.assert_not_in('this-repository-does-not-exist::test', output) @@ -3508,8 +3507,8 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 It should be possible to retrieve the data from an archive even if both the client and the server forget the nonce""" self.create_regular_file('file1', contents=b'Hello, borg') - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') # Oops! We have removed the repo-side memory of the nonce! # See https://github.com/borgbackup/borg/issues/5858 os.remove(os.path.join(self.repository_path, 'nonce')) @@ -3517,18 +3516,18 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 os.remove(os.path.join(self.get_security_dir(), 'nonce')) # The repo should still be readable - repo_info = self.cmd('info', self.repository_location) + repo_info = self.cmd(f'--repo={self.repository_location}', 'info') assert 'All archives:' in repo_info - repo_list = self.cmd('list', self.repository_location) + repo_list = self.cmd(f'--repo={self.repository_location}', 'list') assert 'test' in repo_list # The archive should still be readable - archive_info = self.cmd('info', self.repository_location + '::test') + archive_info = self.cmd(f'--repo={self.repository_location}::test', 'info') assert 'Archive name: test\n' in archive_info - archive_list = self.cmd('list', self.repository_location + '::test') + archive_list = self.cmd(f'--repo={self.repository_location}::test', 'list') assert 'file1' in archive_list # Extracting the archive should work with changedir('output'): - self.cmd('extract', self.repository_location + '::test') + self.cmd(f'--repo={self.repository_location}::test', 'extract') self.assert_dirs_equal('input', 'output/input') def test_recovery_from_deleted_repo_nonce(self): @@ -3539,35 +3538,35 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 repo. Otherwise we can just use our own copy of the nonce. """ self.create_regular_file('file1', contents=b'Hello, borg') - self.cmd('init', '--encryption=repokey', self.repository_location) - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') # Oops! We have removed the repo-side memory of the nonce! # See https://github.com/borgbackup/borg/issues/5858 nonce = os.path.join(self.repository_path, 'nonce') os.remove(nonce) - self.cmd('create', self.repository_location + '::test2', 'input') + self.cmd(f'--repo={self.repository_location}::test2', 'create', 'input') assert os.path.exists(nonce) def test_init_defaults_to_argon2(self): """https://github.com/borgbackup/borg/issues/747#issuecomment-1076160401""" - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') with Repository(self.repository_path) as repository: key = msgpack.unpackb(a2b_base64(repository.load_key())) assert key['algorithm'] == 'argon2 chacha20-poly1305' def test_init_with_explicit_key_algorithm(self): """https://github.com/borgbackup/borg/issues/747#issuecomment-1076160401""" - self.cmd('init', '--encryption=repokey', '--key-algorithm=pbkdf2', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey', '--key-algorithm=pbkdf2') with Repository(self.repository_path) as repository: key = msgpack.unpackb(a2b_base64(repository.load_key())) assert key['algorithm'] == 'sha256' def verify_change_passphrase_does_not_change_algorithm(self, given_algorithm, expected_algorithm): - self.cmd('init', '--encryption=repokey', '--key-algorithm', given_algorithm, self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey', '--key-algorithm', given_algorithm) os.environ['BORG_NEW_PASSPHRASE'] = 'newpassphrase' - self.cmd('key', 'change-passphrase', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'key', 'change-passphrase') with Repository(self.repository_path) as repository: key = msgpack.unpackb(a2b_base64(repository.load_key())) @@ -3580,9 +3579,9 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.verify_change_passphrase_does_not_change_algorithm('pbkdf2', 'sha256') def verify_change_location_does_not_change_algorithm(self, given_algorithm, expected_algorithm): - self.cmd('init', '--encryption=keyfile', '--key-algorithm', given_algorithm, self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=keyfile', '--key-algorithm', given_algorithm) - self.cmd('key', 'change-location', self.repository_location, 'repokey') + self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'repokey') with Repository(self.repository_path) as repository: key = msgpack.unpackb(a2b_base64(repository.load_key())) @@ -3595,14 +3594,14 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.verify_change_location_does_not_change_algorithm('pbkdf2', 'sha256') def test_key_change_algorithm(self): - self.cmd('init', '--encryption=repokey', '--key-algorithm=pbkdf2', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey', '--key-algorithm=pbkdf2') - self.cmd('key', 'change-algorithm', self.repository_location, 'argon2') + self.cmd(f'--repo={self.repository_location}', 'key', 'change-algorithm', 'argon2') with Repository(self.repository_path) as repository: _, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) assert key._encrypted_key_algorithm == 'argon2 chacha20-poly1305' - self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'info') @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available') @@ -3654,30 +3653,30 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): def setUp(self): super().setUp() with patch.object(ChunkBuffer, 'BUFFER_SIZE', 10): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('archive1') self.create_src_archive('archive2') def test_check_usage(self): - output = self.cmd('check', '-v', '--progress', self.repository_location, exit_code=0) + output = self.cmd(f'--repo={self.repository_location}', 'check', '-v', '--progress', exit_code=0) self.assert_in('Starting repository check', output) self.assert_in('Starting archive consistency check', output) self.assert_in('Checking segments', output) # reset logging to new process default to avoid need for fork=True on next check logging.getLogger('borg.output.progress').setLevel(logging.NOTSET) - output = self.cmd('check', '-v', '--repository-only', self.repository_location, exit_code=0) + output = self.cmd(f'--repo={self.repository_location}', 'check', '-v', '--repository-only', exit_code=0) self.assert_in('Starting repository check', output) self.assert_not_in('Starting archive consistency check', output) self.assert_not_in('Checking segments', output) - output = self.cmd('check', '-v', '--archives-only', self.repository_location, exit_code=0) + output = self.cmd(f'--repo={self.repository_location}', 'check', '-v', '--archives-only', exit_code=0) self.assert_not_in('Starting repository check', output) self.assert_in('Starting archive consistency check', output) - output = self.cmd('check', '-v', '--archives-only', '--prefix=archive2', self.repository_location, exit_code=0) + output = self.cmd(f'--repo={self.repository_location}', 'check', '-v', '--archives-only', '--prefix=archive2', exit_code=0) self.assert_not_in('archive1', output) - output = self.cmd('check', '-v', '--archives-only', '--first=1', self.repository_location, exit_code=0) + output = self.cmd(f'--repo={self.repository_location}', 'check', '-v', '--archives-only', '--first=1', 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) + output = self.cmd(f'--repo={self.repository_location}', 'check', '-v', '--archives-only', '--last=1', exit_code=0) self.assert_not_in('archive1', output) self.assert_in('archive2', output) @@ -3693,11 +3692,11 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): else: self.fail('should not happen') repository.commit(compact=False) - self.cmd('check', self.repository_location, exit_code=1) - output = self.cmd('check', '--repair', self.repository_location, exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=1) + output = self.cmd(f'--repo={self.repository_location}', 'check', '--repair', 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.cmd(f'--repo={self.repository_location}', 'check', exit_code=0) + output = self.cmd(f'--repo={self.repository_location}::archive1', 'list', '--format={health}#{path}{LF}', 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'): @@ -3714,7 +3713,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): with patch.object(ChunkBuffer, 'BUFFER_SIZE', 10): self.create_src_archive('archive3') # check should be able to heal the file now: - output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0) + output = self.cmd(f'--repo={self.repository_location}', 'check', '-v', '--repair', exit_code=0) self.assert_in('Healed previously missing file chunk', output) self.assert_in('testsuite/archiver.py: Completely healed previously damaged file!', output) # check that the file in the old archives has the correct chunks again @@ -3728,7 +3727,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): else: self.fail('should not happen') # list is also all-healthy again - output = self.cmd('list', '--format={health}#{path}{LF}', self.repository_location + '::archive1', exit_code=0) + output = self.cmd(f'--repo={self.repository_location}::archive1', 'list', '--format={health}#{path}{LF}', exit_code=0) self.assert_not_in('broken#', output) def test_missing_archive_item_chunk(self): @@ -3736,29 +3735,29 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): with repository: repository.delete(archive.metadata.items[0]) repository.commit(compact=False) - self.cmd('check', self.repository_location, exit_code=1) - self.cmd('check', '--repair', self.repository_location, exit_code=0) - self.cmd('check', self.repository_location, exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=1) + self.cmd(f'--repo={self.repository_location}', 'check', '--repair', exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=0) def test_missing_archive_metadata(self): archive, repository = self.open_archive('archive1') with repository: repository.delete(archive.id) repository.commit(compact=False) - self.cmd('check', self.repository_location, exit_code=1) - self.cmd('check', '--repair', self.repository_location, exit_code=0) - self.cmd('check', self.repository_location, exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=1) + self.cmd(f'--repo={self.repository_location}', 'check', '--repair', exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=0) def test_missing_manifest(self): archive, repository = self.open_archive('archive1') with repository: repository.delete(Manifest.MANIFEST_ID) repository.commit(compact=False) - self.cmd('check', self.repository_location, exit_code=1) - output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=1) + output = self.cmd(f'--repo={self.repository_location}', 'check', '-v', '--repair', exit_code=0) self.assert_in('archive1', output) self.assert_in('archive2', output) - self.cmd('check', self.repository_location, exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=0) def test_corrupted_manifest(self): archive, repository = self.open_archive('archive1') @@ -3767,11 +3766,11 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): corrupted_manifest = manifest + b'corrupted!' repository.put(Manifest.MANIFEST_ID, corrupted_manifest) repository.commit(compact=False) - self.cmd('check', self.repository_location, exit_code=1) - output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=1) + output = self.cmd(f'--repo={self.repository_location}', 'check', '-v', '--repair', exit_code=0) self.assert_in('archive1', output) self.assert_in('archive2', output) - self.cmd('check', self.repository_location, exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=0) def test_manifest_rebuild_corrupted_chunk(self): archive, repository = self.open_archive('archive1') @@ -3784,10 +3783,10 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): corrupted_chunk = chunk + b'corrupted!' repository.put(archive.id, corrupted_chunk) repository.commit(compact=False) - self.cmd('check', self.repository_location, exit_code=1) - output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=1) + output = self.cmd(f'--repo={self.repository_location}', 'check', '-v', '--repair', exit_code=0) self.assert_in('archive2', output) - self.cmd('check', self.repository_location, exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=0) def test_manifest_rebuild_duplicate_archive(self): archive, repository = self.open_archive('archive1') @@ -3809,27 +3808,27 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): archive_id = key.id_hash(archive) repository.put(archive_id, key.encrypt(archive_id, archive)) repository.commit(compact=False) - 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.cmd(f'--repo={self.repository_location}', 'check', exit_code=1) + self.cmd(f'--repo={self.repository_location}', 'check', '--repair', exit_code=0) + output = self.cmd(f'--repo={self.repository_location}', 'list') 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) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=0) with Repository(self.repository_location, exclusive=True) as repository: repository.put(b'01234567890123456789012345678901', b'xxxx') repository.commit(compact=False) - self.cmd('check', self.repository_location, exit_code=1) - self.cmd('check', self.repository_location, exit_code=1) - self.cmd('check', '--repair', self.repository_location, exit_code=0) - self.cmd('check', self.repository_location, exit_code=0) - self.cmd('extract', '--dry-run', self.repository_location + '::archive1', exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=1) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=1) + self.cmd(f'--repo={self.repository_location}', 'check', '--repair', exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=0) + self.cmd(f'--repo={self.repository_location}::archive1', 'extract', '--dry-run', exit_code=0) def _test_verify_data(self, *init_args): shutil.rmtree(self.repository_path) - self.cmd('init', self.repository_location, *init_args) + self.cmd(f'--repo={self.repository_location}', 'init', *init_args) self.create_src_archive('archive1') archive, repository = self.open_archive('archive1') with repository: @@ -3840,11 +3839,11 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): repository.put(chunk.id, data) break repository.commit(compact=False) - self.cmd('check', self.repository_location, exit_code=0) - output = self.cmd('check', '--verify-data', self.repository_location, exit_code=1) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=0) + output = self.cmd(f'--repo={self.repository_location}', 'check', '--verify-data', 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) + output = self.cmd(f'--repo={self.repository_location}', 'check', '--repair', '--verify-data', exit_code=0) assert bin_to_hex(chunk.id) + ', integrity error' in output assert 'testsuite/archiver.py: New missing file chunk detected' in output @@ -3859,7 +3858,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): for id_ in repository.list(): repository.delete(id_) repository.commit(compact=False) - self.cmd('check', self.repository_location, exit_code=1) + self.cmd(f'--repo={self.repository_location}', 'check', exit_code=1) class ManifestAuthenticationTest(ArchiverTestCaseBase): @@ -3875,7 +3874,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): repository.commit(compact=False) def test_fresh_init_tam_required(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') repository = Repository(self.repository_path, exclusive=True) with repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -3887,10 +3886,10 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): repository.commit(compact=False) with pytest.raises(TAMRequiredError): - self.cmd('list', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'list') def test_not_required(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('archive1234') repository = Repository(self.repository_path, exclusive=True) with repository: @@ -3903,39 +3902,39 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): del manifest['tam'] repository.put(Manifest.MANIFEST_ID, key.encrypt(Manifest.MANIFEST_ID, msgpack.packb(manifest))) repository.commit(compact=False) - output = self.cmd('list', '--debug', self.repository_location) + output = self.cmd(f'--repo={self.repository_location}', 'list', '--debug') assert 'archive1234' in output assert 'TAM not found and not required' in output # Run upgrade - self.cmd('upgrade', '--tam', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'upgrade', '--tam') # Manifest must be authenticated now - output = self.cmd('list', '--debug', self.repository_location) + output = self.cmd(f'--repo={self.repository_location}', 'list', '--debug') 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) + self.cmd(f'--repo={self.repository_location}', 'list') # Force upgrade - self.cmd('upgrade', '--tam', '--force', self.repository_location) - self.cmd('list', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'upgrade', '--tam', '--force') + self.cmd(f'--repo={self.repository_location}', 'list') def test_disable(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('archive1234') - self.cmd('upgrade', '--disable-tam', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'upgrade', '--disable-tam') repository = Repository(self.repository_path, exclusive=True) self.spoof_manifest(repository) - assert not self.cmd('list', self.repository_location) + assert not self.cmd(f'--repo={self.repository_location}', 'list') def test_disable2(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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) + self.cmd(f'--repo={self.repository_location}', 'upgrade', '--disable-tam') + assert not self.cmd(f'--repo={self.repository_location}', 'list') class RemoteArchiverTestCase(ArchiverTestCase): @@ -3947,32 +3946,32 @@ 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', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') # 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]): with pytest.raises(PathNotAllowed): - self.cmd('init', '--encryption=repokey', self.repository_location + '_0') + self.cmd(f'--repo={self.repository_location}_0', 'init', '--encryption=repokey') # restricted to a completely different path: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo']): with pytest.raises(PathNotAllowed): - self.cmd('init', '--encryption=repokey', self.repository_location + '_1') + self.cmd(f'--repo={self.repository_location}_1', 'init', '--encryption=repokey') 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', '--encryption=repokey', self.repository_location + '_2') + self.cmd(f'--repo={self.repository_location}_2', 'init', '--encryption=repokey') # 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', '--encryption=repokey', self.repository_location + '_3') + self.cmd(f'--repo={self.repository_location}_3', 'init', '--encryption=repokey') 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) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @unittest.skip('only works locally') def test_debug_put_get_delete_obj(self): @@ -3987,25 +3986,25 @@ class RemoteArchiverTestCase(ArchiverTestCase): pass def test_remote_repo_strip_components_doesnt_leak(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') 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') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') marker = 'cached responses left in RemoteRepository' with changedir('output'): - res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '3') + res = self.cmd(f'--repo={self.repository_location}::test', 'extract', "--debug", '--strip-components', '3') assert marker not in res with self.assert_creates_file('file'): - res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '2') + res = self.cmd(f'--repo={self.repository_location}::test', 'extract', "--debug", '--strip-components', '2') assert marker not in res with self.assert_creates_file('dir/file'): - res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '1') + res = self.cmd(f'--repo={self.repository_location}::test', 'extract', "--debug", '--strip-components', '1') assert marker not in res with self.assert_creates_file('input/dir/file'): - res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '0') + res = self.cmd(f'--repo={self.repository_location}::test', 'extract', "--debug", '--strip-components', '0') assert marker not in res @@ -4013,8 +4012,8 @@ 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'] + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cache_path = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--json'))['cache']['path'] def corrupt(self, file, amount=1): with open(file, 'r+b') as fd: @@ -4027,28 +4026,28 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): self.corrupt(os.path.join(self.cache_path, 'chunks')) if self.FORK_DEFAULT: - out = self.cmd('info', self.repository_location, exit_code=2) + out = self.cmd(f'--repo={self.repository_location}', 'info', exit_code=2) assert 'failed integrity check' in out else: with pytest.raises(FileIntegrityError): - self.cmd('info', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'info') def test_cache_files(self): - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') self.corrupt(os.path.join(self.cache_path, 'files')) - out = self.cmd('create', self.repository_location + '::test1', 'input') + out = self.cmd(f'--repo={self.repository_location}::test1', 'create', 'input') # borg warns about the corrupt files cache, but then continues without files cache. assert 'files cache is corrupted' in out def test_chunks_archive(self): - self.cmd('create', self.repository_location + '::test1', 'input') + self.cmd(f'--repo={self.repository_location}::test1', 'create', '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') + target_id = self.cmd(f'--repo={self.repository_location}', 'list', '--format={id}{LF}').strip() + self.cmd(f'--repo={self.repository_location}::test2', 'create', 'input') # 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') + self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') + self.cmd(f'--repo={self.repository_location}', 'info', '--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 @@ -4064,7 +4063,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): config.write(fd) # Cache sync notices corrupted archive chunks, but automatically recovers. - out = self.cmd('create', '-v', self.repository_location + '::test3', 'input', exit_code=1) + out = self.cmd(f'--repo={self.repository_location}::test3', 'create', '-v', '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 @@ -4079,7 +4078,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): with open(config_path, 'w') as fd: config.write(fd) - out = self.cmd('info', self.repository_location) + out = self.cmd(f'--repo={self.repository_location}', 'info') assert 'Cache integrity data not available: old Borg version modified the cache.' in out @@ -4107,10 +4106,10 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): os.link('input/file_removed', 'input/hardlink_removed') os.link('input/file_removed2', 'input/hardlink_target_removed') - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') # Create the first snapshot - self.cmd('create', self.repository_location + '::test0', 'input') + self.cmd(f'--repo={self.repository_location}::test0', 'create', 'input') # Setup files for the second snapshot self.create_regular_file('file_added', size=2048) @@ -4141,8 +4140,8 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): fd.write(b'appended_data') # Create the second snapshot - self.cmd('create', self.repository_location + '::test1a', 'input') - self.cmd('create', '--chunker-params', '16,18,17,4095', self.repository_location + '::test1b', 'input') + self.cmd(f'--repo={self.repository_location}::test1a', 'create', 'input') + self.cmd(f'--repo={self.repository_location}::test1b', 'create', '--chunker-params', '16,18,17,4095', 'input') def do_asserts(output, can_compare_ids): # File contents changed (deleted and replaced with a new file) @@ -4287,19 +4286,19 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): if are_hardlinks_supported(): assert not any(get_changes('input/hardlink_target_replaced', joutput)) - do_asserts(self.cmd('diff', self.repository_location + '::test0', 'test1a'), True) + do_asserts(self.cmd(f'--repo={self.repository_location}::test0', 'diff', 'TODO_test0', 'test1a'), True) # We expect exit_code=1 due to the chunker params warning - do_asserts(self.cmd('diff', self.repository_location + '::test0', 'test1b', exit_code=1), False) - do_json_asserts(self.cmd('diff', self.repository_location + '::test0', 'test1a', '--json-lines'), True) + do_asserts(self.cmd(f'--repo={self.repository_location}::test0', 'diff', 'TODO_test0', 'test1b', exit_code=1), False) + do_json_asserts(self.cmd(f'--repo={self.repository_location}::test0', 'diff', 'TODO_test0', 'test1a', '--json-lines'), True) def test_sort_option(self): - self.cmd('init', '--encryption=repokey', self.repository_location) + self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('a_file_removed', size=8) self.create_regular_file('f_file_removed', size=16) self.create_regular_file('c_file_changed', size=32) self.create_regular_file('e_file_changed', size=64) - self.cmd('create', self.repository_location + '::test0', 'input') + self.cmd(f'--repo={self.repository_location}::test0', 'create', 'input') os.unlink('input/a_file_removed') os.unlink('input/f_file_removed') @@ -4309,9 +4308,9 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('e_file_changed', size=1024) self.create_regular_file('b_file_added', size=128) self.create_regular_file('d_file_added', size=256) - self.cmd('create', self.repository_location + '::test1', 'input') + self.cmd(f'--repo={self.repository_location}::test1', 'create', 'input') - output = self.cmd('diff', '--sort', self.repository_location + '::test0', 'test1') + output = self.cmd(f'--repo={self.repository_location}::test0', 'diff', '--sort', 'TODO_test0', 'test1') expected = [ 'a_file_removed', 'b_file_added', @@ -4353,7 +4352,7 @@ def test_get_args(): assert args.restrict_to_repositories == ['/r1', '/r2'] # trying to cheat - try to execute different subcommand args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ], - 'borg init --encryption=repokey /') + 'borg --repo=/ init --encryption=repokey') assert args.func == archiver.do_serve # Check that environment variables in the forced command don't cause issues. If the command From 1bf2a6a2409f41bb71ab4473fa79c5ef89d85759 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 15 Jun 2022 17:07:42 +0200 Subject: [PATCH 048/160] remove archive checks from location_validator, use --other-repo --- src/borg/archiver.py | 19 ++++++++----------- src/borg/helpers/parseformat.py | 6 +----- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 0e2d07855..cea035ab0 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -4143,12 +4143,12 @@ class Archiver: # initialize DST_REPO reusing key material from SRC_REPO, so that # chunking and chunk id generation will work in the same way as before. - borg init --other-location=SRC_REPO --encryption=DST_ENC DST_REPO + borg --repo=DST_REPO init --other-repo=SRC_REPO --encryption=DST_ENC # transfer archives from SRC_REPO to DST_REPO - borg transfer --dry-run SRC_REPO DST_REPO # check what it would do - borg transfer SRC_REPO DST_REPO # do it! - borg transfer --dry-run SRC_REPO DST_REPO # check! anything left? + borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run # check what it would do + borg --repo=DST_REPO transfer --other-repo=SRC_REPO # do it! + borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run # check! anything left? The default is to transfer all archives, including checkpoint archives. @@ -4164,12 +4164,9 @@ class Archiver: subparser.set_defaults(func=self.do_transfer) subparser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true', help='do not change repository, just check') - subparser.add_argument('other_location', metavar='SRC_REPOSITORY', - type=location_validator(archive=False, other=True), + subparser.add_argument('--other-repo', metavar='SRC_REPOSITORY', dest='other_location', + type=location_validator(other=True), help='source repository') - # subparser.add_argument('-r', '--repo', dest='location', metavar='DST_REPOSITORY', - # type=location_validator(archive=False, other=False), - # help='destination repository') define_archive_filters_group(subparser) # borg diff @@ -4504,8 +4501,8 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='initialize empty repository') subparser.set_defaults(func=self.do_init) - subparser.add_argument('--other-location', metavar='OTHER_REPOSITORY', dest='other_location', - type=location_validator(archive=False, other=True), + subparser.add_argument('--other-repo', metavar='SRC_REPOSITORY', dest='other_location', + type=location_validator(other=True), help='reuse the key material from the other repository') subparser.add_argument('-e', '--encryption', metavar='MODE', dest='encryption', required=True, choices=key_argument_names(), diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 43e8d05eb..e4b54f0c8 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -507,16 +507,12 @@ class Location: return loc -def location_validator(archive=None, proto=None, other=False): +def location_validator(proto=None, other=False): def validator(text): try: loc = Location(text, other=other) except ValueError as err: raise argparse.ArgumentTypeError(str(err)) from None - if archive is True and not loc.archive: - raise argparse.ArgumentTypeError('"%s": No archive specified' % text) - elif archive is False and loc.archive: - raise argparse.ArgumentTypeError('"%s": No archive can be specified' % text) if proto is not None and loc.proto != proto: if proto == 'file': raise argparse.ArgumentTypeError('"%s": Repository must be local' % text) From 0f0cd2435465a5e11fd320a4e2c21bc40ee9e254 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 15 Jun 2022 17:44:38 +0200 Subject: [PATCH 049/160] if --(other-)repo option is not given, use default from environment remove tests composing a repo+archive location with repo from env and location from cli. --- src/borg/archiver.py | 17 ++++--- src/borg/helpers/parseformat.py | 45 ++++++++----------- src/borg/testsuite/helpers.py | 80 --------------------------------- 3 files changed, 29 insertions(+), 113 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index cea035ab0..1b668f618 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -174,8 +174,10 @@ def with_repository(fake=False, invert_fake=False, create=False, lock=True, def decorator(method): @functools.wraps(method) def wrapper(self, args, **kwargs): + location = getattr(args, 'location') + if not location.valid: # location always must be given + raise Error('missing repository, please use --repo or BORG_REPO env var!') lock = getattr(args, 'lock', _lock) - location = args.location # note: 'location' must be always present in args append_only = getattr(args, 'append_only', False) storage_quota = getattr(args, 'storage_quota', None) make_parent_dirs = getattr(args, 'make_parent_dirs', False) @@ -220,8 +222,8 @@ def with_other_repository(manifest=False, key=False, cache=False, compatibility= def decorator(method): @functools.wraps(method) def wrapper(self, args, **kwargs): - location = getattr(args, 'other_location', None) - if location is None: # nothing to do + location = getattr(args, 'other_location') + if not location.valid: # nothing to do return method(self, args, **kwargs) repository = get_repository(location, create=False, exclusive=True, @@ -3203,8 +3205,9 @@ class Archiver: 'compatible file can be generated by suffixing FILE with ".pyprof".') add_common_option('--rsh', metavar='RSH', dest='rsh', help="Use this command to connect to the 'borg serve' process (default: 'ssh')") - add_common_option('--repo', metavar='REPO', dest='location', type=location_validator(), - help="repository to use") # XXXYYY + add_common_option('--repo', metavar='REPO', dest='location', + type=location_validator(other=False), default=Location(other=False), + help="repository to use") def define_exclude_and_patterns(add_option, *, tag_files=False, strip_components=False): add_option('-e', '--exclude', metavar='PATTERN', dest='patterns', @@ -4165,7 +4168,7 @@ class Archiver: subparser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true', help='do not change repository, just check') subparser.add_argument('--other-repo', metavar='SRC_REPOSITORY', dest='other_location', - type=location_validator(other=True), + type=location_validator(other=True), default=Location(other=True), help='source repository') define_archive_filters_group(subparser) @@ -4502,7 +4505,7 @@ class Archiver: help='initialize empty repository') subparser.set_defaults(func=self.do_init) subparser.add_argument('--other-repo', metavar='SRC_REPOSITORY', dest='other_location', - type=location_validator(other=True), + type=location_validator(other=True), default=Location(other=True), help='reuse the key material from the other repository') subparser.add_argument('-e', '--encryption', metavar='MODE', dest='encryption', required=True, choices=key_argument_names(), diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index e4b54f0c8..7c25437b0 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -365,15 +365,6 @@ class Location: local_re = re.compile( local_path_re + optional_archive_re, re.VERBOSE) # local 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""" # 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) - win_file_re = re.compile(r""" (?:file://)? # optional file protocol (?P @@ -384,27 +375,29 @@ class Location: def __init__(self, text='', overrides={}, other=False): self.repo_env_var = 'BORG_OTHER_REPO' if other else 'BORG_REPO' - if not self.parse(text, overrides): - raise ValueError('Invalid location format: "%s"' % self.processed) + self.valid = False + self.proto = None + self.user = None + self._host = None + self.port = None + self.path = None + self.archive = None + self.parse(text, overrides) def parse(self, text, overrides={}): + if not text: + # we did not get a text to parse, so we try to fetch from the environment + text = os.environ.get(self.repo_env_var) + if text is None: + return + self.raw = text # as given by user, might contain placeholders - self.processed = text = replace_placeholders(text, overrides) # after placeholder replacement - valid = self._parse(text) + self.processed = replace_placeholders(self.raw, overrides) # after placeholder replacement + valid = self._parse(self.processed) if valid: - return True - m = self.env_re.match(text) - if not m: - return False - repo_raw = os.environ.get(self.repo_env_var) - if repo_raw is None: - return False - repo = replace_placeholders(repo_raw, overrides) - valid = self._parse(repo) - self.archive = m.group('archive') - self.raw = repo_raw if not self.archive else repo_raw + self.raw - self.processed = repo if not self.archive else f'{repo}::{self.archive}' - return valid + self.valid = True + else: + raise ValueError('Invalid location format: "%s"' % self.processed) def _parse(self, text): def normpath_special(p): diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index fd0414116..81469a133 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -166,15 +166,6 @@ class TestLocationWithoutEnv: assert repr(Location('path::archive-{utcnow}').with_timestamp(datetime(2002, 9, 19, tzinfo=timezone.utc))) == \ "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive-2002-09-19T00:00:00')" - def test_underspecified(self, monkeypatch): - monkeypatch.delenv('BORG_REPO', raising=False) - with pytest.raises(ValueError): - Location('::archive') - with pytest.raises(ValueError): - Location('::') - with pytest.raises(ValueError): - Location() - def test_no_slashes(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) with pytest.raises(ValueError): @@ -213,77 +204,6 @@ class TestLocationWithoutEnv: assert loc_without_archive.processed == "ssh://user@host:1234/repos/%s" % hostname -class TestLocationWithEnv: - def test_ssh(self, monkeypatch): - 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('::')) == \ - "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_ssh_placeholder(self, monkeypatch): - from borg.platform import hostname - monkeypatch.setenv('BORG_REPO', 'ssh://user@host:1234/{hostname}') - assert repr(Location('::archive')) == \ - f"Location(proto='ssh', user='user', host='host', port=1234, path='/{hostname}', archive='archive')" - assert repr(Location('::')) == \ - f"Location(proto='ssh', user='user', host='host', port=1234, path='/{hostname}', archive=None)" - assert repr(Location()) == \ - f"Location(proto='ssh', user='user', host='host', port=1234, path='/{hostname}', 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('::')) == \ - "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_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('::')) == \ - "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('::')) == \ - "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('::')) == \ - "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') - with pytest.raises(ValueError): - Location('::archive_name_with/slashes/is_invalid') - - class FormatTimedeltaTestCase(BaseTestCase): def test(self): From 281bbbc16bafd6f2e8dff81ce8d1c704408f42ed Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 15 Jun 2022 19:00:19 +0200 Subject: [PATCH 050/160] fix tests and benchmarks --- src/borg/archiver.py | 106 +++-- src/borg/fuse.py | 4 +- src/borg/helpers/manifest.py | 2 +- src/borg/helpers/parseformat.py | 2 + src/borg/testsuite/archiver.py | 773 ++++++++++++++++---------------- src/borg/testsuite/benchmark.py | 42 +- 6 files changed, 477 insertions(+), 452 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 1b668f618..2e112fafe 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -52,7 +52,7 @@ try: from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE from .helpers import Error, NoManifestError, set_ec from .helpers import positive_int_validator, location_validator, archivename_validator, ChunkerParams, Location - from .helpers import PrefixSpec, GlobSpec, CommentSpec, SortBySpec, FilesCacheMode + from .helpers import PrefixSpec, GlobSpec, NameSpec, CommentSpec, SortBySpec, FilesCacheMode from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter from .helpers import format_timedelta, format_file_size, parse_file_size, format_archive from .helpers import remove_surrogates, bin_to_hex, prepare_dump_dict, eval_escapes @@ -257,7 +257,9 @@ def with_other_repository(manifest=False, key=False, cache=False, compatibility= def with_archive(method): @functools.wraps(method) def wrapper(self, args, repository, key, manifest, **kwargs): - archive = Archive(repository, key, manifest, args.location.archive, + archive_name = getattr(args, 'name', None) + assert archive_name is not None + archive = Archive(repository, key, manifest, archive_name, numeric_ids=getattr(args, 'numeric_ids', False), noflags=getattr(args, 'nobsdflags', False) or getattr(args, 'noflags', False), noacls=getattr(args, 'noacls', False), @@ -541,7 +543,7 @@ class Archiver: if args.prefix is not None: args.glob_archives = args.prefix + '*' if not args.repo_only and not ArchiveChecker().check( - repository, repair=args.repair, archive=args.location.archive, + repository, repair=args.repair, archive=args.name, 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 @@ -669,34 +671,34 @@ class Archiver: 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([f'--repo={repo}::borg-benchmark-crud1', 'create', + rc = self.do_create(self.parse_args([f'--repo={repo}', 'create', '--name=borg-benchmark-crud1', compression, '--files-cache=disabled', 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([f'--repo={repo}::borg-benchmark-crud2', 'create', + rc1 = self.do_create(self.parse_args([f'--repo={repo}', 'create', '--name=borg-benchmark-crud2', compression, path])) - rc2 = self.do_delete(self.parse_args([f'--repo={repo}::borg-benchmark-crud2', 'delete'])) + rc2 = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '--name=borg-benchmark-crud2'])) assert rc1 == rc2 == 0 # measure a no-change update (archive1 is still present) t_start = time.monotonic() - rc1 = self.do_create(self.parse_args([f'--repo={repo}::borg-benchmark-crud3', 'create', + rc1 = self.do_create(self.parse_args([f'--repo={repo}', 'create', '--name=borg-benchmark-crud3', compression, path])) t_end = time.monotonic() dt_update = t_end - t_start - rc2 = self.do_delete(self.parse_args([f'--repo={repo}::borg-benchmark-crud3', 'delete'])) + rc2 = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '--name=borg-benchmark-crud3'])) assert rc1 == rc2 == 0 # measure extraction (dry-run: without writing result to disk) t_start = time.monotonic() - rc = self.do_extract(self.parse_args([f'--repo={repo}::borg-benchmark-crud1', 'extract', + rc = self.do_extract(self.parse_args([f'--repo={repo}', 'extract', '--name=borg-benchmark-crud1', '--dry-run'])) 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([f'--repo={repo}::borg-benchmark-crud1', 'delete'])) + rc = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '--name=borg-benchmark-crud1'])) t_end = time.monotonic() dt_delete = t_end - t_start assert rc == 0 @@ -1012,7 +1014,7 @@ class Archiver: with Cache(repository, key, manifest, progress=args.progress, lock_wait=self.lock_wait, permit_adhoc_cache=args.no_cache_sync, cache_mode=args.files_cache_mode, iec=args.iec) as cache: - archive = Archive(repository, key, manifest, args.location.archive, cache=cache, + archive = Archive(repository, key, manifest, args.name, cache=cache, create=True, checkpoint_interval=args.checkpoint_interval, numeric_ids=args.numeric_ids, noatime=not args.atime, noctime=args.noctime, progress=args.progress, @@ -1475,7 +1477,7 @@ class Archiver: print_output = print_json_output if args.json_lines else print_text_output archive1 = archive - archive2 = Archive(repository, key, manifest, args.archive2, + archive2 = Archive(repository, key, manifest, args.name2, consider_part_files=args.consider_part_files) can_compare_chunk_ids = archive1.metadata.get('chunker_params', False) == archive2.metadata.get( @@ -1506,7 +1508,7 @@ class Archiver: @with_archive def do_rename(self, args, repository, manifest, key, cache, archive): """Rename an existing archive""" - archive.rename(args.name) + archive.rename(args.name2) manifest.write() repository.commit(compact=False) cache.commit() @@ -1516,7 +1518,7 @@ class Archiver: def do_delete(self, args, repository): """Delete an existing repository or archives""" archive_filter_specified = any((args.first, args.last, args.prefix is not None, args.glob_archives)) - explicit_archives_specified = args.location.archive or args.archives + explicit_archives_specified = args.name or args.archives self.output_list = args.output_list if archive_filter_specified and explicit_archives_specified: self.print_error('Mixing archive filters and explicitly named archives is not supported.') @@ -1532,10 +1534,10 @@ class Archiver: manifest, key = Manifest.load(repository, (Manifest.Operation.DELETE,)) - if args.location.archive or args.archives: + if args.name or args.archives: archives = list(args.archives) - if args.location.archive: - archives.insert(0, args.location.archive) + if args.name: + archives.insert(0, args.name) archive_names = tuple(archives) else: args.consider_checkpoints = True @@ -1699,7 +1701,7 @@ class Archiver: @with_repository(compatibility=(Manifest.Operation.READ,)) def do_list(self, args, repository, manifest, key): """List archive or repository contents""" - if args.location.archive: + if args.name: if args.json: self.print_error('The --json option is only valid for listing archives, not archive contents.') return self.exit_code @@ -1720,7 +1722,7 @@ class Archiver: format = "{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}" def _list_inner(cache): - archive = Archive(repository, key, manifest, args.location.archive, cache=cache, + archive = Archive(repository, key, manifest, args.name, cache=cache, consider_part_files=args.consider_part_files) formatter = ItemFormatter(archive, format, json_lines=args.json_lines) @@ -1763,7 +1765,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 is not None, args.glob_archives)): + if any((args.name, args.first, args.last, args.prefix is not None, args.glob_archives)): return self._info_archives(args, repository, manifest, key, cache) else: return self._info_repository(args, repository, manifest, key, cache) @@ -1772,8 +1774,8 @@ class Archiver: def format_cmdline(cmdline): return remove_surrogates(' '.join(shlex.quote(x) for x in cmdline)) - if args.location.archive: - archive_names = (args.location.archive,) + if args.name: + archive_names = (args.name,) else: args.consider_checkpoints = True archive_names = tuple(x.name for x in manifest.archives.list_considering(args)) @@ -2003,8 +2005,8 @@ class Archiver: checkpoint_interval=args.checkpoint_interval, dry_run=args.dry_run, timestamp=args.timestamp) - if args.location.archive: - name = args.location.archive + if args.name: + name = args.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 @@ -2048,7 +2050,7 @@ class Archiver: t0 = datetime.utcnow() t0_monotonic = time.monotonic() - archive = Archive(repository, key, manifest, args.location.archive, cache=cache, + archive = Archive(repository, key, manifest, args.name, cache=cache, create=True, checkpoint_interval=args.checkpoint_interval, progress=args.progress, chunker_params=args.chunker_params, start=t0, start_monotonic=t0_monotonic, @@ -2289,7 +2291,7 @@ class Archiver: @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, + archive = Archive(repository, key, manifest, args.name, 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)) @@ -2305,9 +2307,9 @@ class Archiver: """dump decoded archive metadata (not: data)""" try: - archive_meta_orig = manifest.archives.get_raw_dict()[args.location.archive] + archive_meta_orig = manifest.archives.get_raw_dict()[args.name] except KeyError: - raise Archive.DoesNotExist(args.location.archive) + raise Archive.DoesNotExist(args.name) indent = 4 @@ -2317,7 +2319,7 @@ class Archiver: 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(' "_name": ' + json.dumps(args.name) + ",\n") fd.write(' "_manifest_entry":\n') fd.write(do_indent(prepare_dump_dict(archive_meta_orig))) fd.write(',\n') @@ -2808,7 +2810,7 @@ class Archiver: This allows you to share the same patterns between multiple repositories without needing to specify them on the command line.\n\n''') helptext['placeholders'] = textwrap.dedent(''' - Repository (or Archive) URLs, ``--prefix``, ``--glob-archives``, ``--comment`` + Repository URLs, ``--name``, ``--prefix``, ``--glob-archives``, ``--comment`` and ``--remote-path`` values support these placeholders: {hostname} @@ -3271,7 +3273,8 @@ class Archiver: def define_borg_mount(parser): parser.set_defaults(func=self.do_mount) - # archive name + parser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + help='specify the archive name') parser.add_argument('--consider-checkpoints', action='store_true', dest='consider_checkpoints', help='Show checkpoint archives in the repository contents list (default: hidden).') parser.add_argument('mountpoint', metavar='MOUNTPOINT', type=str, @@ -3545,7 +3548,8 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='verify repository') subparser.set_defaults(func=self.do_check) - # archive name + subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + help='specify the archive name') subparser.add_argument('--repository-only', dest='repo_only', action='store_true', help='only perform repository checks') subparser.add_argument('--archives-only', dest='archives_only', action='store_true', @@ -3895,6 +3899,8 @@ class Archiver: 'regular files. Also follows symlinks pointing to these kinds of files.') archive_group = subparser.add_argument_group('Archive options') + archive_group.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, default='{hostname}-{now}', + help='specify the name for the archive') archive_group.add_argument('--comment', dest='comment', metavar='COMMENT', type=CommentSpec, default='', help='add a comment text to the archive') archive_group.add_argument('--timestamp', metavar='TIMESTAMP', dest='timestamp', @@ -3913,7 +3919,6 @@ class Archiver: help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') - # archive name subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to archive') @@ -3956,7 +3961,8 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='dump archive items (metadata) (debug)') subparser.set_defaults(func=self.do_debug_dump_archive_items) - # archive name + subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + help='specify the archive name') debug_dump_archive_epilog = process_epilog(""" This command dumps all metadata of an archive in a decoded form to a file. @@ -3967,7 +3973,8 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='dump decoded archive metadata (debug)') subparser.set_defaults(func=self.do_debug_dump_archive) - # archive name + subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + help='specify the archive name') subparser.add_argument('path', metavar='PATH', type=str, help='file to dump data into') @@ -4133,7 +4140,8 @@ class Archiver: help='keep the local security info when deleting a repository') subparser.add_argument('--save-space', dest='save_space', action='store_true', help='work slower, but using less space') - # archive name + subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + help='specify the archive name') subparser.add_argument('archives', metavar='ARCHIVE', nargs='*', help='archives to delete') define_archive_filters_group(subparser) @@ -4207,10 +4215,10 @@ class Archiver: help='Sort the output lines by file path.') subparser.add_argument('--json-lines', action='store_true', help='Format output as JSON Lines. ') - subparser.add_argument('archive1', metavar='ARCHIVE1', + subparser.add_argument('--name', metavar='ARCHIVE1', type=archivename_validator(), help='ARCHIVE1 name') - subparser.add_argument('archive2', metavar='ARCHIVE2', + subparser.add_argument('--name2', metavar='ARCHIVE2', type=archivename_validator(), help='ARCHIVE2 name') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, @@ -4267,6 +4275,8 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='create tarball from archive') subparser.set_defaults(func=self.do_export_tar) + subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + help='specify the archive name') 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', @@ -4274,7 +4284,6 @@ class Archiver: subparser.add_argument('--tar-format', metavar='FMT', dest='tar_format', default='GNU', choices=('BORG', 'PAX', 'GNU'), help='select tar format: BORG, PAX or GNU') - # archive name subparser.add_argument('tarfile', metavar='FILE', help='output tar file. "-" to write to stdout instead.') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, @@ -4312,6 +4321,8 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='extract archive contents') subparser.set_defaults(func=self.do_extract) + subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + help='specify the archive name') 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', action='store_true', @@ -4332,7 +4343,6 @@ class Archiver: help='write all extracted data to stdout') subparser.add_argument('--sparse', dest='sparse', action='store_true', help='create holes in output sparse file from all-zero chunks') - # archive name subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to extract; patterns are supported') define_exclusion_group(subparser, strip_components=True) @@ -4370,7 +4380,8 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='show repository or archive information') subparser.set_defaults(func=self.do_info) - # archive name + subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + help='specify the archive name') subparser.add_argument('--json', action='store_true', help='format output as JSON') define_archive_filters_group(subparser) @@ -4759,7 +4770,8 @@ class Archiver: '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.') - # archive name + subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + help='specify the archive name') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to list; patterns are supported') define_archive_filters_group(subparser) @@ -4969,7 +4981,8 @@ class Archiver: 'HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the current defaults. ' 'default: %s,%d,%d,%d,%d' % CHUNKER_PARAMS) - # archive name + subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + help='specify the archive name') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to recreate; patterns are supported') @@ -4985,10 +4998,10 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='rename archive') subparser.set_defaults(func=self.do_rename) - subparser.add_argument('name_current', metavar='OLDNAME', + subparser.add_argument('--name', metavar='OLDNAME', type=archivename_validator(), help='the current archive name') - subparser.add_argument('name', metavar='NEWNAME', + subparser.add_argument('--name2', metavar='NEWNAME', type=archivename_validator(), help='the new archive name') @@ -5211,7 +5224,8 @@ class Archiver: help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') - # archive name + subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + help='specify the archive name') subparser.add_argument('tarfile', metavar='TARFILE', help='input tar file. "-" to read from stdin instead.') return parser diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 5c9f8b935..5203a2959 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -272,11 +272,11 @@ class FuseBackend: def _create_filesystem(self): self._create_dir(parent=1) # first call, create root dir (inode == 1) - if self._args.location.archive: + if self._args.name: if self.versions: raise Error("for versions view, do not specify a single archive, " "but always give the repository as location.") - self._process_archive(self._args.location.archive) + self._process_archive(self._args.name) else: self.versions_index = FuseVersionsIndex() for archive in self._manifest.archives.list_considering(self._args): diff --git a/src/borg/helpers/manifest.py b/src/borg/helpers/manifest.py index 1b9d91fb4..587176b49 100644 --- a/src/borg/helpers/manifest.py +++ b/src/borg/helpers/manifest.py @@ -103,7 +103,7 @@ class Archives(abc.MutableMapping): """ get a list of archives, considering --first/last/prefix/glob-archives/sort/consider-checkpoints cmdline args """ - if args.location.archive: + if args.name: raise Error('The options --first, --last, --prefix, and --glob-archives, and --consider-checkpoints can only be used on repository targets.') if args.prefix is not None: args.glob_archives = args.prefix + '*' diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 7c25437b0..ab420dea7 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -213,6 +213,8 @@ PrefixSpec = replace_placeholders GlobSpec = replace_placeholders +NameSpec = replace_placeholders + CommentSpec = replace_placeholders diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 1c67a5573..a22eebe4a 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -149,14 +149,14 @@ def test_return_codes(cmd, tmpdir): input.join('test_file').write('content') rc, out = cmd('--repo=%s' % str(repo), 'init', '--encryption=none') assert rc == EXIT_SUCCESS - rc, out = cmd('--repo=%s::archive' % repo, 'create', str(input)) + rc, out = cmd('--repo=%s' % repo, 'create', '--name=archive', str(input)) assert rc == EXIT_SUCCESS with changedir(str(output)): - rc, out = cmd('--repo=%s::archive' % repo, 'extract') + rc, out = cmd('--repo=%s' % repo, 'extract', '--name=archive') assert rc == EXIT_SUCCESS - rc, out = cmd('--repo=%s::archive' % repo, 'extract', 'does/not/match') + rc, out = cmd('--repo=%s' % repo, 'extract', '--name=archive', 'does/not/match') assert rc == EXIT_WARNING # pattern did not match - rc, out = cmd('--repo=%s::archive' % repo, 'create', str(input)) + rc, out = cmd('--repo=%s' % repo, 'create', '--name=archive', str(input)) assert rc == EXIT_ERROR # duplicate archive name @@ -219,7 +219,7 @@ def test_disk_full(cmd): break raise try: - rc, out = cmd('--repo=%s::test%03d' % (repo, i), 'create', input) + rc, out = cmd('--repo=%s' % repo, 'create', '--name=test%03d' % i, input) success = rc == EXIT_SUCCESS if not success: print('create', rc, out) @@ -299,7 +299,7 @@ class ArchiverTestCaseBase(BaseTestCase): return output def create_src_archive(self, name): - self.cmd(f'--repo={self.repository_location}::{name}', 'create', '--compression=lz4', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', f'--name={name}', '--compression=lz4', src_dir) def open_archive(self, name): repository = Repository(self.repository_path, exclusive=True) @@ -394,12 +394,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): output = self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey', '--show-version', '--show-rc', fork=True) self.assert_in('borgbackup version', output) self.assert_in('terminating with success status, rc 0', output) - self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude-nodump', 'input') - output = self.cmd(f'--repo={self.repository_location}::test.2', 'create', '--exclude-nodump', '--stats', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude-nodump', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', '--exclude-nodump', '--stats', 'input') self.assert_in('Archive name: test.2', output) self.assert_in('This archive: ', output) with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') list_output = self.cmd(f'--repo={self.repository_location}', 'list', '--short') self.assert_in('test', list_output) self.assert_in('test.2', list_output) @@ -427,15 +427,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): # remove the file we did not backup, so input and output become equal expected.remove('input/flagfile') # this file is UF_NODUMP os.remove(os.path.join('input', 'flagfile')) - list_output = self.cmd(f'--repo={self.repository_location}::test', 'list', '--short') + list_output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--short') for name in expected: self.assert_in(name, list_output) self.assert_dirs_equal('input', 'output/input') - info_output = self.cmd(f'--repo={self.repository_location}::test', 'info') + info_output = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') item_count = 5 if has_lchflags else 6 # one file is UF_NODUMP self.assert_in('Number of files: %d' % item_count, info_output) shutil.rmtree(self.cache_path) - info_output2 = self.cmd(f'--repo={self.repository_location}::test', 'info') + info_output2 = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') def filter(output): # filter for interesting "info" output, ignore cache rebuilding related stuff @@ -463,9 +463,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file(hl_a, contents=b'123456') os.link(hl_a, hl_b) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input', 'input') # give input twice! + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input', 'input') # give input twice! # test if created archive has 'input' contents twice: - archive_list = self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines') + archive_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines') paths = [json.loads(line)['path'] for line in archive_list.split('\n') if line] # we have all fs items exactly once! assert sorted(paths) == ['input', 'input/a', 'input/a/hardlink', 'input/b', 'input/b/hardlink'] @@ -491,19 +491,19 @@ class ArchiverTestCase(ArchiverTestCaseBase): pytest.skip('unix sockets disabled or not supported') elif err.errno == errno.EACCES: pytest.skip('permission denied to create unix sockets') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') sock.close() with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') assert os.readlink('input/link1') == 'somewhere' @pytest.mark.skipif(not are_symlinks_supported() or not are_hardlinks_supported(), @@ -514,9 +514,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.symlink('target', 'symlink1') os.link('symlink1', 'symlink2', follow_symlinks=False) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}::test', 'extract') + output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') print(output) with changedir('input'): assert os.path.exists('target') @@ -548,9 +548,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): have_noatime = has_noatime('input/file1') os.utime('input/file1', (atime, mtime)) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', '--atime', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--atime', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') sti = os.stat('input/file1') sto = os.stat('output/input/file1') assert sti.st_mtime_ns == sto.st_mtime_ns == mtime * 1e9 @@ -568,9 +568,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.utime('input/file1', (atime, birthtime)) os.utime('input/file1', (atime, mtime)) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') sti = os.stat('input/file1') sto = os.stat('output/input/file1') assert int(sti.st_birthtime * 1e9) == int(sto.st_birthtime * 1e9) == birthtime * 1e9 @@ -584,9 +584,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.utime('input/file1', (atime, birthtime)) os.utime('input/file1', (atime, mtime)) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', '--nobirthtime', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--nobirthtime', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') sti = os.stat('input/file1') sto = os.stat('output/input/file1') assert int(sti.st_birthtime * 1e9) == birthtime * 1e9 @@ -645,9 +645,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): # we could create a sparse input file, so creating a backup of it and # extracting it again (as sparse) should also work: self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir(self.output_path): - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--sparse') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--sparse') self.assert_dirs_equal('input', 'output/input') filename = os.path.join(self.output_path, 'input', 'sparse') with open(filename, 'rb') as fd: @@ -664,10 +664,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): with open(filename, 'wb'): pass self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') for filename in filenames: with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', os.path.join('input', filename)) + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', os.path.join('input', filename)) assert os.path.exists(os.path.join('output', 'input', filename)) def test_repository_swap_detection(self): @@ -675,70 +675,70 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.environ['BORG_PASSPHRASE'] = 'passphrase' self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') repository_id = self._extract_repository_id(self.repository_path) - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') shutil.rmtree(self.repository_path) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self._set_repository_id(self.repository_path, repository_id) self.assert_equal(repository_id, self._extract_repository_id(self.repository_path)) if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.EncryptionMethodMismatch): - self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') def test_repository_swap_detection2(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}_unencrypted', 'init', '--encryption=none') os.environ['BORG_PASSPHRASE'] = 'passphrase' self.cmd(f'--repo={self.repository_location}_encrypted', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}_encrypted::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}_encrypted', 'create', '--name=test', 'input') shutil.rmtree(self.repository_path + '_encrypted') os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted') if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}_encrypted::test.2', 'create', 'input', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}_encrypted', 'create', '--name=test.2', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.RepositoryAccessAborted): - self.cmd(f'--repo={self.repository_location}_encrypted::test.2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}_encrypted', 'create', '--name=test.2', 'input') def test_repository_swap_detection_no_cache(self): self.create_test_files() os.environ['BORG_PASSPHRASE'] = 'passphrase' self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') repository_id = self._extract_repository_id(self.repository_path) - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') shutil.rmtree(self.repository_path) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self._set_repository_id(self.repository_path, repository_id) self.assert_equal(repository_id, self._extract_repository_id(self.repository_path)) self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.EncryptionMethodMismatch): - self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') def test_repository_swap_detection2_no_cache(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}_unencrypted', 'init', '--encryption=none') os.environ['BORG_PASSPHRASE'] = 'passphrase' self.cmd(f'--repo={self.repository_location}_encrypted', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}_encrypted::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}_encrypted', 'create', '--name=test', 'input') self.cmd(f'--repo={self.repository_location}_unencrypted', 'delete', '--cache-only') self.cmd(f'--repo={self.repository_location}_encrypted', 'delete', '--cache-only') shutil.rmtree(self.repository_path + '_encrypted') os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted') if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}_encrypted::test.2', 'create', 'input', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}_encrypted', 'create', '--name=test.2', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.RepositoryAccessAborted): - self.cmd(f'--repo={self.repository_location}_encrypted::test.2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}_encrypted', 'create', '--name=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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=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=''): @@ -751,10 +751,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): # is set, while it isn't. Previously this raised no warning, # since the repository is, technically, encrypted. if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.CacheInitAbortedError): - self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') def test_repository_move(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -805,16 +805,16 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_strip_components(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('dir/file') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--strip-components', '3') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--strip-components', '3') assert not os.path.exists('file') with self.assert_creates_file('file'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--strip-components', '2') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--strip-components', '2') with self.assert_creates_file('dir/file'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--strip-components', '1') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--strip-components', '1') with self.assert_creates_file('input/dir/file'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--strip-components', '0') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--strip-components', '0') def _extract_hardlinks_setup(self): os.mkdir(os.path.join(self.input_path, 'dir1')) @@ -833,7 +833,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.path.join(self.input_path, 'dir1/aaaa')) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') @requires_hardlinks @unittest.skipUnless(llfuse, 'llfuse not installed') @@ -847,21 +847,21 @@ class ArchiverTestCase(ArchiverTestCaseBase): ignore_perms = ['-o', 'ignore_permissions,defer_permissions'] else: ignore_perms = ['-o', 'ignore_permissions'] - with self.fuse_mount(self.repository_location + '::test', mountpoint, '--strip-components=2', *ignore_perms), \ + with self.fuse_mount(self.repository_location, mountpoint, '--name=test', '--strip-components=2', *ignore_perms), \ changedir(mountpoint): assert os.stat('hardlink').st_nlink == 2 assert os.stat('subdir/hardlink').st_nlink == 2 assert open('subdir/hardlink', 'rb').read() == b'123456' assert os.stat('aaaa').st_nlink == 2 assert os.stat('source2').st_nlink == 2 - with self.fuse_mount(self.repository_location + '::test', mountpoint, 'input/dir1', *ignore_perms), \ + with self.fuse_mount(self.repository_location, mountpoint, 'input/dir1', '--name=test', *ignore_perms), \ changedir(mountpoint): assert os.stat('input/dir1/hardlink').st_nlink == 2 assert os.stat('input/dir1/subdir/hardlink').st_nlink == 2 assert open('input/dir1/subdir/hardlink', 'rb').read() == b'123456' assert os.stat('input/dir1/aaaa').st_nlink == 2 assert os.stat('input/dir1/source2').st_nlink == 2 - with self.fuse_mount(self.repository_location + '::test', mountpoint, *ignore_perms), \ + with self.fuse_mount(self.repository_location, mountpoint, '--name=test', *ignore_perms), \ changedir(mountpoint): assert os.stat('input/source').st_nlink == 4 assert os.stat('input/abba').st_nlink == 4 @@ -873,7 +873,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_extract_hardlinks1(self): self._extract_hardlinks_setup() with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') assert os.stat('input/source').st_nlink == 4 assert os.stat('input/abba').st_nlink == 4 assert os.stat('input/dir1/hardlink').st_nlink == 4 @@ -884,14 +884,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_extract_hardlinks2(self): self._extract_hardlinks_setup() with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--strip-components', '2') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--strip-components', '2') assert os.stat('hardlink').st_nlink == 2 assert os.stat('subdir/hardlink').st_nlink == 2 assert open('subdir/hardlink', 'rb').read() == b'123456' assert os.stat('aaaa').st_nlink == 2 assert os.stat('source2').st_nlink == 2 with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', 'input/dir1') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', 'input/dir1') assert os.stat('input/dir1/hardlink').st_nlink == 2 assert os.stat('input/dir1/subdir/hardlink').st_nlink == 2 assert open('input/dir1/subdir/hardlink', 'rb').read() == b'123456' @@ -910,10 +910,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file(hl_a, contents=b'123456') os.link(hl_a, hl_b) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input', 'input') # give input twice! + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input', 'input') # give input twice! # now test extraction with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') # if issue #5603 happens, extraction gives rc == 1 (triggering AssertionError) and warnings like: # input/a/hardlink: link: [Errno 2] No such file or directory: 'input/a/hardlink' -> 'input/a/hardlink' # input/b/hardlink: link: [Errno 2] No such file or directory: 'input/a/hardlink' -> 'input/b/hardlink' @@ -927,15 +927,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('file3', size=1024 * 80) self.create_regular_file('file4', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude=input/file4', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude=input/file4', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', 'input/file1', ) + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', 'input/file1', ) self.assert_equal(sorted(os.listdir('output/input')), ['file1']) with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude=input/file2') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude=input/file2') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3']) with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude-from=' + self.exclude_file_path) + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude-from=' + self.exclude_file_path) self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3']) def test_extract_include_exclude_regex(self): @@ -947,27 +947,27 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file333', size=1024 * 80) # Create with regular expression exclusion for file4 - self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude=re:input/file4$', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude=re:input/file4$', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333']) shutil.rmtree('output/input') # Extract with regular expression exclusion with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude=re:file3+') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude=re:file3+') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2']) shutil.rmtree('output/input') # Combine --exclude with fnmatch and regular expression with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude=input/file2', '--exclude=re:file[01]') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude=input/file2', '--exclude=re:file[01]') self.assert_equal(sorted(os.listdir('output/input')), ['file3', 'file333']) shutil.rmtree('output/input') # Combine --exclude-from and regular expression exclusion with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude-from=' + self.exclude_file_path, + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude-from=' + self.exclude_file_path, '--exclude=re:file1', '--exclude=re:file(\\d)\\1\\1$') self.assert_equal(sorted(os.listdir('output/input')), ['file3']) @@ -985,9 +985,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b're:input/file4$\n') fd.write(b'fm:*aa:*thing\n') - self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude-from=' + self.exclude_file_path, 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude-from=' + self.exclude_file_path, 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333']) shutil.rmtree('output/input') @@ -996,7 +996,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b're:file3+\n') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude-from=' + self.exclude_file_path) + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude-from=' + self.exclude_file_path) self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2']) shutil.rmtree('output/input') @@ -1008,7 +1008,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b're:file2$\n') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--exclude-from=' + self.exclude_file_path) + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude-from=' + self.exclude_file_path) self.assert_equal(sorted(os.listdir('output/input')), ['file3']) def test_extract_with_pattern(self): @@ -1019,63 +1019,63 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file("file4", size=1024 * 80) self.create_regular_file("file333", size=1024 * 80) - self.cmd(f'--repo={self.repository_location}::test', "create", "input") + self.cmd(f'--repo={self.repository_location}', "create", "--name=test", "input") # Extract everything with regular expression with changedir("output"): - self.cmd(f'--repo={self.repository_location}::test', "extract", "re:.*") + self.cmd(f'--repo={self.repository_location}', "extract", "--name=test", "re:.*") self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file3", "file333", "file4"]) shutil.rmtree("output/input") # Extract with pattern while also excluding files with changedir("output"): - self.cmd(f'--repo={self.repository_location}::test', "extract", "--exclude=re:file[34]$", r"re:file\d$") + self.cmd(f'--repo={self.repository_location}', "extract", "--name=test", "--exclude=re:file[34]$", r"re:file\d$") self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2"]) shutil.rmtree("output/input") # Combine --exclude with pattern for extraction with changedir("output"): - self.cmd(f'--repo={self.repository_location}::test', "extract", "--exclude=input/file1", "re:file[12]$") + self.cmd(f'--repo={self.repository_location}', "extract", "--name=test", "--exclude=input/file1", "re:file[12]$") self.assert_equal(sorted(os.listdir("output/input")), ["file2"]) shutil.rmtree("output/input") # Multiple pattern with changedir("output"): - self.cmd(f'--repo={self.repository_location}::test', "extract", "fm:input/file1", "fm:*file33*", "input/file2") + self.cmd(f'--repo={self.repository_location}', "extract", "--name=test", "fm:input/file1", "fm:*file33*", "input/file2") self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"]) def test_extract_list_output(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}::test', 'extract') + output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') self.assert_not_in("input/file", output) shutil.rmtree('output/input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}::test', 'extract', '--info') + output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--info') self.assert_not_in("input/file", output) shutil.rmtree('output/input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}::test', 'extract', '--list') + output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--list') self.assert_in("input/file", output) shutil.rmtree('output/input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}::test', 'extract', '--list', '--info') + output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--list', '--info') self.assert_in("input/file", output) def test_extract_progress(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}::test', 'extract', '--progress') + output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--progress') assert 'Extracting:' in output def _create_test_caches(self): @@ -1095,32 +1095,32 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_stdin(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') input_data = b'\x00foo\n\nbar\n \n' - self.cmd(f'--repo={self.repository_location}::test', 'create', '-', input=input_data) - item = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines')) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '-', input=input_data) + item = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines')) assert item['uid'] == 0 assert item['gid'] == 0 assert item['size'] == len(input_data) assert item['path'] == 'stdin' - extracted_data = self.cmd(f'--repo={self.repository_location}::test', 'extract', '--stdout', binary_output=True) + extracted_data = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--stdout', binary_output=True) assert extracted_data == input_data def test_create_content_from_command(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') input_data = 'some test content' name = 'a/b/c' - self.cmd(f'--repo={self.repository_location}::test', 'create', '--stdin-name', name, '--content-from-command', + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--stdin-name', name, '--content-from-command', '--', 'echo', input_data) - item = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines')) + item = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines')) assert item['uid'] == 0 assert item['gid'] == 0 assert item['size'] == len(input_data) + 1 # `echo` adds newline assert item['path'] == name - extracted_data = self.cmd(f'--repo={self.repository_location}::test', 'extract', '--stdout') + extracted_data = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--stdout') assert extracted_data == input_data + '\n' def test_create_content_from_command_with_failed_command(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}::test', 'create', '--content-from-command', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--content-from-command', '--', 'sh', '-c', 'exit 73;', exit_code=2) assert output.endswith("Command 'sh' exited with status 73\n") archive_list = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--json')) @@ -1128,7 +1128,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_content_from_command_missing_command(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}::test', 'create', '--content-from-command', exit_code=2) + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--content-from-command', exit_code=2) assert output.endswith('No command given.\n') def test_create_paths_from_stdin(self): @@ -1139,9 +1139,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file("file4", size=1024 * 80) input_data = b'input/file1\0input/dir1\0input/file4' - self.cmd(f'--repo={self.repository_location}::test', 'create', + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--paths-from-stdin', '--paths-delimiter', '\\0', input=input_data) - archive_list = self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines') + archive_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines') paths = [json.loads(line)['path'] for line in archive_list.split('\n') if line] assert paths == ['input/file1', 'input/dir1', 'input/file4'] @@ -1153,15 +1153,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file("file4", size=1024 * 80) input_data = 'input/file1\ninput/file2\ninput/file3' - self.cmd(f'--repo={self.repository_location}::test', 'create', '--paths-from-command', + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--paths-from-command', '--', 'echo', input_data) - archive_list = self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines') + archive_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines') paths = [json.loads(line)['path'] for line in archive_list.split('\n') if line] assert paths == ['input/file1', 'input/file2', 'input/file3'] def test_create_paths_from_command_with_failed_command(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}::test', 'create', '--paths-from-command', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--paths-from-command', '--', 'sh', '-c', 'exit 73;', exit_code=2) assert output.endswith("Command 'sh' exited with status 73\n") archive_list = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--json')) @@ -1169,20 +1169,20 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_paths_from_command_missing_command(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}::test', 'create', '--paths-from-command', exit_code=2) + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--paths-from-command', exit_code=2) assert output.endswith('No command given.\n') def test_create_without_root(self): """test create without a root""" self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', exit_code=2) def test_create_pattern_root(self): """test create with only a root pattern""" self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) - output = self.cmd(f'--repo={self.repository_location}::test', 'create', '-v', '--list', '--pattern=R input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '-v', '--list', '--pattern=R input') self.assert_in("A input/file1", output) self.assert_in("A input/file2", output) @@ -1192,7 +1192,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}::test', 'create', '-v', '--list', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '-v', '--list', '--pattern=+input/file_important', '--pattern=-input/file*', 'input') self.assert_in("A input/file_important", output) @@ -1206,7 +1206,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}::test', 'create', '-v', '--list', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '-v', '--list', '--pattern=-input/otherfile', '--patterns-from=' + self.patterns_file_path, 'input') self.assert_in("A input/file_important", output) @@ -1224,7 +1224,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}::test', 'create', '-v', '--list', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '-v', '--list', '--patterns-from=' + self.patterns_file_path2, 'input') self.assert_in('x input/x/a/foo_a', output) @@ -1241,7 +1241,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}::test', 'create', '-v', '--list', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '-v', '--list', '--patterns-from=' + self.patterns_file_path2, 'input') self.assert_not_in('input/x/a/foo_a', output) @@ -1259,12 +1259,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}::test', 'create', '--patterns-from=' + self.patterns_file_path2, - '.') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', + '--patterns-from=' + self.patterns_file_path2, '.') # list the archive and verify that the "intermediate" folders appear before # their contents - out = self.cmd(f'--repo={self.repository_location}::test', 'list', '--format', '{type} {path}{NL}') + out = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{type} {path}{NL}') out_list = out.splitlines() self.assert_in('d x/a', out_list) @@ -1277,14 +1277,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') - create_json = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'create', '--no-cache-sync', 'input', + create_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', + '--no-cache-sync', 'input', '--json', '--error')) # ignore experimental warning - info_json = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'info', '--json')) + info_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--name=test', '--json')) create_stats = create_json['cache']['stats'] info_stats = info_json['cache']['stats'] assert create_stats == info_stats self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') - self.cmd(f'--repo={self.repository_location}::test2', 'create', '--no-cache-sync', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', '--no-cache-sync', 'input') self.cmd(f'--repo={self.repository_location}', 'info') self.cmd(f'--repo={self.repository_location}', 'check') @@ -1293,27 +1294,27 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--pattern=+input/file_important', '--pattern=-input/file*') self.assert_equal(sorted(os.listdir('output/input')), ['file_important']) def _assert_test_caches(self): with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') self.assert_equal(sorted(os.listdir('output/input')), ['cache2', 'file1']) self.assert_equal(sorted(os.listdir('output/input/cache2')), [CACHE_TAG_NAME]) def test_exclude_caches(self): self._create_test_caches() - self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude-caches', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude-caches', 'input') self._assert_test_caches() def test_recreate_exclude_caches(self): self._create_test_caches() - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test', 'recreate', '--exclude-caches') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--exclude-caches') self._assert_test_caches() def _create_test_tagged(self): @@ -1325,19 +1326,19 @@ class ArchiverTestCase(ArchiverTestCaseBase): def _assert_test_tagged(self): with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') self.assert_equal(sorted(os.listdir('output/input')), ['file1']) def test_exclude_tagged(self): self._create_test_tagged() - self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude-if-present', '.NOBACKUP', + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP', 'input') self._assert_test_tagged() def test_recreate_exclude_tagged(self): self._create_test_tagged() - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test', 'recreate', '--exclude-if-present', '.NOBACKUP', + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP') self._assert_test_tagged() @@ -1359,7 +1360,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def _assert_test_keep_tagged(self): with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') self.assert_equal(sorted(os.listdir('output/input')), ['file0', 'tagged1', 'tagged2', 'tagged3', 'taggedall']) self.assert_equal(os.listdir('output/input/tagged1'), ['.NOBACKUP1']) self.assert_equal(os.listdir('output/input/tagged2'), ['.NOBACKUP2']) @@ -1369,14 +1370,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_exclude_keep_tagged(self): self._create_test_keep_tagged() - self.cmd(f'--repo={self.repository_location}::test', 'create', '--exclude-if-present', '.NOBACKUP1', + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2', '--exclude-caches', '--keep-exclude-tags', 'input') self._assert_test_keep_tagged() def test_recreate_exclude_keep_tagged(self): self._create_test_keep_tagged() - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test', 'recreate', '--exclude-if-present', '.NOBACKUP1', + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2', '--exclude-caches', '--keep-exclude-tags') self._assert_test_keep_tagged() @@ -1387,11 +1388,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.mkdir(os.path.join(self.input_path, 'subdir')) # to make sure the tag is encountered *after* file1 os.link(os.path.join(self.input_path, 'file1'), os.path.join(self.input_path, 'subdir', CACHE_TAG_NAME)) # correct tag name, hardlink to file1 - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') # in the "test" archive, we now have, in this order: # - a regular file item for "file1" # - a hardlink item for "CACHEDIR.TAG" referring back to file1 for its contents - self.cmd(f'--repo={self.repository_location}::test', 'recreate', '--exclude-caches', '--keep-exclude-tags') + self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--exclude-caches', '--keep-exclude-tags') # if issue #4911 is present, the recreate will crash with a KeyError for "input/file1" @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='Linux capabilities test, requires fakeroot >= 1.20.2') @@ -1409,10 +1410,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file') xattr.setxattr(b'input/file', b'security.capability', capabilities) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir('output'): with patch.object(os, 'fchown', patched_fchown): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') assert xattr.getxattr(b'input/file', b'security.capability') == capabilities @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='xattr not supported on this system or on this version of' @@ -1430,19 +1431,19 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file') xattr.setxattr(b'input/file', b'user.attribute', b'value') self.cmd(f'--repo={self.repository_location}', 'init', '-e' 'none') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir('output'): input_abspath = os.path.abspath('input/file') with patch.object(xattr, 'setxattr', patched_setxattr_E2BIG): - out = self.cmd(f'--repo={self.repository_location}::test', 'extract', exit_code=EXIT_WARNING) + out = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_WARNING) assert ': when setting extended attribute user.attribute: too big for this filesystem\n' in out os.remove(input_abspath) with patch.object(xattr, 'setxattr', patched_setxattr_ENOTSUP): - out = self.cmd(f'--repo={self.repository_location}::test', 'extract', exit_code=EXIT_WARNING) + out = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_WARNING) assert ': when setting extended attribute user.attribute: xattrs not supported on this filesystem\n' in out os.remove(input_abspath) with patch.object(xattr, 'setxattr', patched_setxattr_EACCES): - out = self.cmd(f'--repo={self.repository_location}::test', 'extract', exit_code=EXIT_WARNING) + out = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_WARNING) assert ': when setting extended attribute user.attribute: Permission denied\n' in out assert os.path.isfile(input_abspath) @@ -1450,8 +1451,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('dir1/dir2/file', size=1024 * 80) with changedir('input/dir1/dir2'): - self.cmd(f'--repo={self.repository_location}::test', 'create', '../../../input/dir1/../dir1/dir2/..') - output = self.cmd(f'--repo={self.repository_location}::test', 'list') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', + '../../../input/dir1/../dir1/dir2/..') + output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') self.assert_not_in('..', output) self.assert_in(' input/dir1/dir2/file', output) @@ -1460,57 +1462,57 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) with changedir('input'): - self.cmd(f'--repo={self.repository_location}::test1', 'create', '--exclude=file1', '.') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', '--exclude=file1', '.') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test1', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test1') self.assert_equal(sorted(os.listdir('output')), ['file2']) with changedir('input'): - self.cmd(f'--repo={self.repository_location}::test2', 'create', '--exclude=./file1', '.') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', '--exclude=./file1', '.') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test2', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test2') self.assert_equal(sorted(os.listdir('output')), ['file2']) - self.cmd(f'--repo={self.repository_location}::test3', 'create', '--exclude=input/./file1', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3', '--exclude=input/./file1', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test3', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test3') self.assert_equal(sorted(os.listdir('output/input')), ['file2']) def test_repeated_files(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') # Overwriting regular files and directories should be supported os.mkdir('output/input') os.mkdir('output/input/file1') os.mkdir('output/input/dir2') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') self.assert_dirs_equal('input', 'output/input') # But non-empty dirs should fail os.unlink('output/input/file1') os.mkdir('output/input/file1') os.mkdir('output/input/file1/dir') with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract', exit_code=1) + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=1) def test_rename(self): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('dir2/file2', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--dry-run') - self.cmd(f'--repo={self.repository_location}::test.2', 'extract', '--dry-run') - self.cmd(f'--repo={self.repository_location}::test', 'rename', 'TODO_test', 'test.3') - self.cmd(f'--repo={self.repository_location}::test.2', 'extract', '--dry-run') - self.cmd(f'--repo={self.repository_location}::test.2', 'rename', 'TODO_test.2', 'test.4') - self.cmd(f'--repo={self.repository_location}::test.3', 'extract', '--dry-run') - self.cmd(f'--repo={self.repository_location}::test.4', 'extract', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.2', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'rename', '--name=test', '--name2=test.3') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.2', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'rename', '--name=test.2', '--name2=test.4') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.3', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.4', '--dry-run') # Make sure both archives have been renamed with Repository(self.repository_path) as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -1521,10 +1523,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_info(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') info_repo = self.cmd(f'--repo={self.repository_location}', 'info') assert 'All archives:' in info_repo - info_archive = self.cmd(f'--repo={self.repository_location}::test', 'info') + info_archive = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') assert 'Archive name: test\n' in info_archive info_archive = self.cmd(f'--repo={self.repository_location}', 'info', '--first', '1') assert 'Archive name: test\n' in info_archive @@ -1532,7 +1534,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_info_json(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') info_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--json')) repository = info_repo['repository'] assert len(repository['id']) == 64 @@ -1545,7 +1547,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert all(isinstance(o, int) for o in stats.values()) assert all(key in stats for key in ('total_chunks', 'total_size', 'total_unique_chunks', 'unique_size')) - info_archive = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'info', '--json')) + info_archive = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--name=test', '--json')) assert info_repo['repository'] == info_archive['repository'] assert info_repo['cache'] == info_archive['cache'] archives = info_archive['archives'] @@ -1570,38 +1572,38 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_comment(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test1', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test2', 'create', '--comment', 'this is the comment', 'input') - self.cmd(f'--repo={self.repository_location}::test3', 'create', '--comment', '"deleted" comment', 'input') - self.cmd(f'--repo={self.repository_location}::test4', 'create', '--comment', 'preserved comment', 'input') - assert 'Comment: \n' in self.cmd(f'--repo={self.repository_location}::test1', 'info') - assert 'Comment: this is the comment' in self.cmd(f'--repo={self.repository_location}::test2', 'info') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', '--comment', 'this is the comment', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3', '--comment', '"deleted" comment', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test4', '--comment', 'preserved comment', 'input') + assert 'Comment: \n' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test1') + assert 'Comment: this is the comment' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test2') - self.cmd(f'--repo={self.repository_location}::test1', 'recreate', '--comment', 'added comment') - self.cmd(f'--repo={self.repository_location}::test2', 'recreate', '--comment', 'modified comment') - self.cmd(f'--repo={self.repository_location}::test3', 'recreate', '--comment', '') - self.cmd(f'--repo={self.repository_location}::test4', 'recreate', '12345') - assert 'Comment: added comment' in self.cmd(f'--repo={self.repository_location}::test1', 'info') - assert 'Comment: modified comment' in self.cmd(f'--repo={self.repository_location}::test2', 'info') - assert 'Comment: \n' in self.cmd(f'--repo={self.repository_location}::test3', 'info') - assert 'Comment: preserved comment' in self.cmd(f'--repo={self.repository_location}::test4', 'info') + self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test1', '--comment', 'added comment') + self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test2', '--comment', 'modified comment') + self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test3', '--comment', '') + self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test4', '12345') + assert 'Comment: added comment' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test1') + assert 'Comment: modified comment' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test2') + assert 'Comment: \n' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test3') + assert 'Comment: preserved comment' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test4') def test_delete(self): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('dir2/file2', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test.3', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::another_test.1', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::another_test.2', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--dry-run') - self.cmd(f'--repo={self.repository_location}::test.2', 'extract', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.3', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=another_test.1', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=another_test.2', 'input') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.2', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'delete', '--prefix', 'another_') self.cmd(f'--repo={self.repository_location}', 'delete', '--last', '1') - self.cmd(f'--repo={self.repository_location}::test', 'delete') - self.cmd(f'--repo={self.repository_location}::test.2', 'extract', '--dry-run') - output = self.cmd(f'--repo={self.repository_location}::test.2', 'delete', '--stats') + self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.2', '--dry-run') + output = self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test.2', '--stats') self.assert_in('Deleted data:', output) # Make sure all data except the manifest has been deleted with Repository(self.repository_path) as repository: @@ -1610,20 +1612,20 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_delete_multiple(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test1', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test2', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test3', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test1', 'delete', 'test2') - self.cmd(f'--repo={self.repository_location}::test3', 'extract', '--dry-run') - self.cmd(f'--repo={self.repository_location}::test3', 'delete') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3', 'input') + self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test1', 'test2') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test3', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test3') assert not self.cmd(f'--repo={self.repository_location}', 'list') def test_delete_repo(self): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('dir2/file2', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'no' self.cmd(f'--repo={self.repository_location}', 'delete', exit_code=2) assert os.path.exists(self.repository_path) @@ -1645,7 +1647,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): else: assert False # missed the file repository.commit(compact=False) - output = self.cmd(f'--repo={self.repository_location}::test', 'delete', '--force') + output = self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test', '--force') self.assert_in('deleted archive was corrupted', output) self.cmd(f'--repo={self.repository_location}', 'check', '--repair') output = self.cmd(f'--repo={self.repository_location}', 'list') @@ -1660,7 +1662,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): id = archive.metadata.items[0] repository.put(id, b'corrupted items metadata stream chunk') repository.commit(compact=False) - self.cmd(f'--repo={self.repository_location}::test', 'delete', '--force', '--force') + self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test', '--force', '--force') self.cmd(f'--repo={self.repository_location}', 'check', '--repair') output = self.cmd(f'--repo={self.repository_location}', 'list') self.assert_not_in('test', output) @@ -1668,7 +1670,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_corrupted_repository(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('test') - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--dry-run') output = self.cmd(f'--repo={self.repository_location}', 'check', '--show-version') self.assert_in('borgbackup version', output) # implied output even without --info given self.assert_not_in('Starting repository check', output) # --info not given for root logger @@ -1702,14 +1704,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}::a', 'diff', 'TODO_a', 'b', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'diff', '--name=a', '--name2=b', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd(f'--repo={self.repository_location}::a', 'diff', 'TODO_a', 'b') + self.cmd(f'--repo={self.repository_location}', 'diff', '--name=a', '--name=b') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd(f'--repo={self.repository_location}::a', 'diff', 'TODO_a', 'b', '--bypass-lock') + self.cmd(f'--repo={self.repository_location}', 'diff', '--name=a', '--name2=b', '--bypass-lock') def test_readonly_export_tar(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -1717,14 +1719,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'test.tar', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'test.tar', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'test.tar') + self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'test.tar') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'test.tar', '--bypass-lock') + self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'test.tar', '--bypass-lock') def test_readonly_extract(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -1732,14 +1734,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}::test', 'extract', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd(f'--repo={self.repository_location}::test', 'extract', '--bypass-lock') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--bypass-lock') def test_readonly_info(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -1795,13 +1797,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_umask(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', '--dry-run', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--dry-run', 'input') # Make sure no archive has been created with Repository(self.repository_path) as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -1825,13 +1827,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_unknown_feature_on_create(self): print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) self.add_unknown_feature(Manifest.Operation.WRITE) - self.cmd_raises_unknown_feature([f'--repo={self.repository_location}::test', 'create', 'input']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'create', '--name=test', 'input']) def test_unknown_feature_on_cache_sync(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') self.add_unknown_feature(Manifest.Operation.READ) - self.cmd_raises_unknown_feature([f'--repo={self.repository_location}::test', 'create', 'input']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'create', '--name=test', 'input']) def test_unknown_feature_on_change_passphrase(self): print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) @@ -1840,26 +1842,26 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_unknown_feature_on_read(self): print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') self.add_unknown_feature(Manifest.Operation.READ) with changedir('output'): - self.cmd_raises_unknown_feature([f'--repo={self.repository_location}::test', 'extract']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'extract', '--name=test']) self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'list']) - self.cmd_raises_unknown_feature([f'--repo={self.repository_location}::test', 'info']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'info', '--name=test']) def test_unknown_feature_on_rename(self): print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') self.add_unknown_feature(Manifest.Operation.CHECK) - self.cmd_raises_unknown_feature([f'--repo={self.repository_location}::test', 'rename', 'TODO_test', 'other']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'rename', '--name=test', '--name2=other']) def test_unknown_feature_on_delete(self): print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') self.add_unknown_feature(Manifest.Operation.DELETE) # delete of an archive raises - self.cmd_raises_unknown_feature([f'--repo={self.repository_location}::test', 'delete']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'delete', '--name=test']) self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'prune', '--keep-daily=3']) # delete of the whole repository ignores features self.cmd(f'--repo={self.repository_location}', 'delete') @@ -1867,7 +1869,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(llfuse, 'llfuse not installed') def test_unknown_feature_on_mount(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') self.add_unknown_feature(Manifest.Operation.READ) mountpoint = os.path.join(self.tmpdir, 'mountpoint') os.mkdir(mountpoint) @@ -1893,7 +1895,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): cache.commit() if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') else: called = False wipe_cache_safe = LocalCache.wipe_cache @@ -1904,7 +1906,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): wipe_cache_safe(*args) with patch.object(LocalCache, 'wipe_cache', wipe_wrapper): - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') assert called @@ -1918,13 +1920,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_progress_on(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}::test4', 'create', '--progress', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test4', '--progress', 'input') self.assert_in("\r", output) def test_progress_off(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}::test5', 'create', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test5', 'input') self.assert_not_in("\r", output) def test_file_status(self): @@ -1935,11 +1937,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}::test', 'create', '--list', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--list', 'input') self.assert_in("A input/file1", output) self.assert_in("A input/file2", output) # should find first file as unmodified - output = self.cmd(f'--repo={self.repository_location}::test1', 'create', '--list', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', '--list', 'input') self.assert_in("U input/file1", output) # this is expected, although surprising, for why, see: # https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file @@ -1951,14 +1953,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=10) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}::test1', 'create', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', '--list', '--files-cache=ctime,size', 'input') # modify file1, but cheat with the mtime (and atime) and also keep same size: st = os.stat('input/file1') self.create_regular_file('file1', contents=b'321') os.utime('input/file1', ns=(st.st_atime_ns, st.st_mtime_ns)) # this mode uses ctime for change detection, so it should find file1 as modified - output = self.cmd(f'--repo={self.repository_location}::test2', 'create', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', '--list', '--files-cache=ctime,size', 'input') self.assert_in("M input/file1", output) @@ -1968,13 +1970,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=10) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}::test1', 'create', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', '--list', '--files-cache=mtime,size', 'input') # change mode of file1, no content change: st = os.stat('input/file1') os.chmod('input/file1', st.st_mode ^ stat.S_IRWXO) # this triggers a ctime change, but mtime is unchanged # this mode uses mtime for change detection, so it should find file1 as unmodified - output = self.cmd(f'--repo={self.repository_location}::test2', 'create', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', '--list', '--files-cache=mtime,size', 'input') self.assert_in("U input/file1", output) @@ -1984,10 +1986,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=10) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}::test1', 'create', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', '--list', '--files-cache=rechunk,ctime', 'input') # no changes here, but this mode rechunks unconditionally - output = self.cmd(f'--repo={self.repository_location}::test2', 'create', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', '--list', '--files-cache=rechunk,ctime', 'input') self.assert_in("A input/file1", output) @@ -2001,13 +2003,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file3', size=1024 * 80) platform.set_flags(os.path.join(self.input_path, 'file3'), stat.UF_NODUMP) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}::test', 'create', '--list', '--exclude-nodump', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--list', '--exclude-nodump', 'input') self.assert_in("A input/file1", output) self.assert_in("A input/file2", output) if has_lchflags: self.assert_in("x input/file3", output) # should find second file as excluded - output = self.cmd(f'--repo={self.repository_location}::test1', 'create', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', '--list', '--exclude-nodump', 'input', '--exclude', '*/file2') self.assert_in("U input/file1", output) self.assert_in("x input/file2", output) @@ -2017,7 +2019,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_json(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - create_info = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'create', + create_info = json.loads(self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--json', 'input')) # The usual keys assert 'encryption' in create_info @@ -2038,21 +2040,21 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file2', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') # no listing by default - output = self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') self.assert_not_in('file1', output) # shouldn't be listed even if unchanged - output = self.cmd(f'--repo={self.repository_location}::test0', 'create', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test0', 'input') self.assert_not_in('file1', output) # should list the file as unchanged - output = self.cmd(f'--repo={self.repository_location}::test1', 'create', '--list', '--filter=U', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', '--list', '--filter=U', 'input') self.assert_in('file1', output) # should *not* list the file as changed - output = self.cmd(f'--repo={self.repository_location}::test2', 'create', '--list', '--filter=AM', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', '--list', '--filter=AM', 'input') self.assert_not_in('file1', output) # change the file self.create_regular_file('file1', size=1024 * 100) # should list the file as changed - output = self.cmd(f'--repo={self.repository_location}::test3', 'create', '--list', '--filter=AM', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3', '--list', '--filter=AM', 'input') self.assert_in('file1', output) @pytest.mark.skipif(not are_fifos_supported(), reason='FIFOs not supported') @@ -2077,11 +2079,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): t = Thread(target=fifo_feeder, args=(fifo_fn, data)) t.start() try: - self.cmd(f'--repo={self.repository_location}::test', 'create', '--read-special', 'input/link_fifo') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--read-special', 'input/link_fifo') finally: t.join() with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') fifo_fn = 'input/link_fifo' with open(fifo_fn, 'rb') as f: extracted_data = f.read() @@ -2090,25 +2092,25 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_read_special_broken_symlink(self): os.symlink('somewhere does not exist', os.path.join(self.input_path, 'link')) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', '--read-special', 'input') - output = self.cmd(f'--repo={self.repository_location}::test', 'list') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--read-special', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') assert 'input/link -> somewhere does not exist' in output # def test_cmdline_compatibility(self): # self.create_regular_file('file1', size=1024 * 80) # self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - # self.cmd('create', self.repository_location + '::test', 'input') + # self.cmd(f'--repo={self.repository_location}', 'create', '--name=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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test1', 'create', src_dir) - self.cmd(f'--repo={self.repository_location}::test2', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', src_dir) # these are not really a checkpoints, but they look like some: - self.cmd(f'--repo={self.repository_location}::test3.checkpoint', 'create', src_dir) - self.cmd(f'--repo={self.repository_location}::test3.checkpoint.1', 'create', src_dir) - self.cmd(f'--repo={self.repository_location}::test4.checkpoint', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3.checkpoint', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3.checkpoint.1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test4.checkpoint', src_dir) output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1') assert re.search(r'Would prune:\s+test1', output) # must keep the latest non-checkpoint archive: @@ -2131,7 +2133,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_not_in('test3.checkpoint.1', output) self.assert_in('test4.checkpoint', output) # now we supersede the latest checkpoint by a successful backup: - self.cmd(f'--repo={self.repository_location}::test5', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test5', src_dir) self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=2') output = self.cmd(f'--repo={self.repository_location}', 'list', '--consider-checkpoints') # all checkpoints should be gone now: @@ -2146,7 +2148,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): return dtime.astimezone(dateutil.tz.UTC).strftime("%Y-%m-%dT%H:%M:%S") def _create_archive_ts(self, name, y, m, d, H=0, M=0, S=0): - self.cmd(f'--repo={self.repository_location}::{name}', 'create', + self.cmd(f'--repo={self.repository_location}', 'create', f'--name={name}', '--timestamp', self._to_utc_timestamp(y, m, d, H, M, S), src_dir) # This test must match docs/misc/prune-example.txt @@ -2236,8 +2238,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_prune_repository_save_space(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test1', 'create', src_dir) - self.cmd(f'--repo={self.repository_location}::test2', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', src_dir) output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1') assert re.search(r'Keeping archive \(rule: daily #1\):\s+test2', output) assert re.search(r'Would prune:\s+test1', output) @@ -2251,10 +2253,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_prune_repository_prefix(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::foo-2015-08-12-10:00', 'create', src_dir) - self.cmd(f'--repo={self.repository_location}::foo-2015-08-12-20:00', 'create', src_dir) - self.cmd(f'--repo={self.repository_location}::bar-2015-08-12-10:00', 'create', src_dir) - self.cmd(f'--repo={self.repository_location}::bar-2015-08-12-20:00', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=foo-2015-08-12-10:00', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=foo-2015-08-12-20:00', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=bar-2015-08-12-10:00', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=bar-2015-08-12-20:00', src_dir) output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1', '--prefix=foo-') assert re.search(r'Keeping archive \(rule: daily #1\):\s+foo-2015-08-12-20:00', output) assert re.search(r'Would prune:\s+foo-2015-08-12-10:00', output) @@ -2272,10 +2274,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_prune_repository_glob(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::2015-08-12-10:00-foo', 'create', src_dir) - self.cmd(f'--repo={self.repository_location}::2015-08-12-20:00-foo', 'create', src_dir) - self.cmd(f'--repo={self.repository_location}::2015-08-12-10:00-bar', 'create', src_dir) - self.cmd(f'--repo={self.repository_location}::2015-08-12-20:00-bar', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=2015-08-12-10:00-foo', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=2015-08-12-20:00-foo', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=2015-08-12-10:00-bar', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=2015-08-12-20:00-bar', src_dir) output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1', '--glob-archives=2015-*-foo') assert re.search(r'Keeping archive \(rule: daily #1\):\s+2015-08-12-20:00-foo', output) assert re.search(r'Would prune:\s+2015-08-12-10:00-foo', output) @@ -2293,9 +2295,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_list_prefix(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test-1', 'create', src_dir) - self.cmd(f'--repo={self.repository_location}::something-else-than-test-1', 'create', src_dir) - self.cmd(f'--repo={self.repository_location}::test-2', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test-1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=something-else-than-test-1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test-2', src_dir) output = self.cmd(f'--repo={self.repository_location}', 'list', '--prefix=test-') self.assert_in('test-1', output) self.assert_in('test-2', output) @@ -2303,17 +2305,17 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_list_format(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', src_dir) - output_1 = self.cmd(f'--repo={self.repository_location}::test', 'list') - output_2 = self.cmd(f'--repo={self.repository_location}::test', 'list', '--format', '{mode} {user:6} {group:6} {size:8d} {mtime} {path}{extra}{NEWLINE}') - output_3 = self.cmd(f'--repo={self.repository_location}::test', 'list', '--format', '{mtime:%s} {path}{NL}') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', src_dir) + output_1 = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') + output_2 = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{mode} {user:6} {group:6} {size:8d} {mtime} {path}{extra}{NEWLINE}') + output_3 = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{mtime:%s} {path}{NL}') self.assertEqual(output_1, output_2) self.assertNotEqual(output_1, output_3) def test_list_repository_format(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test-1', 'create', '--comment', 'comment 1', src_dir) - self.cmd(f'--repo={self.repository_location}::test-2', 'create', '--comment', 'comment 2', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test-1', '--comment', 'comment 1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test-2', '--comment', 'comment 2', src_dir) output_1 = self.cmd(f'--repo={self.repository_location}', 'list') output_2 = self.cmd(f'--repo={self.repository_location}', 'list', '--format', '{archive:<36} {time} [{id}]{NL}') self.assertEqual(output_1, output_2) @@ -2329,17 +2331,17 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('empty_file', size=0) self.create_regular_file('amb', contents=b'a' * 1000000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - output = self.cmd(f'--repo={self.repository_location}::test', 'list', '--format', '{sha256} {path}{NL}') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{sha256} {path}{NL}') assert "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0 input/amb" in output assert "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 input/empty_file" in output def test_list_consider_checkpoints(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test1', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', src_dir) # these are not really a checkpoints, but they look like some: - self.cmd(f'--repo={self.repository_location}::test2.checkpoint', 'create', src_dir) - self.cmd(f'--repo={self.repository_location}::test3.checkpoint.1', 'create', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2.checkpoint', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3.checkpoint.1', src_dir) output = self.cmd(f'--repo={self.repository_location}', 'list') assert "test1" in output assert "test2.checkpoint" not in output @@ -2356,23 +2358,23 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b'abba' * 2000000) fd.write(b'baab' * 2000000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - output = self.cmd(f'--repo={self.repository_location}::test', 'list', '--format', '{num_chunks} {unique_chunks} {path}{NL}') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{num_chunks} {unique_chunks} {path}{NL}') assert "0 0 input/empty_file" in output assert "2 2 input/two_chunks" in output def test_list_size(self): self.create_regular_file('compressible_file', size=10000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', '-C', 'lz4', 'input') - output = self.cmd(f'--repo={self.repository_location}::test', 'list', '--format', '{size} {path}{NL}') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '-C', 'lz4', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{size} {path}{NL}') size, path = output.split("\n")[1].split(" ") assert int(size) == 10000 def test_list_json(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') list_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--json')) repository = list_repo['repository'] assert len(repository['id']) == 64 @@ -2382,7 +2384,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): archive0 = list_repo['archives'][0] assert datetime.strptime(archive0['time'], ISO_FORMAT) # must not raise - list_archive = self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines') + list_archive = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines') items = [json.loads(s) for s in list_archive.splitlines()] assert len(items) == 2 file1 = items[1] @@ -2390,7 +2392,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert file1['size'] == 81920 assert datetime.strptime(file1['mtime'], ISO_FORMAT) # must not raise - list_archive = self.cmd(f'--repo={self.repository_location}::test', 'list', '--json-lines', '--format={sha256}') + list_archive = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines', '--format={sha256}') items = [json.loads(s) for s in list_archive.splitlines()] assert len(items) == 2 file1 = items[1] @@ -2400,12 +2402,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_list_json_args(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'list', '--json-lines', exit_code=2) - self.cmd(f'--repo={self.repository_location}::archive', 'list', '--json', exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'list', '--name=archive', '--json', exit_code=2) def test_log_json(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - log = self.cmd(f'--repo={self.repository_location}::test', 'create', '--log-json', 'input', '--list', '--debug') + log = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--log-json', 'input', '--list', '--debug') messages = {} # type -> message, one of each kind for line in log.splitlines(): msg = json.loads(line) @@ -2423,13 +2425,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_debug_profile(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input', '--debug-profile=create.prof') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input', '--debug-profile=create.prof') self.cmd('debug', 'convert-profile', 'create.prof', 'create.pyprof') stats = pstats.Stats('create.pyprof') stats.strip_dirs() stats.sort_stats('cumtime') - self.cmd(f'--repo={self.repository_location}::test2', 'create', 'input', '--debug-profile=create.pyprof') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', 'input', '--debug-profile=create.pyprof') stats = pstats.Stats('create.pyprof') # Only do this on trusted data! stats.strip_dirs() stats.sort_stats('cumtime') @@ -2437,7 +2439,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_common_options(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - log = self.cmd(f'--repo={self.repository_location}::test', '--debug', 'create', 'input') + log = self.cmd(f'--repo={self.repository_location}', '--debug', 'create', '--name=test', 'input') assert 'security: read previous location' in log def test_change_passphrase(self): @@ -2511,8 +2513,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_test_files() have_noatime = has_noatime('input/file1') - self.cmd(f'--repo={self.repository_location}::archive', 'create', '--exclude-nodump', '--atime', 'input') - self.cmd(f'--repo={self.repository_location}::archive2', 'create', '--exclude-nodump', '--atime', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=archive', '--exclude-nodump', '--atime', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=archive2', '--exclude-nodump', '--atime', 'input') if has_lchflags: # remove the file we did not backup, so input and output become equal os.remove(os.path.join('input', 'flagfile')) @@ -2526,7 +2528,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive2', 'input'), ignore_flags=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): + with self.fuse_mount(self.repository_location, mountpoint, '--name=archive'): self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input'), ignore_flags=True, ignore_xattrs=True) # regular file @@ -2604,9 +2606,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('hardlink1', contents=b'123456') os.link('input/hardlink1', 'input/hardlink2') os.link('input/hardlink1', 'input/hardlink3') - self.cmd(f'--repo={self.repository_location}::archive1', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=archive1', 'input') self.create_regular_file('test', contents=b'second') - self.cmd(f'--repo={self.repository_location}::archive2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=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, '-o', 'versions'): @@ -2646,11 +2648,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'check', '--repair', exit_code=0) mountpoint = os.path.join(self.tmpdir, 'mountpoint') - with self.fuse_mount(self.repository_location + '::archive', mountpoint): + with self.fuse_mount(self.repository_location, mountpoint, '--name=archive'): 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, '-o', 'allow_damaged_files'): + with self.fuse_mount(self.repository_location, mountpoint, '--name=archive', '-o', 'allow_damaged_files'): open(os.path.join(mountpoint, path)).close() @unittest.skipUnless(llfuse, 'llfuse not installed') @@ -2784,11 +2786,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.environ['BORG_PASSPHRASE'] = 'passphrase' self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=' + method) verify_uniqueness() - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') verify_uniqueness() - self.cmd(f'--repo={self.repository_location}::test.2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') verify_uniqueness() - self.cmd(f'--repo={self.repository_location}::test.2', 'delete') + self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test.2') verify_uniqueness() def test_aes_counter_uniqueness_keyfile(self): @@ -2800,9 +2802,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_debug_dump_archive_items(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}::test', 'debug', 'dump-archive-items') + output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-archive-items', '--name=test') output_dir = sorted(os.listdir('output')) assert len(output_dir) > 0 and output_dir[0].startswith('000000_') assert 'Done.' in output @@ -2810,7 +2812,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_debug_dump_repo_objs(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir('output'): output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-repo-objs') output_dir = sorted(os.listdir('output')) @@ -2897,7 +2899,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_check_cache(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with self.open_repository() as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) with Cache(repository, key, manifest, sync=False) as cache: @@ -2916,17 +2918,17 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.check_cache() - self.cmd(f'--repo={self.repository_location}::test0', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test0', 'input') self.check_cache() original_archive = self.cmd(f'--repo={self.repository_location}', 'list') - self.cmd(f'--repo={self.repository_location}::test0', 'recreate', 'input/dir2', + self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test0', 'input/dir2', '-e', 'input/dir2/file3', '--target=new-archive') self.check_cache() archives = self.cmd(f'--repo={self.repository_location}', 'list') assert original_archive in archives assert 'new-archive' in archives - listing = self.cmd(f'--repo={self.repository_location}::new-archive', 'list', '--short') + listing = self.cmd(f'--repo={self.repository_location}', 'list', '--name=new-archive', '--short') assert 'file1' not in listing assert 'dir2/file2' in listing assert 'dir2/file3' not in listing @@ -2935,10 +2937,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_test_files() self.create_regular_file('dir2/file3', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test0', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test0', 'recreate', 'input/dir2', '-e', 'input/dir2/file3') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test0', 'input') + self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test0', 'input/dir2', '-e', 'input/dir2/file3') self.check_cache() - listing = self.cmd(f'--repo={self.repository_location}::test0', 'list', '--short') + listing = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test0', '--short') assert 'file1' not in listing assert 'dir2/file2' in listing assert 'dir2/file3' not in listing @@ -2947,17 +2949,17 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_subtree_hardlinks(self): # This is essentially the same problem set as in test_extract_hardlinks self._extract_hardlinks_setup() - self.cmd(f'--repo={self.repository_location}::test2', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test', 'recreate', 'input/dir1') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', 'input') + self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', 'input/dir1') self.check_cache() with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') 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 with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test2', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test2') assert os.stat('input/dir1/hardlink').st_nlink == 4 def test_recreate_rechunkify(self): @@ -2965,9 +2967,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b'a' * 280) fd.write(b'b' * 280) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test1', 'create', '--chunker-params', '7,9,8,128', 'input') - self.cmd(f'--repo={self.repository_location}::test2', 'create', 'input', '--files-cache=disabled') - list = self.cmd(f'--repo={self.repository_location}::test1', 'list', 'input/large_file', + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', '--chunker-params', '7,9,8,128', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', 'input', '--files-cache=disabled') + list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test1', 'input/large_file', '--format', '{num_chunks} {unique_chunks}') num_chunks, unique_chunks = map(int, list.split(' ')) # test1 and test2 do not deduplicate @@ -2975,20 +2977,20 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'recreate', '--chunker-params', 'default') self.check_cache() # test1 and test2 do deduplicate after recreate - assert int(self.cmd(f'--repo={self.repository_location}::test1', 'list', 'input/large_file', '--format={size}')) - assert not int(self.cmd(f'--repo={self.repository_location}::test1', 'list', 'input/large_file', + assert int(self.cmd(f'--repo={self.repository_location}', 'list', '--name=test1', 'input/large_file', '--format={size}')) + assert not int(self.cmd(f'--repo={self.repository_location}', 'list', '--name=test1', 'input/large_file', '--format', '{unique_chunks}')) def test_recreate_recompress(self): self.create_regular_file('compressible', size=10000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input', '-C', 'none') - file_list = self.cmd(f'--repo={self.repository_location}::test', 'list', 'input/compressible', + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input', '-C', 'none') + file_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', 'input/compressible', '--format', '{size} {sha256}') size, sha256_before = file_list.split(' ') self.cmd(f'--repo={self.repository_location}', 'recreate', '-C', 'lz4', '--recompress') self.check_cache() - file_list = self.cmd(f'--repo={self.repository_location}::test', 'list', 'input/compressible', + file_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', 'input/compressible', '--format', '{size} {sha256}') size, sha256_after = file_list.split(' ') assert sha256_before == sha256_after @@ -2997,10 +2999,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): local_timezone = datetime.now(timezone(timedelta(0))).astimezone().tzinfo self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test0', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test0', 'recreate', '--timestamp', "1970-01-02T00:00:00", + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test0', 'input') + self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test0', '--timestamp', "1970-01-02T00:00:00", '--comment', 'test') - info = self.cmd(f'--repo={self.repository_location}::test0', 'info').splitlines() + info = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test0').splitlines() dtime = datetime(1970, 1, 2) + local_timezone.utcoffset(None) s_time = dtime.strftime("%Y-%m-%d") assert any([re.search(r'Time \(start\).+ %s' % s_time, item) for item in info]) @@ -3009,21 +3011,21 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_dry_run(self): self.create_regular_file('compressible', size=10000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - archives_before = self.cmd(f'--repo={self.repository_location}::test', 'list') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + archives_before = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') self.cmd(f'--repo={self.repository_location}', 'recreate', '-n', '-e', 'input/compressible') self.check_cache() - archives_after = self.cmd(f'--repo={self.repository_location}::test', 'list') + archives_after = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') assert archives_after == archives_before def test_recreate_skips_nothing_to_do(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - info_before = self.cmd(f'--repo={self.repository_location}::test', 'info') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + info_before = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') self.cmd(f'--repo={self.repository_location}', 'recreate', '--chunker-params', 'default') self.check_cache() - info_after = self.cmd(f'--repo={self.repository_location}::test', 'info') + info_after = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') assert info_before == info_after # includes archive ID def test_with_lock(self): @@ -3040,31 +3042,31 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file4', size=0) self.create_regular_file('file5', size=0) - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') - output = self.cmd(f'--repo={self.repository_location}::test', 'recreate', '--list', '--info', '-e', 'input/file2') + output = self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--list', '--info', '-e', 'input/file2') self.check_cache() self.assert_in("input/file1", output) self.assert_in("x input/file2", output) - output = self.cmd(f'--repo={self.repository_location}::test', 'recreate', '--list', '-e', 'input/file3') + output = self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--list', '-e', 'input/file3') self.check_cache() self.assert_in("input/file1", output) self.assert_in("x input/file3", output) - output = self.cmd(f'--repo={self.repository_location}::test', 'recreate', '-e', 'input/file4') + output = self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=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(f'--repo={self.repository_location}::test', 'recreate', '--info', '-e', 'input/file5') + output = self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--info', '-e', 'input/file5') self.check_cache() self.assert_not_in("input/file1", output) self.assert_not_in("x input/file5", output) def test_bad_filters(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') self.cmd(f'--repo={self.repository_location}', 'delete', '--first', '1', '--last', '1', fork=True, exit_code=2) def test_key_export_keyfile(self): @@ -3264,7 +3266,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_debug_dump_manifest(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') dump_file = self.output_path + '/dump' output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-manifest', dump_file) assert output == "" @@ -3279,9 +3281,9 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_debug_dump_archive(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') dump_file = self.output_path + '/dump' - output = self.cmd(f'--repo={self.repository_location}::test', 'debug', 'dump-archive', dump_file) + output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-archive', '--name=test', dump_file) assert output == "" with open(dump_file) as f: result = json.load(f) @@ -3295,7 +3297,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 output = self.cmd(f'--repo={self.repository_location}', 'debug', 'refcount-obj', '0' * 64).strip() assert output == 'object 0000000000000000000000000000000000000000000000000000000000000000 not found [info from chunks cache].' - create_json = json.loads(self.cmd(f'--repo={self.repository_location}::test', 'create', '--json', 'input')) + create_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--json', 'input')) archive_id = create_json['archive']['id'] output = self.cmd(f'--repo={self.repository_location}', 'debug', 'refcount-obj', archive_id).strip() assert output == 'object ' + archive_id + ' has 1 referrers [info from chunks cache].' @@ -3361,8 +3363,8 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_test_files() os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'simple.tar', '--progress', '--tar-format=GNU') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'simple.tar', '--progress', '--tar-format=GNU') with changedir('output'): # This probably assumes GNU tar. Note -p switch to extract permissions regardless of umask. subprocess.check_call(['tar', 'xpf', '../simple.tar', '--warning=no-timestamp']) @@ -3376,8 +3378,8 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_test_files() os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - list = self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'simple.tar.gz', + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + list = self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'simple.tar.gz', '--list', '--tar-format=GNU') assert 'input/file1\n' in list assert 'input/dir2\n' in list @@ -3392,8 +3394,8 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_test_files() os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') - list = self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'simple.tar', + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + list = self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'simple.tar', '--strip-components=1', '--list', '--tar-format=GNU') # --list's path are those before processing with --strip-components assert 'input/file1\n' in list @@ -3406,7 +3408,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 @requires_gnutar def test_export_tar_strip_components_links(self): self._extract_hardlinks_setup() - self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'output.tar', + self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'output.tar', '--strip-components=2', '--tar-format=GNU') with changedir('output'): subprocess.check_call(['tar', 'xpf', '../output.tar', '--warning=no-timestamp']) @@ -3419,7 +3421,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 @requires_gnutar def test_extract_hardlinks_tar(self): self._extract_hardlinks_setup() - self.cmd(f'--repo={self.repository_location}::test', 'export-tar', 'output.tar', 'input/dir1', '--tar-format=GNU') + self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'output.tar', 'input/dir1', '--tar-format=GNU') with changedir('output'): subprocess.check_call(['tar', 'xpf', '../output.tar', '--warning=no-timestamp']) assert os.stat('input/dir1/hardlink').st_nlink == 2 @@ -3431,11 +3433,11 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_test_files(create_hardlinks=False) # hardlinks become separate files os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') - self.cmd(f'--repo={self.repository_location}::src', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::src', 'export-tar', 'simple.tar', f'--tar-format={tar_format}') - self.cmd(f'--repo={self.repository_location}::dst', 'import-tar', 'simple.tar') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=src', 'input') + self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=src', 'simple.tar', f'--tar-format={tar_format}') + self.cmd(f'--repo={self.repository_location}', 'import-tar', '--name=dst', 'simple.tar') with changedir(self.output_path): - self.cmd(f'--repo={self.repository_location}::dst', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=dst') self.assert_dirs_equal('input', 'output/input', ignore_ns=True, ignore_xattrs=True) @requires_gzip @@ -3445,21 +3447,21 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_test_files(create_hardlinks=False) # hardlinks become separate files os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') - self.cmd(f'--repo={self.repository_location}::src', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::src', 'export-tar', 'simple.tgz', f'--tar-format={tar_format}') - self.cmd(f'--repo={self.repository_location}::dst', 'import-tar', 'simple.tgz') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=src', 'input') + self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=src', 'simple.tgz', f'--tar-format={tar_format}') + self.cmd(f'--repo={self.repository_location}', 'import-tar', '--name=dst', 'simple.tgz') with changedir(self.output_path): - self.cmd(f'--repo={self.repository_location}::dst', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=dst') self.assert_dirs_equal('input', 'output/input', ignore_ns=True, ignore_xattrs=True) def test_roundtrip_pax_borg(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') - self.cmd(f'--repo={self.repository_location}::src', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::src', 'export-tar', 'simple.tar', '--tar-format=BORG') - self.cmd(f'--repo={self.repository_location}::dst', 'import-tar', 'simple.tar') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=src', 'input') + self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=src', 'simple.tar', '--tar-format=BORG') + self.cmd(f'--repo={self.repository_location}', 'import-tar', '--name=dst', 'simple.tar') with changedir(self.output_path): - self.cmd(f'--repo={self.repository_location}::dst', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=dst') self.assert_dirs_equal('input', 'output/input') # derived from test_extract_xattrs_errors() @@ -3473,10 +3475,10 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_regular_file('file') xattr.setxattr(b'input/file', b'user.attribute%p', b'value') self.cmd('init', self.repository_location, '-e' 'none') - self.cmd('create', self.repository_location + '::test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir('output'): with patch.object(xattr, 'setxattr', patched_setxattr_EACCES): - self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING) + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_WARNING) # derived from test_extract_xattrs_errors() @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='xattr not supported on this system or on this version of' @@ -3489,14 +3491,14 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 os.makedirs(os.path.join(self.input_path, 'dir%p')) xattr.setxattr(b'input/dir%p', b'user.attribute', b'value') self.cmd(f'--repo={self.repository_location}', 'init', '-e' 'none') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') with changedir('output'): with patch.object(xattr, 'setxattr', patched_setxattr_EACCES): - self.cmd(f'--repo={self.repository_location}::test', 'extract', exit_code=EXIT_WARNING) + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_WARNING) def test_do_not_mention_archive_if_you_can_not_find_repo(self): """https://github.com/borgbackup/borg/issues/6014""" - output = self.cmd(f'--repo={self.repository_location}-this-repository-does-not-exist::test', 'info', + output = self.cmd(f'--repo={self.repository_location}-this-repository-does-not-exist', 'info', '--name=test', exit_code=2, fork=True) self.assert_in('this-repository-does-not-exist', output) self.assert_not_in('this-repository-does-not-exist::test', output) @@ -3508,7 +3510,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 both the client and the server forget the nonce""" self.create_regular_file('file1', contents=b'Hello, borg') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') # Oops! We have removed the repo-side memory of the nonce! # See https://github.com/borgbackup/borg/issues/5858 os.remove(os.path.join(self.repository_path, 'nonce')) @@ -3521,13 +3523,13 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 repo_list = self.cmd(f'--repo={self.repository_location}', 'list') assert 'test' in repo_list # The archive should still be readable - archive_info = self.cmd(f'--repo={self.repository_location}::test', 'info') + archive_info = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') assert 'Archive name: test\n' in archive_info - archive_list = self.cmd(f'--repo={self.repository_location}::test', 'list') + archive_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') assert 'file1' in archive_list # Extracting the archive should work with changedir('output'): - self.cmd(f'--repo={self.repository_location}::test', 'extract') + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') self.assert_dirs_equal('input', 'output/input') def test_recovery_from_deleted_repo_nonce(self): @@ -3539,13 +3541,13 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 """ self.create_regular_file('file1', contents=b'Hello, borg') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') # Oops! We have removed the repo-side memory of the nonce! # See https://github.com/borgbackup/borg/issues/5858 nonce = os.path.join(self.repository_path, 'nonce') os.remove(nonce) - self.cmd(f'--repo={self.repository_location}::test2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', 'input') assert os.path.exists(nonce) def test_init_defaults_to_argon2(self): @@ -3696,7 +3698,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): output = self.cmd(f'--repo={self.repository_location}', 'check', '--repair', exit_code=0) self.assert_in('New missing file chunk detected', output) self.cmd(f'--repo={self.repository_location}', 'check', exit_code=0) - output = self.cmd(f'--repo={self.repository_location}::archive1', 'list', '--format={health}#{path}{LF}', exit_code=0) + output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=archive1', '--format={health}#{path}{LF}', 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'): @@ -3727,7 +3729,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): else: self.fail('should not happen') # list is also all-healthy again - output = self.cmd(f'--repo={self.repository_location}::archive1', 'list', '--format={health}#{path}{LF}', exit_code=0) + output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=archive1', '--format={health}#{path}{LF}', exit_code=0) self.assert_not_in('broken#', output) def test_missing_archive_item_chunk(self): @@ -3824,7 +3826,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'check', exit_code=1) self.cmd(f'--repo={self.repository_location}', 'check', '--repair', exit_code=0) self.cmd(f'--repo={self.repository_location}', 'check', exit_code=0) - self.cmd(f'--repo={self.repository_location}::archive1', 'extract', '--dry-run', exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'extract', '--name=archive1', '--dry-run', exit_code=0) def _test_verify_data(self, *init_args): shutil.rmtree(self.repository_path) @@ -3992,19 +3994,19 @@ class RemoteArchiverTestCase(ArchiverTestCase): 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(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') marker = 'cached responses left in RemoteRepository' with changedir('output'): - res = self.cmd(f'--repo={self.repository_location}::test', 'extract', "--debug", '--strip-components', '3') + res = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', "--debug", '--strip-components', '3') assert marker not in res with self.assert_creates_file('file'): - res = self.cmd(f'--repo={self.repository_location}::test', 'extract', "--debug", '--strip-components', '2') + res = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', "--debug", '--strip-components', '2') assert marker not in res with self.assert_creates_file('dir/file'): - res = self.cmd(f'--repo={self.repository_location}::test', 'extract', "--debug", '--strip-components', '1') + res = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', "--debug", '--strip-components', '1') assert marker not in res with self.assert_creates_file('input/dir/file'): - res = self.cmd(f'--repo={self.repository_location}::test', 'extract', "--debug", '--strip-components', '0') + res = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', "--debug", '--strip-components', '0') assert marker not in res @@ -4033,17 +4035,17 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'info') def test_cache_files(self): - self.cmd(f'--repo={self.repository_location}::test', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') self.corrupt(os.path.join(self.cache_path, 'files')) - out = self.cmd(f'--repo={self.repository_location}::test1', 'create', 'input') + out = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', 'input') # borg warns about the corrupt files cache, but then continues without files cache. assert 'files cache is corrupted' in out def test_chunks_archive(self): - self.cmd(f'--repo={self.repository_location}::test1', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', 'input') # Find ID of test1 so we can corrupt it later :) target_id = self.cmd(f'--repo={self.repository_location}', 'list', '--format={id}{LF}').strip() - self.cmd(f'--repo={self.repository_location}::test2', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', 'input') # Force cache sync, creating archive chunks of test1 and test2 in chunks.archive.d self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') @@ -4063,7 +4065,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): config.write(fd) # Cache sync notices corrupted archive chunks, but automatically recovers. - out = self.cmd(f'--repo={self.repository_location}::test3', 'create', '-v', 'input', exit_code=1) + out = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3', '-v', '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 @@ -4109,7 +4111,7 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') # Create the first snapshot - self.cmd(f'--repo={self.repository_location}::test0', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test0', 'input') # Setup files for the second snapshot self.create_regular_file('file_added', size=2048) @@ -4140,8 +4142,8 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): fd.write(b'appended_data') # Create the second snapshot - self.cmd(f'--repo={self.repository_location}::test1a', 'create', 'input') - self.cmd(f'--repo={self.repository_location}::test1b', 'create', '--chunker-params', '16,18,17,4095', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1a', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1b', '--chunker-params', '16,18,17,4095', 'input') def do_asserts(output, can_compare_ids): # File contents changed (deleted and replaced with a new file) @@ -4286,10 +4288,10 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): if are_hardlinks_supported(): assert not any(get_changes('input/hardlink_target_replaced', joutput)) - do_asserts(self.cmd(f'--repo={self.repository_location}::test0', 'diff', 'TODO_test0', 'test1a'), True) + do_asserts(self.cmd(f'--repo={self.repository_location}', 'diff', '--name=test0', '--name2=test1a'), True) # We expect exit_code=1 due to the chunker params warning - do_asserts(self.cmd(f'--repo={self.repository_location}::test0', 'diff', 'TODO_test0', 'test1b', exit_code=1), False) - do_json_asserts(self.cmd(f'--repo={self.repository_location}::test0', 'diff', 'TODO_test0', 'test1a', '--json-lines'), True) + do_asserts(self.cmd(f'--repo={self.repository_location}', 'diff', '--name=test0', '--name2=test1b', exit_code=1), False) + do_json_asserts(self.cmd(f'--repo={self.repository_location}', 'diff', '--name=test0', '--name2=test1a', '--json-lines'), True) def test_sort_option(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -4298,7 +4300,7 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('f_file_removed', size=16) self.create_regular_file('c_file_changed', size=32) self.create_regular_file('e_file_changed', size=64) - self.cmd(f'--repo={self.repository_location}::test0', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test0', 'input') os.unlink('input/a_file_removed') os.unlink('input/f_file_removed') @@ -4308,9 +4310,10 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('e_file_changed', size=1024) self.create_regular_file('b_file_added', size=128) self.create_regular_file('d_file_added', size=256) - self.cmd(f'--repo={self.repository_location}::test1', 'create', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', 'input') - output = self.cmd(f'--repo={self.repository_location}::test0', 'diff', '--sort', 'TODO_test0', 'test1') + output = self.cmd(f'--repo={self.repository_location}', 'diff', '--name=test0', '--name2=test1', + '--sort') expected = [ 'a_file_removed', 'b_file_added', diff --git a/src/borg/testsuite/benchmark.py b/src/borg/testsuite/benchmark.py index f3ec06f2a..4eb3a3da6 100644 --- a/src/borg/testsuite/benchmark.py +++ b/src/borg/testsuite/benchmark.py @@ -28,7 +28,7 @@ def repo_url(request, tmpdir, monkeypatch): @pytest.fixture(params=["none", "repokey"]) def repo(request, cmd, repo_url): - cmd('init', '--encryption', request.param, repo_url) + cmd(f'--repo={repo_url}', 'init', '--encryption', request.param) return repo_url @@ -55,46 +55,52 @@ def testdata(request, tmpdir_factory): @pytest.fixture(params=['none', 'lz4']) -def archive(request, cmd, repo, testdata): - archive_url = repo + '::test' - cmd('create', '--compression', request.param, archive_url, testdata) - return archive_url +def repo_archive(request, cmd, repo, testdata): + archive = 'test' + cmd(f'--repo={repo}', 'create', f'--name={archive}', '--compression', request.param, testdata) + return repo, archive def test_create_none(benchmark, cmd, repo, testdata): - result, out = benchmark.pedantic(cmd, ('create', '--compression', 'none', repo + '::test', testdata)) + result, out = benchmark.pedantic(cmd, (f'--repo={repo}', 'create', '--compression', 'none', + '--name', 'test', testdata)) assert result == 0 def test_create_lz4(benchmark, cmd, repo, testdata): - result, out = benchmark.pedantic(cmd, ('create', '--compression', 'lz4', repo + '::test', testdata)) + result, out = benchmark.pedantic(cmd, (f'--repo={repo}', 'create', '--compression', 'lz4', + '--name', 'test', testdata)) assert result == 0 -def test_extract(benchmark, cmd, archive, tmpdir): +def test_extract(benchmark, cmd, repo_archive, tmpdir): + repo, archive = repo_archive with changedir(str(tmpdir)): - result, out = benchmark.pedantic(cmd, ('extract', archive)) + result, out = benchmark.pedantic(cmd, (f'--repo={repo}', 'extract', '--name', archive)) assert result == 0 -def test_delete(benchmark, cmd, archive): - result, out = benchmark.pedantic(cmd, ('delete', archive)) +def test_delete(benchmark, cmd, repo_archive): + repo, archive = repo_archive + result, out = benchmark.pedantic(cmd, (f'--repo={repo}', 'delete', '--name', archive)) assert result == 0 -def test_list(benchmark, cmd, archive): - result, out = benchmark(cmd, 'list', archive) +def test_list(benchmark, cmd, repo_archive): + repo, archive = repo_archive + result, out = benchmark(cmd, f'--repo={repo}', 'list', '--name', archive) assert result == 0 -def test_info(benchmark, cmd, archive): - result, out = benchmark(cmd, 'info', archive) +def test_info(benchmark, cmd, repo_archive): + repo, archive = repo_archive + result, out = benchmark(cmd, f'--repo={repo}', 'info', '--name', archive) assert result == 0 -def test_check(benchmark, cmd, archive): - repo = archive.split('::')[0] - result, out = benchmark(cmd, 'check', repo) +def test_check(benchmark, cmd, repo_archive): + repo, archive = repo_archive + result, out = benchmark(cmd, f'--repo={repo}', 'check') assert result == 0 From 7dbf125083afc80714255233038210ab3c7ac72b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 16 Jun 2022 00:58:21 +0200 Subject: [PATCH 051/160] Location: remove archive name --- src/borg/archiver.py | 2 +- src/borg/helpers/parseformat.py | 41 +++------- src/borg/testsuite/helpers.py | 129 +++++++++++++------------------- 3 files changed, 60 insertions(+), 112 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 2e112fafe..62636c1a0 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -116,7 +116,7 @@ def argument(args, str_or_bool): def get_repository(location, *, create, exclusive, lock_wait, lock, append_only, make_parent_dirs, storage_quota, args): if location.proto == 'ssh': - repository = RemoteRepository(location.omit_archive(), create=create, exclusive=exclusive, + repository = RemoteRepository(location, create=create, exclusive=exclusive, lock_wait=lock_wait, lock=lock, append_only=append_only, make_parent_dirs=make_parent_dirs, args=args) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index ab420dea7..808466776 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -301,9 +301,7 @@ def parse_stringified_list(s): class Location: - """Object representing a repository / archive location - """ - proto = user = _host = port = path = archive = None + """Object representing a repository location""" # user must not contain "@", ":" or "/". # Quoting adduser error message: @@ -335,15 +333,6 @@ class Location: (?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. - optional_archive_re = r""" - (?: - :: # "::" as separator - (?P[^/]+) # archive name must not contain "/" - )?$""" # must match until the end - # host NAME, or host IP ADDRESS (v4 or v6, v6 must be in square brackets) host_re = r""" (?P( @@ -358,14 +347,13 @@ class Location: (?Pssh):// # ssh:// """ + optional_user_re + host_re + r""" # user@ (optional), host name or address (?::(?P\d+))? # :port (optional) - """ + abs_path_re + optional_archive_re, re.VERBOSE) # path or path::archive + """ + abs_path_re, re.VERBOSE) # path file_re = re.compile(r""" (?Pfile):// # file:// - """ + file_path_re + optional_archive_re, re.VERBOSE) # servername/path, path or path::archive + """ + file_path_re, re.VERBOSE) # servername/path or path - local_re = re.compile( - local_path_re + optional_archive_re, re.VERBOSE) # local path with optional archive + local_re = re.compile(local_path_re, re.VERBOSE) # local path win_file_re = re.compile(r""" (?:file://)? # optional file protocol @@ -373,7 +361,7 @@ class Location: (?:[a-zA-Z]:)? # Drive letter followed by a colon (optional) (?:[^:]+) # Anything which does not contain a :, at least one character ) - """ + optional_archive_re, re.VERBOSE) # archive name (optional, may be empty) + """, re.VERBOSE) def __init__(self, text='', overrides={}, other=False): self.repo_env_var = 'BORG_OTHER_REPO' if other else 'BORG_REPO' @@ -383,7 +371,8 @@ class Location: self._host = None self.port = None self.path = None - self.archive = None + self.raw = None + self.processed = None self.parse(text, overrides) def parse(self, text, overrides={}): @@ -413,10 +402,9 @@ class Location: if m: self.proto = 'file' self.path = m.group('path') - self.archive = m.group('archive') return True - # On windows we currently only support windows paths + # On windows we currently only support windows paths. return False m = self.ssh_re.match(text) @@ -426,19 +414,16 @@ class Location: 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') return True m = self.file_re.match(text) if m: self.proto = m.group('proto') self.path = normpath_special(m.group('path')) - self.archive = m.group('archive') return True m = self.local_re.match(text) if m: - self.path = normpath_special(m.group('path')) - self.archive = m.group('archive') self.proto = 'file' + self.path = normpath_special(m.group('path')) return True return False @@ -449,7 +434,6 @@ class Location: 'host=%r' % self.host, 'port=%r' % self.port, 'path=%r' % self.path, - 'archive=%r' % self.archive, ] return ', '.join(items) @@ -494,13 +478,6 @@ class Location: 'utcnow': DatetimeWrapper(timestamp), }) - def omit_archive(self): - loc = Location(self.raw) - loc.archive = None - loc.raw = loc.raw.split("::")[0] - loc.processed = loc.processed.split("::")[0] - return loc - def location_validator(proto=None, other=False): def validator(text): diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 81469a133..52bbd207b 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -54,65 +54,63 @@ class TestLocationWithoutEnv: 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)" + "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path')" + assert Location('ssh://user@host:1234/some/path').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')" 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')" + "Location(proto='ssh', user='user', host='host', port=None, path='/some/path')" assert repr(Location('ssh://user@[::]:1234/some/path')) == \ - "Location(proto='ssh', user='user', host='::', port=1234, path='/some/path', archive=None)" + "Location(proto='ssh', user='user', host='::', port=1234, path='/some/path')" + assert repr(Location('ssh://user@[::]:1234/some/path')) == \ + "Location(proto='ssh', user='user', host='::', port=1234, path='/some/path')" 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')" + "Location(proto='ssh', user='user', host='::', port=None, path='/some/path')" 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)" + "Location(proto='ssh', user='user', host='2001:db8::', port=1234, path='/some/path')" + assert repr(Location('ssh://user@[2001:db8::]:1234/some/path')) == \ + "Location(proto='ssh', user='user', host='2001:db8::', port=1234, path='/some/path')" 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')) == \ - "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=1234, path='/some/path', archive='archive')" + "Location(proto='ssh', user='user', host='2001:db8::', port=None, path='/some/path')" 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)" + "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=1234, path='/some/path')" + 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')" 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')" + "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=None, path='/some/path')" 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)" + "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=1234, path='/some/path')" + 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')" 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)" + "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path')" 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' assert repr(Location('ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]/some/path')) == \ - "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=None, path='/some/path', archive=None)" + "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=None, path='/some/path')" assert repr(Location('ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]:1234/some/path')) == \ - "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=1234, path='/some/path', archive=None)" + "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=1234, path='/some/path')" 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)" + "Location(proto='file', user=None, host=None, port=None, path='/some/path')" + assert repr(Location('file:///some/path')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/some/path')" assert Location('file:///some/path').to_key_filename() == keys_dir + 'some_path' 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' + assert repr(Location('file:////server/share/path')) == \ + "Location(proto='file', user=None, host=None, port=None, path='//server/share/path')" + assert Location('file:////server/share/path').to_key_filename() == keys_dir + 'server_share_path' 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)" + "Location(proto='file', user=None, host=None, port=None, path='path')" assert Location('path').to_key_filename() == keys_dir + 'path' def test_long_path(self, monkeypatch, keys_dir): @@ -121,88 +119,61 @@ class TestLocationWithoutEnv: 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)" + "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path')" + assert repr(Location('/some/absolute/path')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path')" 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)" + "Location(proto='ssh', user='user', host='host', port=None, path='/some/path')" assert Location('ssh://user@host/some/path').to_key_filename() == keys_dir + 'host__some_path' 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)" + "Location(proto='file', user=None, host=None, port=None, path='some/relative/path')" + assert repr(Location('some/relative/path')) == \ + "Location(proto='file', user=None, host=None, port=None, path='some/relative/path')" 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)" + "Location(proto='ssh', user='user', host='host', port=None, path='/./some/path')" 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)" + "Location(proto='ssh', user='user', host='host', port=None, path='/~/some/path')" 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)" + "Location(proto='ssh', user='user', host='host', port=None, path='/~user/some/path')" assert Location('ssh://user@host/~user/some/path').to_key_filename() == keys_dir + 'host__user_some_path' 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')" - 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:w:cols')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols')" assert repr(Location('/abs/path:with:colons')) == \ - "Location(proto='file', user=None, host=None, port=None, path='/abs/path:with:colons', archive=None)" + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:with:colons')" + assert repr(Location('/abs/path:with:colons')) == \ + "Location(proto='file', user=None, host=None, port=None, path='/abs/path:with:colons')" assert Location('/abs/path:with:colons').to_key_filename() == keys_dir + 'abs_path_with_colons' def test_user_parsing(self): # see issue #1930 - 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_with_timestamp(self): - assert repr(Location('path::archive-{utcnow}').with_timestamp(datetime(2002, 9, 19, tzinfo=timezone.utc))) == \ - "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive-2002-09-19T00:00:00')" - - def test_no_slashes(self, monkeypatch): - monkeypatch.delenv('BORG_REPO', raising=False) - with pytest.raises(ValueError): - Location('/some/path/to/repo::archive_name_with/slashes/is_invalid') + assert repr(Location('ssh://host/path')) == \ + "Location(proto='ssh', user=None, host='host', port=None, path='/path')" def test_canonical_path(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) - locations = ['some/path::archive', 'file://some/path::archive', 'host:some/path::archive', - 'host:~user/some/path::archive', 'ssh://host/some/path::archive', - 'ssh://user@host:1234/some/path::archive'] + locations = ['some/path', 'file://some/path', 'host:some/path', + 'host:~user/some/path', 'ssh://host/some/path', + 'ssh://user@host:1234/some/path'] for location in locations: assert Location(location).canonical_path() == \ Location(Location(location).canonical_path()).canonical_path(), "failed: %s" % location - def test_format_path(self, monkeypatch): - monkeypatch.delenv('BORG_REPO', raising=False) - test_pid = os.getpid() - assert repr(Location('/some/path::archive{pid}')) == \ - f"Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive{test_pid}')" - location_time1 = Location('/some/path::archive{now:%s}') - sleep(1.1) - 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') - def test_omit_archive(self): - from borg.platform import hostname - loc = Location('ssh://user@host:1234/repos/{hostname}::archive') - loc_without_archive = loc.omit_archive() - assert loc_without_archive.archive is None - assert loc_without_archive.raw == "ssh://user@host:1234/repos/{hostname}" - assert loc_without_archive.processed == "ssh://user@host:1234/repos/%s" % hostname - class FormatTimedeltaTestCase(BaseTestCase): From 3e765522de4dccc820e9305758cbee75c664a916 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 16 Jun 2022 14:23:47 +0200 Subject: [PATCH 052/160] help: transfer from other repo --- 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 62636c1a0..415bfc001 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -4177,7 +4177,7 @@ class Archiver: help='do not change repository, just check') subparser.add_argument('--other-repo', metavar='SRC_REPOSITORY', dest='other_location', type=location_validator(other=True), default=Location(other=True), - help='source repository') + help='transfer archives from the other repository') define_archive_filters_group(subparser) # borg diff From 801ce819a317d6432c2c25ea6a7c8c7b63221026 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 16 Jun 2022 14:32:40 +0200 Subject: [PATCH 053/160] help: archive name --- 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 415bfc001..dd834f592 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -3900,7 +3900,7 @@ class Archiver: archive_group = subparser.add_argument_group('Archive options') archive_group.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, default='{hostname}-{now}', - help='specify the name for the archive') + help='specify the archive name') archive_group.add_argument('--comment', dest='comment', metavar='COMMENT', type=CommentSpec, default='', help='add a comment text to the archive') archive_group.add_argument('--timestamp', metavar='TIMESTAMP', dest='timestamp', @@ -5000,10 +5000,10 @@ class Archiver: subparser.set_defaults(func=self.do_rename) subparser.add_argument('--name', metavar='OLDNAME', type=archivename_validator(), - help='the current archive name') + help='specify the archive name') subparser.add_argument('--name2', metavar='NEWNAME', type=archivename_validator(), - help='the new archive name') + help='specify the new archive name') # borg serve serve_epilog = process_epilog(""" From 206245f3cdee48e551e38294a6d1d0dc71308a4b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 16 Jun 2022 15:41:37 +0200 Subject: [PATCH 054/160] --repo: add -r short option --- 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 dd834f592..487b31a16 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -3207,7 +3207,7 @@ class Archiver: 'compatible file can be generated by suffixing FILE with ".pyprof".') add_common_option('--rsh', metavar='RSH', dest='rsh', help="Use this command to connect to the 'borg serve' process (default: 'ssh')") - add_common_option('--repo', metavar='REPO', dest='location', + add_common_option('-r', '--repo', metavar='REPO', dest='location', type=location_validator(other=False), default=Location(other=False), help="repository to use") From c085c2744ba2c2ba9f8c76242090d6bca81ea944 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 20 Jun 2022 15:18:24 +0200 Subject: [PATCH 055/160] borg rename NAME NEWNAME --- src/borg/archiver.py | 6 +++--- src/borg/testsuite/archiver.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 487b31a16..a12034e3c 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1508,7 +1508,7 @@ class Archiver: @with_archive def do_rename(self, args, repository, manifest, key, cache, archive): """Rename an existing archive""" - archive.rename(args.name2) + archive.rename(args.newname) manifest.write() repository.commit(compact=False) cache.commit() @@ -4998,10 +4998,10 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='rename archive') subparser.set_defaults(func=self.do_rename) - subparser.add_argument('--name', metavar='OLDNAME', + subparser.add_argument('name', metavar='OLDNAME', type=archivename_validator(), help='specify the archive name') - subparser.add_argument('--name2', metavar='NEWNAME', + subparser.add_argument('newname', metavar='NEWNAME', type=archivename_validator(), help='specify the new archive name') diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index a22eebe4a..918e8f089 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1508,9 +1508,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.2', '--dry-run') - self.cmd(f'--repo={self.repository_location}', 'rename', '--name=test', '--name2=test.3') + self.cmd(f'--repo={self.repository_location}', 'rename', 'test', 'test.3') self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.2', '--dry-run') - self.cmd(f'--repo={self.repository_location}', 'rename', '--name=test.2', '--name2=test.4') + self.cmd(f'--repo={self.repository_location}', 'rename', 'test.2', 'test.4') self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.3', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.4', '--dry-run') # Make sure both archives have been renamed @@ -1854,7 +1854,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') self.add_unknown_feature(Manifest.Operation.CHECK) - self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'rename', '--name=test', '--name2=other']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'rename', 'test', 'other']) def test_unknown_feature_on_delete(self): print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) From 3fd5b73e1e89f8b1d1a4f22cd910f2f3e9e81ca2 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 20 Jun 2022 15:31:26 +0200 Subject: [PATCH 056/160] borg create NAME ... --- src/borg/archiver.py | 16 +- src/borg/testsuite/archiver.py | 438 ++++++++++++++++---------------- src/borg/testsuite/benchmark.py | 6 +- 3 files changed, 230 insertions(+), 230 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index a12034e3c..1daf2a91c 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -671,20 +671,20 @@ class Archiver: 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([f'--repo={repo}', 'create', '--name=borg-benchmark-crud1', - compression, '--files-cache=disabled', path])) + rc = self.do_create(self.parse_args([f'--repo={repo}', 'create', compression, '--files-cache=disabled', + 'borg-benchmark-crud1', 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([f'--repo={repo}', 'create', '--name=borg-benchmark-crud2', - compression, path])) + rc1 = self.do_create(self.parse_args([f'--repo={repo}', 'create', compression, + 'borg-benchmark-crud2', path])) rc2 = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '--name=borg-benchmark-crud2'])) assert rc1 == rc2 == 0 # measure a no-change update (archive1 is still present) t_start = time.monotonic() - rc1 = self.do_create(self.parse_args([f'--repo={repo}', 'create', '--name=borg-benchmark-crud3', - compression, path])) + rc1 = self.do_create(self.parse_args([f'--repo={repo}', 'create', compression, + 'borg-benchmark-crud3', path])) t_end = time.monotonic() dt_update = t_end - t_start rc2 = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '--name=borg-benchmark-crud3'])) @@ -3899,8 +3899,6 @@ class Archiver: 'regular files. Also follows symlinks pointing to these kinds of files.') archive_group = subparser.add_argument_group('Archive options') - archive_group.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, default='{hostname}-{now}', - help='specify the archive name') archive_group.add_argument('--comment', dest='comment', metavar='COMMENT', type=CommentSpec, default='', help='add a comment text to the archive') archive_group.add_argument('--timestamp', metavar='TIMESTAMP', dest='timestamp', @@ -3919,6 +3917,8 @@ class Archiver: help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') + subparser.add_argument('name', metavar='NAME', type=NameSpec, + help='specify the archive name') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to archive') diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 918e8f089..48f682f23 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -149,14 +149,14 @@ def test_return_codes(cmd, tmpdir): input.join('test_file').write('content') rc, out = cmd('--repo=%s' % str(repo), 'init', '--encryption=none') assert rc == EXIT_SUCCESS - rc, out = cmd('--repo=%s' % repo, 'create', '--name=archive', str(input)) + rc, out = cmd('--repo=%s' % repo, 'create', 'archive', str(input)) assert rc == EXIT_SUCCESS with changedir(str(output)): rc, out = cmd('--repo=%s' % repo, 'extract', '--name=archive') assert rc == EXIT_SUCCESS rc, out = cmd('--repo=%s' % repo, 'extract', '--name=archive', 'does/not/match') assert rc == EXIT_WARNING # pattern did not match - rc, out = cmd('--repo=%s' % repo, 'create', '--name=archive', str(input)) + rc, out = cmd('--repo=%s' % repo, 'create', 'archive', str(input)) assert rc == EXIT_ERROR # duplicate archive name @@ -219,7 +219,7 @@ def test_disk_full(cmd): break raise try: - rc, out = cmd('--repo=%s' % repo, 'create', '--name=test%03d' % i, input) + rc, out = cmd('--repo=%s' % repo, 'create', 'test%03d' % i, input) success = rc == EXIT_SUCCESS if not success: print('create', rc, out) @@ -299,7 +299,7 @@ class ArchiverTestCaseBase(BaseTestCase): return output def create_src_archive(self, name): - self.cmd(f'--repo={self.repository_location}', 'create', f'--name={name}', '--compression=lz4', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--compression=lz4', name, src_dir) def open_archive(self, name): repository = Repository(self.repository_path, exclusive=True) @@ -394,8 +394,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): output = self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey', '--show-version', '--show-rc', fork=True) self.assert_in('borgbackup version', output) self.assert_in('terminating with success status, rc 0', output) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude-nodump', 'input') - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', '--exclude-nodump', '--stats', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--exclude-nodump', 'test', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--exclude-nodump', '--stats', 'test.2', 'input') self.assert_in('Archive name: test.2', output) self.assert_in('This archive: ', output) with changedir('output'): @@ -463,7 +463,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file(hl_a, contents=b'123456') os.link(hl_a, hl_b) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input', 'input') # give input twice! + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', 'input') # give input twice! # test if created archive has 'input' contents twice: archive_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines') paths = [json.loads(line)['path'] for line in archive_list.split('\n') if line] @@ -491,7 +491,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): pytest.skip('unix sockets disabled or not supported') elif err.errno == errno.EACCES: pytest.skip('permission denied to create unix sockets') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') sock.close() with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') @@ -501,7 +501,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_symlink_extract(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') assert os.readlink('input/link1') == 'somewhere' @@ -514,7 +514,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.symlink('target', 'symlink1') os.link('symlink1', 'symlink2', follow_symlinks=False) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') print(output) @@ -548,7 +548,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): have_noatime = has_noatime('input/file1') os.utime('input/file1', (atime, mtime)) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--atime', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--atime', 'test', 'input') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') sti = os.stat('input/file1') @@ -568,7 +568,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.utime('input/file1', (atime, birthtime)) os.utime('input/file1', (atime, mtime)) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') sti = os.stat('input/file1') @@ -584,7 +584,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.utime('input/file1', (atime, birthtime)) os.utime('input/file1', (atime, mtime)) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--nobirthtime', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', '--nobirthtime') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') sti = os.stat('input/file1') @@ -645,7 +645,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # we could create a sparse input file, so creating a backup of it and # extracting it again (as sparse) should also work: self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir(self.output_path): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--sparse') self.assert_dirs_equal('input', 'output/input') @@ -664,7 +664,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with open(filename, 'wb'): pass self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') for filename in filenames: with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', os.path.join('input', filename)) @@ -675,70 +675,70 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.environ['BORG_PASSPHRASE'] = 'passphrase' self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') repository_id = self._extract_repository_id(self.repository_path) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') shutil.rmtree(self.repository_path) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self._set_repository_id(self.repository_path, repository_id) self.assert_equal(repository_id, self._extract_repository_id(self.repository_path)) if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.EncryptionMethodMismatch): - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') def test_repository_swap_detection2(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}_unencrypted', 'init', '--encryption=none') os.environ['BORG_PASSPHRASE'] = 'passphrase' self.cmd(f'--repo={self.repository_location}_encrypted', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}_encrypted', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}_encrypted', 'create', 'test', 'input') shutil.rmtree(self.repository_path + '_encrypted') os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted') if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}_encrypted', 'create', '--name=test.2', 'input', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}_encrypted', 'create', 'test.2', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.RepositoryAccessAborted): - self.cmd(f'--repo={self.repository_location}_encrypted', 'create', '--name=test.2', 'input') + self.cmd(f'--repo={self.repository_location}_encrypted', 'create', 'test.2', 'input') def test_repository_swap_detection_no_cache(self): self.create_test_files() os.environ['BORG_PASSPHRASE'] = 'passphrase' self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') repository_id = self._extract_repository_id(self.repository_path) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') shutil.rmtree(self.repository_path) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self._set_repository_id(self.repository_path, repository_id) self.assert_equal(repository_id, self._extract_repository_id(self.repository_path)) self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.EncryptionMethodMismatch): - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') def test_repository_swap_detection2_no_cache(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}_unencrypted', 'init', '--encryption=none') os.environ['BORG_PASSPHRASE'] = 'passphrase' self.cmd(f'--repo={self.repository_location}_encrypted', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}_encrypted', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}_encrypted', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}_unencrypted', 'delete', '--cache-only') self.cmd(f'--repo={self.repository_location}_encrypted', 'delete', '--cache-only') shutil.rmtree(self.repository_path + '_encrypted') os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted') if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}_encrypted', 'create', '--name=test.2', 'input', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}_encrypted', 'create', 'test.2', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.RepositoryAccessAborted): - self.cmd(f'--repo={self.repository_location}_encrypted', 'create', '--name=test.2', 'input') + self.cmd(f'--repo={self.repository_location}_encrypted', 'create', '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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '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=''): @@ -751,10 +751,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): # is set, while it isn't. Previously this raised no warning, # since the repository is, technically, encrypted. if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.CacheInitAbortedError): - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') def test_repository_move(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -805,7 +805,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_strip_components(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('dir/file') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--strip-components', '3') assert not os.path.exists('file') @@ -833,7 +833,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.path.join(self.input_path, 'dir1/aaaa')) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') @requires_hardlinks @unittest.skipUnless(llfuse, 'llfuse not installed') @@ -910,7 +910,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file(hl_a, contents=b'123456') os.link(hl_a, hl_b) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input', 'input') # give input twice! + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', 'input') # give input twice! # now test extraction with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') @@ -927,7 +927,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('file3', size=1024 * 80) self.create_regular_file('file4', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude=input/file4', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--exclude=input/file4', 'test', 'input') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', 'input/file1', ) self.assert_equal(sorted(os.listdir('output/input')), ['file1']) @@ -947,7 +947,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file333', size=1024 * 80) # Create with regular expression exclusion for file4 - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude=re:input/file4$', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--exclude=re:input/file4$', 'test', 'input') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333']) @@ -985,7 +985,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b're:input/file4$\n') fd.write(b'fm:*aa:*thing\n') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude-from=' + self.exclude_file_path, 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--exclude-from=' + self.exclude_file_path, 'test', 'input') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333']) @@ -1019,7 +1019,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file("file4", size=1024 * 80) self.create_regular_file("file333", size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', "create", "--name=test", "input") + self.cmd(f'--repo={self.repository_location}', 'create', "test", "input") # Extract everything with regular expression with changedir("output"): @@ -1048,7 +1048,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') @@ -1072,7 +1072,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_extract_progress(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--progress') @@ -1095,7 +1095,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_stdin(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') input_data = b'\x00foo\n\nbar\n \n' - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '-', input=input_data) + self.cmd(f'--repo={self.repository_location}', 'create', 'test', '-', input=input_data) item = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines')) assert item['uid'] == 0 assert item['gid'] == 0 @@ -1108,8 +1108,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') input_data = 'some test content' name = 'a/b/c' - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--stdin-name', name, '--content-from-command', - '--', 'echo', input_data) + self.cmd(f'--repo={self.repository_location}', 'create', '--stdin-name', name, '--content-from-command', + 'test', '--', 'echo', input_data) item = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines')) assert item['uid'] == 0 assert item['gid'] == 0 @@ -1120,7 +1120,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_content_from_command_with_failed_command(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--content-from-command', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--content-from-command', 'test', '--', 'sh', '-c', 'exit 73;', exit_code=2) assert output.endswith("Command 'sh' exited with status 73\n") archive_list = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--json')) @@ -1128,7 +1128,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_content_from_command_missing_command(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--content-from-command', exit_code=2) + output = self.cmd(f'--repo={self.repository_location}', 'create', 'test', '--content-from-command', exit_code=2) assert output.endswith('No command given.\n') def test_create_paths_from_stdin(self): @@ -1139,7 +1139,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file("file4", size=1024 * 80) input_data = b'input/file1\0input/dir1\0input/file4' - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', + self.cmd(f'--repo={self.repository_location}', 'create', 'test', '--paths-from-stdin', '--paths-delimiter', '\\0', input=input_data) archive_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines') paths = [json.loads(line)['path'] for line in archive_list.split('\n') if line] @@ -1153,7 +1153,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file("file4", size=1024 * 80) input_data = 'input/file1\ninput/file2\ninput/file3' - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--paths-from-command', + self.cmd(f'--repo={self.repository_location}', 'create', '--paths-from-command', 'test', '--', 'echo', input_data) archive_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines') paths = [json.loads(line)['path'] for line in archive_list.split('\n') if line] @@ -1161,7 +1161,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_paths_from_command_with_failed_command(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--paths-from-command', + output = self.cmd(f'--repo={self.repository_location}', 'create', '--paths-from-command', 'test', '--', 'sh', '-c', 'exit 73;', exit_code=2) assert output.endswith("Command 'sh' exited with status 73\n") archive_list = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--json')) @@ -1169,20 +1169,20 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_paths_from_command_missing_command(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--paths-from-command', exit_code=2) + output = self.cmd(f'--repo={self.repository_location}', 'create', 'test', '--paths-from-command', exit_code=2) assert output.endswith('No command given.\n') def test_create_without_root(self): """test create without a root""" self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'create', 'test', exit_code=2) def test_create_pattern_root(self): """test create with only a root pattern""" self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '-v', '--list', '--pattern=R input') + output = self.cmd(f'--repo={self.repository_location}', 'create', 'test', '-v', '--list', '--pattern=R input') self.assert_in("A input/file1", output) self.assert_in("A input/file2", output) @@ -1192,9 +1192,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}', 'create', '--name=test', '-v', '--list', + output = self.cmd(f'--repo={self.repository_location}', 'create', '-v', '--list', '--pattern=+input/file_important', '--pattern=-input/file*', - 'input') + 'test', 'input') self.assert_in("A input/file_important", output) self.assert_in('x input/file1', output) self.assert_in('x input/file2', output) @@ -1206,9 +1206,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}', 'create', '--name=test', '-v', '--list', + output = self.cmd(f'--repo={self.repository_location}', 'create', '-v', '--list', '--pattern=-input/otherfile', '--patterns-from=' + self.patterns_file_path, - 'input') + 'test', 'input') self.assert_in("A input/file_important", output) self.assert_in('x input/file1', output) self.assert_in('x input/file2', output) @@ -1224,9 +1224,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}', 'create', '--name=test', '-v', '--list', + output = self.cmd(f'--repo={self.repository_location}', 'create', '-v', '--list', '--patterns-from=' + self.patterns_file_path2, - 'input') + '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) @@ -1241,9 +1241,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}', 'create', '--name=test', '-v', '--list', + output = self.cmd(f'--repo={self.repository_location}', 'create', '-v', '--list', '--patterns-from=' + self.patterns_file_path2, - 'input') + '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) @@ -1259,8 +1259,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}', 'create', '--name=test', - '--patterns-from=' + self.patterns_file_path2, '.') + self.cmd(f'--repo={self.repository_location}', 'create', '--patterns-from=' + self.patterns_file_path2, + 'test', '.') # list the archive and verify that the "intermediate" folders appear before # their contents @@ -1277,15 +1277,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') - create_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', - '--no-cache-sync', 'input', - '--json', '--error')) # ignore experimental warning + create_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'create', + '--no-cache-sync', '--json', '--error', + 'test', 'input')) # ignore experimental warning info_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--name=test', '--json')) create_stats = create_json['cache']['stats'] info_stats = info_json['cache']['stats'] assert create_stats == info_stats self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', '--no-cache-sync', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--no-cache-sync', 'test2', 'input') self.cmd(f'--repo={self.repository_location}', 'info') self.cmd(f'--repo={self.repository_location}', 'check') @@ -1294,7 +1294,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--pattern=+input/file_important', '--pattern=-input/file*') @@ -1308,12 +1308,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_exclude_caches(self): self._create_test_caches() - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude-caches', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', '--exclude-caches') self._assert_test_caches() def test_recreate_exclude_caches(self): self._create_test_caches() - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--exclude-caches') self._assert_test_caches() @@ -1331,13 +1331,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_exclude_tagged(self): self._create_test_tagged() - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude-if-present', '.NOBACKUP', - '--exclude-if-present', '00-NOBACKUP', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', + '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP') self._assert_test_tagged() def test_recreate_exclude_tagged(self): self._create_test_tagged() - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP') self._assert_test_tagged() @@ -1370,13 +1370,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_exclude_keep_tagged(self): self._create_test_keep_tagged() - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--exclude-if-present', '.NOBACKUP1', - '--exclude-if-present', '.NOBACKUP2', '--exclude-caches', '--keep-exclude-tags', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', '--exclude-if-present', '.NOBACKUP1', + '--exclude-if-present', '.NOBACKUP2', '--exclude-caches', '--keep-exclude-tags') self._assert_test_keep_tagged() def test_recreate_exclude_keep_tagged(self): self._create_test_keep_tagged() - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2', '--exclude-caches', '--keep-exclude-tags') self._assert_test_keep_tagged() @@ -1388,7 +1388,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.mkdir(os.path.join(self.input_path, 'subdir')) # to make sure the tag is encountered *after* file1 os.link(os.path.join(self.input_path, 'file1'), os.path.join(self.input_path, 'subdir', CACHE_TAG_NAME)) # correct tag name, hardlink to file1 - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') # in the "test" archive, we now have, in this order: # - a regular file item for "file1" # - a hardlink item for "CACHEDIR.TAG" referring back to file1 for its contents @@ -1410,7 +1410,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file') xattr.setxattr(b'input/file', b'security.capability', capabilities) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): with patch.object(os, 'fchown', patched_fchown): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') @@ -1431,7 +1431,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file') xattr.setxattr(b'input/file', b'user.attribute', b'value') self.cmd(f'--repo={self.repository_location}', 'init', '-e' 'none') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): input_abspath = os.path.abspath('input/file') with patch.object(xattr, 'setxattr', patched_setxattr_E2BIG): @@ -1451,7 +1451,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_regular_file('dir1/dir2/file', size=1024 * 80) with changedir('input/dir1/dir2'): - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', + self.cmd(f'--repo={self.repository_location}', 'create', 'test', '../../../input/dir1/../dir1/dir2/..') output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') self.assert_not_in('..', output) @@ -1462,16 +1462,16 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) with changedir('input'): - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', '--exclude=file1', '.') + self.cmd(f'--repo={self.repository_location}', 'create', 'test1', '.', '--exclude=file1') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test1') self.assert_equal(sorted(os.listdir('output')), ['file2']) with changedir('input'): - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', '--exclude=./file1', '.') + self.cmd(f'--repo={self.repository_location}', 'create', 'test2', '.', '--exclude=./file1') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test2') self.assert_equal(sorted(os.listdir('output')), ['file2']) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3', '--exclude=input/./file1', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test3', 'input', '--exclude=input/./file1') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test3') self.assert_equal(sorted(os.listdir('output/input')), ['file2']) @@ -1479,13 +1479,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_repeated_files(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') # Overwriting regular files and directories should be supported os.mkdir('output/input') os.mkdir('output/input/file1') @@ -1504,8 +1504,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('dir2/file2', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.2', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'rename', 'test', 'test.3') @@ -1523,7 +1523,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_info(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') info_repo = self.cmd(f'--repo={self.repository_location}', 'info') assert 'All archives:' in info_repo info_archive = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') @@ -1534,7 +1534,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_info_json(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') info_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--json')) repository = info_repo['repository'] assert len(repository['id']) == 64 @@ -1572,10 +1572,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_comment(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', '--comment', 'this is the comment', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3', '--comment', '"deleted" comment', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test4', '--comment', 'preserved comment', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input', '--comment', 'this is the comment') + self.cmd(f'--repo={self.repository_location}', 'create', 'test3', 'input', '--comment', '"deleted" comment') + self.cmd(f'--repo={self.repository_location}', 'create', 'test4', 'input', '--comment', 'preserved comment') assert 'Comment: \n' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test1') assert 'Comment: this is the comment' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test2') @@ -1592,11 +1592,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('dir2/file2', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.3', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=another_test.1', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=another_test.2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test.3', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'another_test.1', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'another_test.2', 'input') self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.2', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'delete', '--prefix', 'another_') @@ -1612,9 +1612,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_delete_multiple(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test3', 'input') self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test1', 'test2') self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test3', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test3') @@ -1624,8 +1624,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('dir2/file2', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'no' self.cmd(f'--repo={self.repository_location}', 'delete', exit_code=2) assert os.path.exists(self.repository_path) @@ -1797,13 +1797,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_umask(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--dry-run', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--dry-run', 'test', 'input') # Make sure no archive has been created with Repository(self.repository_path) as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -1827,13 +1827,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_unknown_feature_on_create(self): print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) self.add_unknown_feature(Manifest.Operation.WRITE) - self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'create', '--name=test', 'input']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'create', 'test', 'input']) def test_unknown_feature_on_cache_sync(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') self.add_unknown_feature(Manifest.Operation.READ) - self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'create', '--name=test', 'input']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'create', 'test', 'input']) def test_unknown_feature_on_change_passphrase(self): print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) @@ -1842,7 +1842,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_unknown_feature_on_read(self): print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.add_unknown_feature(Manifest.Operation.READ) with changedir('output'): self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'extract', '--name=test']) @@ -1852,13 +1852,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_unknown_feature_on_rename(self): print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.add_unknown_feature(Manifest.Operation.CHECK) self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'rename', 'test', 'other']) def test_unknown_feature_on_delete(self): print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.add_unknown_feature(Manifest.Operation.DELETE) # delete of an archive raises self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'delete', '--name=test']) @@ -1869,7 +1869,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(llfuse, 'llfuse not installed') def test_unknown_feature_on_mount(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.add_unknown_feature(Manifest.Operation.READ) mountpoint = os.path.join(self.tmpdir, 'mountpoint') os.mkdir(mountpoint) @@ -1895,7 +1895,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): cache.commit() if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') else: called = False wipe_cache_safe = LocalCache.wipe_cache @@ -1906,7 +1906,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): wipe_cache_safe(*args) with patch.object(LocalCache, 'wipe_cache', wipe_wrapper): - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') assert called @@ -1920,13 +1920,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_progress_on(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test4', '--progress', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', 'test4', 'input', '--progress') self.assert_in("\r", output) def test_progress_off(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test5', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', 'test5', 'input') self.assert_not_in("\r", output) def test_file_status(self): @@ -1937,11 +1937,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--list', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--list', 'test', 'input') self.assert_in("A input/file1", output) self.assert_in("A input/file2", output) # should find first file as unmodified - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', '--list', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--list', 'test2', 'input') self.assert_in("U input/file1", output) # this is expected, although surprising, for why, see: # https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file @@ -1953,15 +1953,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=10) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', - '--list', '--files-cache=ctime,size', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input', + '--list', '--files-cache=ctime,size') # modify file1, but cheat with the mtime (and atime) and also keep same size: st = os.stat('input/file1') self.create_regular_file('file1', contents=b'321') os.utime('input/file1', ns=(st.st_atime_ns, st.st_mtime_ns)) # this mode uses ctime for change detection, so it should find file1 as modified - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', - '--list', '--files-cache=ctime,size', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input', + '--list', '--files-cache=ctime,size') self.assert_in("M input/file1", output) def test_file_status_ms_cache_mode(self): @@ -1970,14 +1970,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=10) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', - '--list', '--files-cache=mtime,size', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', + '--list', '--files-cache=mtime,size', 'test1', 'input') # change mode of file1, no content change: st = os.stat('input/file1') os.chmod('input/file1', st.st_mode ^ stat.S_IRWXO) # this triggers a ctime change, but mtime is unchanged # this mode uses mtime for change detection, so it should find file1 as unmodified - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', - '--list', '--files-cache=mtime,size', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', + '--list', '--files-cache=mtime,size', 'test2', 'input') self.assert_in("U input/file1", output) def test_file_status_rc_cache_mode(self): @@ -1986,11 +1986,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=10) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', - '--list', '--files-cache=rechunk,ctime', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', + '--list', '--files-cache=rechunk,ctime', 'test1', 'input') # no changes here, but this mode rechunks unconditionally - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', - '--list', '--files-cache=rechunk,ctime', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', + '--list', '--files-cache=rechunk,ctime', 'test2', 'input') self.assert_in("A input/file1", output) def test_file_status_excluded(self): @@ -2003,14 +2003,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file3', size=1024 * 80) platform.set_flags(os.path.join(self.input_path, 'file3'), stat.UF_NODUMP) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--list', '--exclude-nodump', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', '--list', '--exclude-nodump', 'test', 'input') self.assert_in("A input/file1", output) self.assert_in("A input/file2", output) if has_lchflags: self.assert_in("x input/file3", output) # should find second file as excluded - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', - '--list', '--exclude-nodump', 'input', '--exclude', '*/file2') + output = self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input', + '--list', '--exclude-nodump', '--exclude', '*/file2') self.assert_in("U input/file1", output) self.assert_in("x input/file2", output) if has_lchflags: @@ -2019,8 +2019,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_json(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - create_info = json.loads(self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', - '--json', 'input')) + create_info = json.loads(self.cmd(f'--repo={self.repository_location}', 'create', '--json', + 'test', 'input')) # The usual keys assert 'encryption' in create_info assert 'repository' in create_info @@ -2040,21 +2040,21 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file2', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') # no listing by default - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.assert_not_in('file1', output) # shouldn't be listed even if unchanged - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test0', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') self.assert_not_in('file1', output) # should list the file as unchanged - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', '--list', '--filter=U', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input', '--list', '--filter=U') self.assert_in('file1', output) # should *not* list the file as changed - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', '--list', '--filter=AM', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input', '--list', '--filter=AM') self.assert_not_in('file1', output) # change the file self.create_regular_file('file1', size=1024 * 100) # should list the file as changed - output = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3', '--list', '--filter=AM', 'input') + output = self.cmd(f'--repo={self.repository_location}', 'create', 'test3', 'input', '--list', '--filter=AM') self.assert_in('file1', output) @pytest.mark.skipif(not are_fifos_supported(), reason='FIFOs not supported') @@ -2079,7 +2079,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): t = Thread(target=fifo_feeder, args=(fifo_fn, data)) t.start() try: - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--read-special', 'input/link_fifo') + self.cmd(f'--repo={self.repository_location}', 'create', '--read-special', 'test', 'input/link_fifo') finally: t.join() with changedir('output'): @@ -2092,25 +2092,25 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_read_special_broken_symlink(self): os.symlink('somewhere does not exist', os.path.join(self.input_path, 'link')) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--read-special', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--read-special', 'test', 'input') output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') assert 'input/link -> somewhere does not exist' in output # def test_cmdline_compatibility(self): # self.create_regular_file('file1', size=1024 * 80) # self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - # self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + # self.cmd(f'--repo={self.repository_location}', 'create', '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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test2', src_dir) # these are not really a checkpoints, but they look like some: - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3.checkpoint', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3.checkpoint.1', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test4.checkpoint', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test3.checkpoint', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test3.checkpoint.1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test4.checkpoint', src_dir) output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1') assert re.search(r'Would prune:\s+test1', output) # must keep the latest non-checkpoint archive: @@ -2133,7 +2133,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_not_in('test3.checkpoint.1', output) self.assert_in('test4.checkpoint', output) # now we supersede the latest checkpoint by a successful backup: - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test5', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test5', src_dir) self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=2') output = self.cmd(f'--repo={self.repository_location}', 'list', '--consider-checkpoints') # all checkpoints should be gone now: @@ -2148,8 +2148,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): return dtime.astimezone(dateutil.tz.UTC).strftime("%Y-%m-%dT%H:%M:%S") def _create_archive_ts(self, name, y, m, d, H=0, M=0, S=0): - self.cmd(f'--repo={self.repository_location}', 'create', f'--name={name}', - '--timestamp', self._to_utc_timestamp(y, m, d, H, M, S), src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', + '--timestamp', self._to_utc_timestamp(y, m, d, H, M, S), name, src_dir) # This test must match docs/misc/prune-example.txt def test_prune_repository_example(self): @@ -2238,8 +2238,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_prune_repository_save_space(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test2', src_dir) output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1') assert re.search(r'Keeping archive \(rule: daily #1\):\s+test2', output) assert re.search(r'Would prune:\s+test1', output) @@ -2253,10 +2253,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_prune_repository_prefix(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=foo-2015-08-12-10:00', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=foo-2015-08-12-20:00', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=bar-2015-08-12-10:00', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=bar-2015-08-12-20:00', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'foo-2015-08-12-10:00', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'foo-2015-08-12-20:00', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'bar-2015-08-12-10:00', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'bar-2015-08-12-20:00', src_dir) output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1', '--prefix=foo-') assert re.search(r'Keeping archive \(rule: daily #1\):\s+foo-2015-08-12-20:00', output) assert re.search(r'Would prune:\s+foo-2015-08-12-10:00', output) @@ -2274,10 +2274,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_prune_repository_glob(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=2015-08-12-10:00-foo', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=2015-08-12-20:00-foo', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=2015-08-12-10:00-bar', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=2015-08-12-20:00-bar', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '2015-08-12-10:00-foo', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '2015-08-12-20:00-foo', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '2015-08-12-10:00-bar', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '2015-08-12-20:00-bar', src_dir) output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1', '--glob-archives=2015-*-foo') assert re.search(r'Keeping archive \(rule: daily #1\):\s+2015-08-12-20:00-foo', output) assert re.search(r'Would prune:\s+2015-08-12-10:00-foo', output) @@ -2295,9 +2295,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_list_prefix(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test-1', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=something-else-than-test-1', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test-2', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test-1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'something-else-than-test-1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test-2', src_dir) output = self.cmd(f'--repo={self.repository_location}', 'list', '--prefix=test-') self.assert_in('test-1', output) self.assert_in('test-2', output) @@ -2305,7 +2305,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_list_format(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test', src_dir) output_1 = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') output_2 = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{mode} {user:6} {group:6} {size:8d} {mtime} {path}{extra}{NEWLINE}') output_3 = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{mtime:%s} {path}{NL}') @@ -2314,8 +2314,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_list_repository_format(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test-1', '--comment', 'comment 1', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test-2', '--comment', 'comment 2', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--comment', 'comment 1', 'test-1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', '--comment', 'comment 2', 'test-2', src_dir) output_1 = self.cmd(f'--repo={self.repository_location}', 'list') output_2 = self.cmd(f'--repo={self.repository_location}', 'list', '--format', '{archive:<36} {time} [{id}]{NL}') self.assertEqual(output_1, output_2) @@ -2331,17 +2331,17 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('empty_file', size=0) self.create_regular_file('amb', contents=b'a' * 1000000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{sha256} {path}{NL}') assert "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0 input/amb" in output assert "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 input/empty_file" in output def test_list_consider_checkpoints(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test1', src_dir) # these are not really a checkpoints, but they look like some: - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2.checkpoint', src_dir) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3.checkpoint.1', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test2.checkpoint', src_dir) + self.cmd(f'--repo={self.repository_location}', 'create', 'test3.checkpoint.1', src_dir) output = self.cmd(f'--repo={self.repository_location}', 'list') assert "test1" in output assert "test2.checkpoint" not in output @@ -2358,7 +2358,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b'abba' * 2000000) fd.write(b'baab' * 2000000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{num_chunks} {unique_chunks} {path}{NL}') assert "0 0 input/empty_file" in output assert "2 2 input/two_chunks" in output @@ -2366,7 +2366,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_list_size(self): self.create_regular_file('compressible_file', size=10000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '-C', 'lz4', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '-C', 'lz4', 'test', 'input') output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{size} {path}{NL}') size, path = output.split("\n")[1].split(" ") assert int(size) == 10000 @@ -2374,7 +2374,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_list_json(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') list_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--json')) repository = list_repo['repository'] assert len(repository['id']) == 64 @@ -2407,7 +2407,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_log_json(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - log = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--log-json', 'input', '--list', '--debug') + log = self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', '--log-json', '--list', '--debug') messages = {} # type -> message, one of each kind for line in log.splitlines(): msg = json.loads(line) @@ -2425,13 +2425,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_debug_profile(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input', '--debug-profile=create.prof') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', '--debug-profile=create.prof') self.cmd('debug', 'convert-profile', 'create.prof', 'create.pyprof') stats = pstats.Stats('create.pyprof') stats.strip_dirs() stats.sort_stats('cumtime') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', 'input', '--debug-profile=create.pyprof') + self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input', '--debug-profile=create.pyprof') stats = pstats.Stats('create.pyprof') # Only do this on trusted data! stats.strip_dirs() stats.sort_stats('cumtime') @@ -2439,7 +2439,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_common_options(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - log = self.cmd(f'--repo={self.repository_location}', '--debug', 'create', '--name=test', 'input') + log = self.cmd(f'--repo={self.repository_location}', '--debug', 'create', 'test', 'input') assert 'security: read previous location' in log def test_change_passphrase(self): @@ -2513,8 +2513,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_test_files() have_noatime = has_noatime('input/file1') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=archive', '--exclude-nodump', '--atime', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=archive2', '--exclude-nodump', '--atime', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--exclude-nodump', '--atime', 'archive', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '--exclude-nodump', '--atime', 'archive2', 'input') if has_lchflags: # remove the file we did not backup, so input and output become equal os.remove(os.path.join('input', 'flagfile')) @@ -2606,9 +2606,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('hardlink1', contents=b'123456') os.link('input/hardlink1', 'input/hardlink2') os.link('input/hardlink1', 'input/hardlink3') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=archive1', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'archive1', 'input') self.create_regular_file('test', contents=b'second') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=archive2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', '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, '-o', 'versions'): @@ -2786,9 +2786,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.environ['BORG_PASSPHRASE'] = 'passphrase' self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=' + method) verify_uniqueness() - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') verify_uniqueness() - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test.2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') verify_uniqueness() self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test.2') verify_uniqueness() @@ -2802,7 +2802,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_debug_dump_archive_items(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-archive-items', '--name=test') output_dir = sorted(os.listdir('output')) @@ -2812,7 +2812,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_debug_dump_repo_objs(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-repo-objs') output_dir = sorted(os.listdir('output')) @@ -2899,7 +2899,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_check_cache(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with self.open_repository() as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) with Cache(repository, key, manifest, sync=False) as cache: @@ -2918,7 +2918,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.check_cache() - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test0', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') self.check_cache() original_archive = self.cmd(f'--repo={self.repository_location}', 'list') self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test0', 'input/dir2', @@ -2937,7 +2937,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_test_files() self.create_regular_file('dir2/file3', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test0', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test0', 'input/dir2', '-e', 'input/dir2/file3') self.check_cache() listing = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test0', '--short') @@ -2949,7 +2949,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_subtree_hardlinks(self): # This is essentially the same problem set as in test_extract_hardlinks self._extract_hardlinks_setup() - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input') self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', 'input/dir1') self.check_cache() with changedir('output'): @@ -2967,8 +2967,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b'a' * 280) fd.write(b'b' * 280) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', '--chunker-params', '7,9,8,128', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', 'input', '--files-cache=disabled') + self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input', '--chunker-params', '7,9,8,128') + self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input', '--files-cache=disabled') list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test1', 'input/large_file', '--format', '{num_chunks} {unique_chunks}') num_chunks, unique_chunks = map(int, list.split(' ')) @@ -2984,7 +2984,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_recompress(self): self.create_regular_file('compressible', size=10000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input', '-C', 'none') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', '-C', 'none') file_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', 'input/compressible', '--format', '{size} {sha256}') size, sha256_before = file_list.split(' ') @@ -2999,7 +2999,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): local_timezone = datetime.now(timezone(timedelta(0))).astimezone().tzinfo self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test0', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test0', '--timestamp', "1970-01-02T00:00:00", '--comment', 'test') info = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test0').splitlines() @@ -3011,7 +3011,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_dry_run(self): self.create_regular_file('compressible', size=10000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') archives_before = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') self.cmd(f'--repo={self.repository_location}', 'recreate', '-n', '-e', 'input/compressible') self.check_cache() @@ -3021,7 +3021,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_skips_nothing_to_do(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') info_before = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') self.cmd(f'--repo={self.repository_location}', 'recreate', '--chunker-params', 'default') self.check_cache() @@ -3042,7 +3042,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file4', size=0) self.create_regular_file('file5', size=0) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') output = self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--list', '--info', '-e', 'input/file2') self.check_cache() @@ -3066,7 +3066,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_bad_filters(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}', 'delete', '--first', '1', '--last', '1', fork=True, exit_code=2) def test_key_export_keyfile(self): @@ -3266,7 +3266,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_debug_dump_manifest(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') dump_file = self.output_path + '/dump' output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-manifest', dump_file) assert output == "" @@ -3281,7 +3281,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_debug_dump_archive(self): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') dump_file = self.output_path + '/dump' output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-archive', '--name=test', dump_file) assert output == "" @@ -3297,7 +3297,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 output = self.cmd(f'--repo={self.repository_location}', 'debug', 'refcount-obj', '0' * 64).strip() assert output == 'object 0000000000000000000000000000000000000000000000000000000000000000 not found [info from chunks cache].' - create_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', '--json', 'input')) + create_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'create', '--json', 'test', 'input')) archive_id = create_json['archive']['id'] output = self.cmd(f'--repo={self.repository_location}', 'debug', 'refcount-obj', archive_id).strip() assert output == 'object ' + archive_id + ' has 1 referrers [info from chunks cache].' @@ -3363,7 +3363,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_test_files() os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'simple.tar', '--progress', '--tar-format=GNU') with changedir('output'): # This probably assumes GNU tar. Note -p switch to extract permissions regardless of umask. @@ -3378,7 +3378,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_test_files() os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') list = self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'simple.tar.gz', '--list', '--tar-format=GNU') assert 'input/file1\n' in list @@ -3394,7 +3394,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_test_files() os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') list = self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'simple.tar', '--strip-components=1', '--list', '--tar-format=GNU') # --list's path are those before processing with --strip-components @@ -3433,7 +3433,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_test_files(create_hardlinks=False) # hardlinks become separate files os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=src', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'src', 'input') self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=src', 'simple.tar', f'--tar-format={tar_format}') self.cmd(f'--repo={self.repository_location}', 'import-tar', '--name=dst', 'simple.tar') with changedir(self.output_path): @@ -3447,7 +3447,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_test_files(create_hardlinks=False) # hardlinks become separate files os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=src', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'src', 'input') self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=src', 'simple.tgz', f'--tar-format={tar_format}') self.cmd(f'--repo={self.repository_location}', 'import-tar', '--name=dst', 'simple.tgz') with changedir(self.output_path): @@ -3457,7 +3457,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_roundtrip_pax_borg(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=src', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'src', 'input') self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=src', 'simple.tar', '--tar-format=BORG') self.cmd(f'--repo={self.repository_location}', 'import-tar', '--name=dst', 'simple.tar') with changedir(self.output_path): @@ -3475,7 +3475,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_regular_file('file') xattr.setxattr(b'input/file', b'user.attribute%p', b'value') self.cmd('init', self.repository_location, '-e' 'none') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): with patch.object(xattr, 'setxattr', patched_setxattr_EACCES): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_WARNING) @@ -3491,7 +3491,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 os.makedirs(os.path.join(self.input_path, 'dir%p')) xattr.setxattr(b'input/dir%p', b'user.attribute', b'value') self.cmd(f'--repo={self.repository_location}', 'init', '-e' 'none') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): with patch.object(xattr, 'setxattr', patched_setxattr_EACCES): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_WARNING) @@ -3510,7 +3510,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 both the client and the server forget the nonce""" self.create_regular_file('file1', contents=b'Hello, borg') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') # Oops! We have removed the repo-side memory of the nonce! # See https://github.com/borgbackup/borg/issues/5858 os.remove(os.path.join(self.repository_path, 'nonce')) @@ -3541,13 +3541,13 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 """ self.create_regular_file('file1', contents=b'Hello, borg') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') # Oops! We have removed the repo-side memory of the nonce! # See https://github.com/borgbackup/borg/issues/5858 nonce = os.path.join(self.repository_path, 'nonce') os.remove(nonce) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input') assert os.path.exists(nonce) def test_init_defaults_to_argon2(self): @@ -3994,7 +3994,7 @@ class RemoteArchiverTestCase(ArchiverTestCase): 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(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') marker = 'cached responses left in RemoteRepository' with changedir('output'): res = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', "--debug", '--strip-components', '3') @@ -4035,17 +4035,17 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'info') def test_cache_files(self): - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.corrupt(os.path.join(self.cache_path, 'files')) - out = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', 'input') + out = self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input') # borg warns about the corrupt files cache, but then continues without files cache. assert 'files cache is corrupted' in out def test_chunks_archive(self): - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input') # Find ID of test1 so we can corrupt it later :) target_id = self.cmd(f'--repo={self.repository_location}', 'list', '--format={id}{LF}').strip() - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test2', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input') # Force cache sync, creating archive chunks of test1 and test2 in chunks.archive.d self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') @@ -4065,7 +4065,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): config.write(fd) # Cache sync notices corrupted archive chunks, but automatically recovers. - out = self.cmd(f'--repo={self.repository_location}', 'create', '--name=test3', '-v', 'input', exit_code=1) + out = self.cmd(f'--repo={self.repository_location}', 'create', '-v', '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 @@ -4111,7 +4111,7 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') # Create the first snapshot - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test0', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') # Setup files for the second snapshot self.create_regular_file('file_added', size=2048) @@ -4142,8 +4142,8 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): fd.write(b'appended_data') # Create the second snapshot - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1a', 'input') - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1b', '--chunker-params', '16,18,17,4095', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test1a', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test1b', 'input', '--chunker-params', '16,18,17,4095') def do_asserts(output, can_compare_ids): # File contents changed (deleted and replaced with a new file) @@ -4300,7 +4300,7 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('f_file_removed', size=16) self.create_regular_file('c_file_changed', size=32) self.create_regular_file('e_file_changed', size=64) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test0', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') os.unlink('input/a_file_removed') os.unlink('input/f_file_removed') @@ -4310,7 +4310,7 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('e_file_changed', size=1024) self.create_regular_file('b_file_added', size=128) self.create_regular_file('d_file_added', size=256) - self.cmd(f'--repo={self.repository_location}', 'create', '--name=test1', 'input') + self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input') output = self.cmd(f'--repo={self.repository_location}', 'diff', '--name=test0', '--name2=test1', '--sort') diff --git a/src/borg/testsuite/benchmark.py b/src/borg/testsuite/benchmark.py index 4eb3a3da6..fac828c13 100644 --- a/src/borg/testsuite/benchmark.py +++ b/src/borg/testsuite/benchmark.py @@ -57,19 +57,19 @@ def testdata(request, tmpdir_factory): @pytest.fixture(params=['none', 'lz4']) def repo_archive(request, cmd, repo, testdata): archive = 'test' - cmd(f'--repo={repo}', 'create', f'--name={archive}', '--compression', request.param, testdata) + cmd(f'--repo={repo}', 'create', f'{archive}', '--compression', request.param, testdata) return repo, archive def test_create_none(benchmark, cmd, repo, testdata): result, out = benchmark.pedantic(cmd, (f'--repo={repo}', 'create', '--compression', 'none', - '--name', 'test', testdata)) + 'test', testdata)) assert result == 0 def test_create_lz4(benchmark, cmd, repo, testdata): result, out = benchmark.pedantic(cmd, (f'--repo={repo}', 'create', '--compression', 'lz4', - '--name', 'test', testdata)) + 'test', testdata)) assert result == 0 From f8d2024578e199a2c20d4d478839808fc98eefd0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 20 Jun 2022 15:56:03 +0200 Subject: [PATCH 057/160] borg recreate -a ARCHIVE_GLOB ... --- src/borg/archiver.py | 28 +++++++++------------------- src/borg/helpers/manifest.py | 8 +++++--- src/borg/testsuite/archiver.py | 32 ++++++++++++++++---------------- 3 files changed, 30 insertions(+), 38 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 1daf2a91c..f82b4726b 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2005,25 +2005,16 @@ class Archiver: checkpoint_interval=args.checkpoint_interval, dry_run=args.dry_run, timestamp=args.timestamp) - if args.name: - name = args.name + archive_names = tuple(archive.name for archive in manifest.archives.list_considering(args)) + if args.target is not None and len(archive_names) != 1: + self.print_error('--target: Need to specify single archive') + return self.exit_code + for name in archive_names: if recreater.is_temporary_archive(name): - self.print_error('Refusing to work on temporary archive of prior recreate: %s', name) - return self.exit_code + continue + print('Processing', name) if not recreater.recreate(name, args.comment, args.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') - 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): - logger.info('Skipped archive %s: Nothing to do. Archive was not processed.', name) + logger.info('Skipped archive %s: Nothing to do. Archive was not processed.', name) if not args.dry_run: manifest.write() repository.commit(compact=False) @@ -4946,6 +4937,7 @@ class Archiver: define_exclusion_group(subparser, tag_files=True) archive_group = subparser.add_argument_group('Archive options') + define_archive_filters_group(archive_group) 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 ' @@ -4981,8 +4973,6 @@ class Archiver: 'HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the current defaults. ' 'default: %s,%d,%d,%d,%d' % CHUNKER_PARAMS) - subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, - help='specify the archive name') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to recreate; patterns are supported') diff --git a/src/borg/helpers/manifest.py b/src/borg/helpers/manifest.py index 587176b49..786eb27bb 100644 --- a/src/borg/helpers/manifest.py +++ b/src/borg/helpers/manifest.py @@ -103,11 +103,13 @@ class Archives(abc.MutableMapping): """ get a list of archives, considering --first/last/prefix/glob-archives/sort/consider-checkpoints cmdline args """ - if args.name: - raise Error('The options --first, --last, --prefix, and --glob-archives, and --consider-checkpoints can only be used on repository targets.') + name = getattr(args, 'name', None) + consider_checkpoints = getattr(args, 'consider_checkpoints', None) + if name is not None: + raise Error('Giving a specific name is incompatible with options --first, --last, --prefix, and --glob-archives, and --consider-checkpoints.') if args.prefix is not None: args.glob_archives = args.prefix + '*' - return self.list(sort_by=args.sort_by.split(','), consider_checkpoints=args.consider_checkpoints, glob=args.glob_archives, first=args.first, last=args.last) + return self.list(sort_by=args.sort_by.split(','), consider_checkpoints=consider_checkpoints, 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 48f682f23..b9c18ec22 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1314,7 +1314,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_exclude_caches(self): self._create_test_caches() self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--exclude-caches') + self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test', '--exclude-caches') self._assert_test_caches() def _create_test_tagged(self): @@ -1338,7 +1338,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_exclude_tagged(self): self._create_test_tagged() self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--exclude-if-present', '.NOBACKUP', + self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP') self._assert_test_tagged() @@ -1377,7 +1377,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_exclude_keep_tagged(self): self._create_test_keep_tagged() self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--exclude-if-present', '.NOBACKUP1', + self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2', '--exclude-caches', '--keep-exclude-tags') self._assert_test_keep_tagged() @@ -1392,7 +1392,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # in the "test" archive, we now have, in this order: # - a regular file item for "file1" # - a hardlink item for "CACHEDIR.TAG" referring back to file1 for its contents - self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--exclude-caches', '--keep-exclude-tags') + self.cmd(f'--repo={self.repository_location}', 'recreate', 'test', '--exclude-caches', '--keep-exclude-tags') # if issue #4911 is present, the recreate will crash with a KeyError for "input/file1" @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='Linux capabilities test, requires fakeroot >= 1.20.2') @@ -1579,10 +1579,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert 'Comment: \n' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test1') assert 'Comment: this is the comment' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test2') - self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test1', '--comment', 'added comment') - self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test2', '--comment', 'modified comment') - self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test3', '--comment', '') - self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test4', '12345') + self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test1', '--comment', 'added comment') + self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test2', '--comment', 'modified comment') + self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test3', '--comment', '') + self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test4', '12345') assert 'Comment: added comment' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test1') assert 'Comment: modified comment' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test2') assert 'Comment: \n' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test3') @@ -2921,7 +2921,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') self.check_cache() original_archive = self.cmd(f'--repo={self.repository_location}', 'list') - self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test0', 'input/dir2', + self.cmd(f'--repo={self.repository_location}', 'recreate', 'test0', 'input/dir2', '-e', 'input/dir2/file3', '--target=new-archive') self.check_cache() archives = self.cmd(f'--repo={self.repository_location}', 'list') @@ -2938,7 +2938,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('dir2/file3', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') - self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test0', 'input/dir2', '-e', 'input/dir2/file3') + self.cmd(f'--repo={self.repository_location}', 'recreate', 'test0', 'input/dir2', '-e', 'input/dir2/file3') self.check_cache() listing = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test0', '--short') assert 'file1' not in listing @@ -2950,7 +2950,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # This is essentially the same problem set as in test_extract_hardlinks self._extract_hardlinks_setup() self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input') - self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', 'input/dir1') + self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test', 'input/dir1') self.check_cache() with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') @@ -3000,7 +3000,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') - self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test0', '--timestamp', "1970-01-02T00:00:00", + self.cmd(f'--repo={self.repository_location}', 'recreate', 'test0', '--timestamp', "1970-01-02T00:00:00", '--comment', 'test') info = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test0').splitlines() dtime = datetime(1970, 1, 2) + local_timezone.utcoffset(None) @@ -3044,22 +3044,22 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - output = self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--list', '--info', '-e', 'input/file2') + output = self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test', '--list', '--info', '-e', 'input/file2') self.check_cache() self.assert_in("input/file1", output) self.assert_in("x input/file2", output) - output = self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '--list', '-e', 'input/file3') + output = self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test', '--list', '-e', 'input/file3') self.check_cache() self.assert_in("input/file1", output) self.assert_in("x input/file3", output) - output = self.cmd(f'--repo={self.repository_location}', 'recreate', '--name=test', '-e', 'input/file4') + output = self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', '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(f'--repo={self.repository_location}', 'recreate', '--name=test', '--info', '-e', 'input/file5') + output = self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test', '--info', '-e', 'input/file5') self.check_cache() self.assert_not_in("input/file1", output) self.assert_not_in("x input/file5", output) From b8c7c53dde1df9cbf69566606d796f2a3f606352 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 20 Jun 2022 16:04:58 +0200 Subject: [PATCH 058/160] borg extract NAME ... --- src/borg/archiver.py | 6 +- src/borg/testsuite/archiver.py | 162 ++++++++++++++++---------------- src/borg/testsuite/benchmark.py | 2 +- 3 files changed, 85 insertions(+), 85 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f82b4726b..8dd84d56a 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -691,7 +691,7 @@ class Archiver: assert rc1 == rc2 == 0 # measure extraction (dry-run: without writing result to disk) t_start = time.monotonic() - rc = self.do_extract(self.parse_args([f'--repo={repo}', 'extract', '--name=borg-benchmark-crud1', + rc = self.do_extract(self.parse_args([f'--repo={repo}', 'extract', 'borg-benchmark-crud1', '--dry-run'])) t_end = time.monotonic() dt_extract = t_end - t_start @@ -4312,8 +4312,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='extract archive contents') subparser.set_defaults(func=self.do_extract) - subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, - help='specify the archive name') 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', action='store_true', @@ -4334,6 +4332,8 @@ class Archiver: help='write all extracted data to stdout') subparser.add_argument('--sparse', dest='sparse', action='store_true', help='create holes in output sparse file from all-zero chunks') + subparser.add_argument('name', metavar='NAME', type=NameSpec, + help='specify the archive name') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to extract; patterns are supported') define_exclusion_group(subparser, strip_components=True) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index b9c18ec22..5093ee511 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -152,9 +152,9 @@ def test_return_codes(cmd, tmpdir): rc, out = cmd('--repo=%s' % repo, 'create', 'archive', str(input)) assert rc == EXIT_SUCCESS with changedir(str(output)): - rc, out = cmd('--repo=%s' % repo, 'extract', '--name=archive') + rc, out = cmd('--repo=%s' % repo, 'extract', 'archive') assert rc == EXIT_SUCCESS - rc, out = cmd('--repo=%s' % repo, 'extract', '--name=archive', 'does/not/match') + rc, out = cmd('--repo=%s' % repo, 'extract', 'archive', 'does/not/match') assert rc == EXIT_WARNING # pattern did not match rc, out = cmd('--repo=%s' % repo, 'create', 'archive', str(input)) assert rc == EXIT_ERROR # duplicate archive name @@ -399,7 +399,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('Archive name: test.2', output) self.assert_in('This archive: ', output) with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') list_output = self.cmd(f'--repo={self.repository_location}', 'list', '--short') self.assert_in('test', list_output) self.assert_in('test.2', list_output) @@ -494,7 +494,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') sock.close() with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') assert not os.path.exists('input/unix-socket') @pytest.mark.skipif(not are_symlinks_supported(), reason='symlinks not supported') @@ -503,7 +503,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') assert os.readlink('input/link1') == 'somewhere' @pytest.mark.skipif(not are_symlinks_supported() or not are_hardlinks_supported(), @@ -516,7 +516,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + output = self.cmd(f'--repo={self.repository_location}', 'extract', 'test') print(output) with changedir('input'): assert os.path.exists('target') @@ -550,7 +550,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', '--atime', 'test', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') sti = os.stat('input/file1') sto = os.stat('output/input/file1') assert sti.st_mtime_ns == sto.st_mtime_ns == mtime * 1e9 @@ -570,7 +570,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') sti = os.stat('input/file1') sto = os.stat('output/input/file1') assert int(sti.st_birthtime * 1e9) == int(sto.st_birthtime * 1e9) == birthtime * 1e9 @@ -586,7 +586,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', '--nobirthtime') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') sti = os.stat('input/file1') sto = os.stat('output/input/file1') assert int(sti.st_birthtime * 1e9) == birthtime * 1e9 @@ -647,7 +647,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir(self.output_path): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--sparse') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--sparse') self.assert_dirs_equal('input', 'output/input') filename = os.path.join(self.output_path, 'input', 'sparse') with open(filename, 'rb') as fd: @@ -667,7 +667,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') for filename in filenames: with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', os.path.join('input', filename)) + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', os.path.join('input', filename)) assert os.path.exists(os.path.join('output', 'input', filename)) def test_repository_swap_detection(self): @@ -807,14 +807,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('dir/file') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--strip-components', '3') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--strip-components', '3') assert not os.path.exists('file') with self.assert_creates_file('file'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--strip-components', '2') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--strip-components', '2') with self.assert_creates_file('dir/file'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--strip-components', '1') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--strip-components', '1') with self.assert_creates_file('input/dir/file'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--strip-components', '0') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--strip-components', '0') def _extract_hardlinks_setup(self): os.mkdir(os.path.join(self.input_path, 'dir1')) @@ -873,7 +873,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_extract_hardlinks1(self): self._extract_hardlinks_setup() with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') assert os.stat('input/source').st_nlink == 4 assert os.stat('input/abba').st_nlink == 4 assert os.stat('input/dir1/hardlink').st_nlink == 4 @@ -884,14 +884,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_extract_hardlinks2(self): self._extract_hardlinks_setup() with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--strip-components', '2') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--strip-components', '2') assert os.stat('hardlink').st_nlink == 2 assert os.stat('subdir/hardlink').st_nlink == 2 assert open('subdir/hardlink', 'rb').read() == b'123456' assert os.stat('aaaa').st_nlink == 2 assert os.stat('source2').st_nlink == 2 with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', 'input/dir1') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', 'input/dir1') assert os.stat('input/dir1/hardlink').st_nlink == 2 assert os.stat('input/dir1/subdir/hardlink').st_nlink == 2 assert open('input/dir1/subdir/hardlink', 'rb').read() == b'123456' @@ -913,7 +913,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', 'input') # give input twice! # now test extraction with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') # if issue #5603 happens, extraction gives rc == 1 (triggering AssertionError) and warnings like: # input/a/hardlink: link: [Errno 2] No such file or directory: 'input/a/hardlink' -> 'input/a/hardlink' # input/b/hardlink: link: [Errno 2] No such file or directory: 'input/a/hardlink' -> 'input/b/hardlink' @@ -929,13 +929,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file4', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'create', '--exclude=input/file4', 'test', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', 'input/file1', ) + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', 'input/file1', ) self.assert_equal(sorted(os.listdir('output/input')), ['file1']) with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude=input/file2') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--exclude=input/file2') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3']) with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude-from=' + self.exclude_file_path) + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--exclude-from=' + self.exclude_file_path) self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3']) def test_extract_include_exclude_regex(self): @@ -949,25 +949,25 @@ class ArchiverTestCase(ArchiverTestCaseBase): # Create with regular expression exclusion for file4 self.cmd(f'--repo={self.repository_location}', 'create', '--exclude=re:input/file4$', 'test', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333']) shutil.rmtree('output/input') # Extract with regular expression exclusion with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude=re:file3+') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--exclude=re:file3+') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2']) shutil.rmtree('output/input') # Combine --exclude with fnmatch and regular expression with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude=input/file2', '--exclude=re:file[01]') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--exclude=input/file2', '--exclude=re:file[01]') self.assert_equal(sorted(os.listdir('output/input')), ['file3', 'file333']) shutil.rmtree('output/input') # Combine --exclude-from and regular expression exclusion with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude-from=' + self.exclude_file_path, + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--exclude-from=' + self.exclude_file_path, '--exclude=re:file1', '--exclude=re:file(\\d)\\1\\1$') self.assert_equal(sorted(os.listdir('output/input')), ['file3']) @@ -987,7 +987,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', '--exclude-from=' + self.exclude_file_path, 'test', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333']) shutil.rmtree('output/input') @@ -996,7 +996,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b're:file3+\n') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude-from=' + self.exclude_file_path) + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--exclude-from=' + self.exclude_file_path) self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2']) shutil.rmtree('output/input') @@ -1008,7 +1008,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b're:file2$\n') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--exclude-from=' + self.exclude_file_path) + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--exclude-from=' + self.exclude_file_path) self.assert_equal(sorted(os.listdir('output/input')), ['file3']) def test_extract_with_pattern(self): @@ -1023,25 +1023,25 @@ class ArchiverTestCase(ArchiverTestCaseBase): # Extract everything with regular expression with changedir("output"): - self.cmd(f'--repo={self.repository_location}', "extract", "--name=test", "re:.*") + self.cmd(f'--repo={self.repository_location}', "extract", "test", "re:.*") self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file3", "file333", "file4"]) shutil.rmtree("output/input") # Extract with pattern while also excluding files with changedir("output"): - self.cmd(f'--repo={self.repository_location}', "extract", "--name=test", "--exclude=re:file[34]$", r"re:file\d$") + self.cmd(f'--repo={self.repository_location}', "extract", "--exclude=re:file[34]$", "test", r"re:file\d$") self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2"]) shutil.rmtree("output/input") # Combine --exclude with pattern for extraction with changedir("output"): - self.cmd(f'--repo={self.repository_location}', "extract", "--name=test", "--exclude=input/file1", "re:file[12]$") + self.cmd(f'--repo={self.repository_location}', "extract", "--exclude=input/file1", "test", "re:file[12]$") self.assert_equal(sorted(os.listdir("output/input")), ["file2"]) shutil.rmtree("output/input") # Multiple pattern with changedir("output"): - self.cmd(f'--repo={self.repository_location}', "extract", "--name=test", "fm:input/file1", "fm:*file33*", "input/file2") + self.cmd(f'--repo={self.repository_location}', "extract", "test", "fm:input/file1", "fm:*file33*", "input/file2") self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"]) def test_extract_list_output(self): @@ -1051,22 +1051,22 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + output = self.cmd(f'--repo={self.repository_location}', 'extract', 'test') self.assert_not_in("input/file", output) shutil.rmtree('output/input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--info') + output = self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--info') self.assert_not_in("input/file", output) shutil.rmtree('output/input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--list') + output = self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--list') self.assert_in("input/file", output) shutil.rmtree('output/input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--list', '--info') + output = self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--list', '--info') self.assert_in("input/file", output) def test_extract_progress(self): @@ -1075,7 +1075,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--progress') + output = self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--progress') assert 'Extracting:' in output def _create_test_caches(self): @@ -1101,7 +1101,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert item['gid'] == 0 assert item['size'] == len(input_data) assert item['path'] == 'stdin' - extracted_data = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--stdout', binary_output=True) + extracted_data = self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--stdout', binary_output=True) assert extracted_data == input_data def test_create_content_from_command(self): @@ -1115,7 +1115,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert item['gid'] == 0 assert item['size'] == len(input_data) + 1 # `echo` adds newline assert item['path'] == name - extracted_data = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--stdout') + extracted_data = self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--stdout') assert extracted_data == input_data + '\n' def test_create_content_from_command_with_failed_command(self): @@ -1296,13 +1296,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file_important', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--pattern=+input/file_important', '--pattern=-input/file*') self.assert_equal(sorted(os.listdir('output/input')), ['file_important']) def _assert_test_caches(self): with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') self.assert_equal(sorted(os.listdir('output/input')), ['cache2', 'file1']) self.assert_equal(sorted(os.listdir('output/input/cache2')), [CACHE_TAG_NAME]) @@ -1326,7 +1326,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def _assert_test_tagged(self): with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') self.assert_equal(sorted(os.listdir('output/input')), ['file1']) def test_exclude_tagged(self): @@ -1360,7 +1360,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def _assert_test_keep_tagged(self): with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') self.assert_equal(sorted(os.listdir('output/input')), ['file0', 'tagged1', 'tagged2', 'tagged3', 'taggedall']) self.assert_equal(os.listdir('output/input/tagged1'), ['.NOBACKUP1']) self.assert_equal(os.listdir('output/input/tagged2'), ['.NOBACKUP2']) @@ -1413,7 +1413,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): with patch.object(os, 'fchown', patched_fchown): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') assert xattr.getxattr(b'input/file', b'security.capability') == capabilities @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='xattr not supported on this system or on this version of' @@ -1435,15 +1435,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): with changedir('output'): input_abspath = os.path.abspath('input/file') with patch.object(xattr, 'setxattr', patched_setxattr_E2BIG): - out = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_WARNING) + out = self.cmd(f'--repo={self.repository_location}', 'extract', 'test', exit_code=EXIT_WARNING) assert ': when setting extended attribute user.attribute: too big for this filesystem\n' in out os.remove(input_abspath) with patch.object(xattr, 'setxattr', patched_setxattr_ENOTSUP): - out = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_WARNING) + out = self.cmd(f'--repo={self.repository_location}', 'extract', 'test', exit_code=EXIT_WARNING) assert ': when setting extended attribute user.attribute: xattrs not supported on this filesystem\n' in out os.remove(input_abspath) with patch.object(xattr, 'setxattr', patched_setxattr_EACCES): - out = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_WARNING) + out = self.cmd(f'--repo={self.repository_location}', 'extract', 'test', exit_code=EXIT_WARNING) assert ': when setting extended attribute user.attribute: Permission denied\n' in out assert os.path.isfile(input_abspath) @@ -1464,16 +1464,16 @@ class ArchiverTestCase(ArchiverTestCaseBase): with changedir('input'): self.cmd(f'--repo={self.repository_location}', 'create', 'test1', '.', '--exclude=file1') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test1') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test1') self.assert_equal(sorted(os.listdir('output')), ['file2']) with changedir('input'): self.cmd(f'--repo={self.repository_location}', 'create', 'test2', '.', '--exclude=./file1') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test2') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test2') self.assert_equal(sorted(os.listdir('output')), ['file2']) self.cmd(f'--repo={self.repository_location}', 'create', 'test3', 'input', '--exclude=input/./file1') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test3') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test3') self.assert_equal(sorted(os.listdir('output/input')), ['file2']) def test_repeated_files(self): @@ -1491,14 +1491,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.mkdir('output/input/file1') os.mkdir('output/input/dir2') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') self.assert_dirs_equal('input', 'output/input') # But non-empty dirs should fail os.unlink('output/input/file1') os.mkdir('output/input/file1') os.mkdir('output/input/file1/dir') with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=1) + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', exit_code=1) def test_rename(self): self.create_regular_file('file1', size=1024 * 80) @@ -1506,13 +1506,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--dry-run') - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.2', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test.2', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'rename', 'test', 'test.3') - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.2', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test.2', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'rename', 'test.2', 'test.4') - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.3', '--dry-run') - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.4', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test.3', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test.4', '--dry-run') # Make sure both archives have been renamed with Repository(self.repository_path) as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -1597,12 +1597,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test.3', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'another_test.1', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'another_test.2', 'input') - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--dry-run') - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.2', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test.2', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'delete', '--prefix', 'another_') self.cmd(f'--repo={self.repository_location}', 'delete', '--last', '1') self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test') - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test.2', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test.2', '--dry-run') output = self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test.2', '--stats') self.assert_in('Deleted data:', output) # Make sure all data except the manifest has been deleted @@ -1616,7 +1616,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'test3', 'input') self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test1', 'test2') - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test3', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test3', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test3') assert not self.cmd(f'--repo={self.repository_location}', 'list') @@ -1670,7 +1670,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_corrupted_repository(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.create_src_archive('test') - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--dry-run') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--dry-run') output = self.cmd(f'--repo={self.repository_location}', 'check', '--show-version') self.assert_in('borgbackup version', output) # implied output even without --info given self.assert_not_in('Starting repository check', output) # --info not given for root logger @@ -1734,14 +1734,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', '--bypass-lock') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--bypass-lock') def test_readonly_info(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -1845,7 +1845,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.add_unknown_feature(Manifest.Operation.READ) with changedir('output'): - self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'extract', '--name=test']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'extract', 'test']) self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'list']) self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'info', '--name=test']) @@ -2083,7 +2083,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): finally: t.join() with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') fifo_fn = 'input/link_fifo' with open(fifo_fn, 'rb') as f: extracted_data = f.read() @@ -2953,13 +2953,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test', 'input/dir1') self.check_cache() with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') 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 with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test2') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test2') assert os.stat('input/dir1/hardlink').st_nlink == 4 def test_recreate_rechunkify(self): @@ -3437,7 +3437,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=src', 'simple.tar', f'--tar-format={tar_format}') self.cmd(f'--repo={self.repository_location}', 'import-tar', '--name=dst', 'simple.tar') with changedir(self.output_path): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=dst') + self.cmd(f'--repo={self.repository_location}', 'extract', 'dst') self.assert_dirs_equal('input', 'output/input', ignore_ns=True, ignore_xattrs=True) @requires_gzip @@ -3451,7 +3451,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=src', 'simple.tgz', f'--tar-format={tar_format}') self.cmd(f'--repo={self.repository_location}', 'import-tar', '--name=dst', 'simple.tgz') with changedir(self.output_path): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=dst') + self.cmd(f'--repo={self.repository_location}', 'extract', 'dst') self.assert_dirs_equal('input', 'output/input', ignore_ns=True, ignore_xattrs=True) def test_roundtrip_pax_borg(self): @@ -3461,7 +3461,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=src', 'simple.tar', '--tar-format=BORG') self.cmd(f'--repo={self.repository_location}', 'import-tar', '--name=dst', 'simple.tar') with changedir(self.output_path): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=dst') + self.cmd(f'--repo={self.repository_location}', 'extract', 'dst') self.assert_dirs_equal('input', 'output/input') # derived from test_extract_xattrs_errors() @@ -3478,7 +3478,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): with patch.object(xattr, 'setxattr', patched_setxattr_EACCES): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_WARNING) + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', exit_code=EXIT_WARNING) # derived from test_extract_xattrs_errors() @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='xattr not supported on this system or on this version of' @@ -3494,7 +3494,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): with patch.object(xattr, 'setxattr', patched_setxattr_EACCES): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', exit_code=EXIT_WARNING) + self.cmd(f'--repo={self.repository_location}', 'extract', 'test', exit_code=EXIT_WARNING) def test_do_not_mention_archive_if_you_can_not_find_repo(self): """https://github.com/borgbackup/borg/issues/6014""" @@ -3529,7 +3529,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 assert 'file1' in archive_list # Extracting the archive should work with changedir('output'): - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'extract', 'test') self.assert_dirs_equal('input', 'output/input') def test_recovery_from_deleted_repo_nonce(self): @@ -3826,7 +3826,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'check', exit_code=1) self.cmd(f'--repo={self.repository_location}', 'check', '--repair', exit_code=0) self.cmd(f'--repo={self.repository_location}', 'check', exit_code=0) - self.cmd(f'--repo={self.repository_location}', 'extract', '--name=archive1', '--dry-run', exit_code=0) + self.cmd(f'--repo={self.repository_location}', 'extract', 'archive1', '--dry-run', exit_code=0) def _test_verify_data(self, *init_args): shutil.rmtree(self.repository_path) @@ -3997,16 +3997,16 @@ class RemoteArchiverTestCase(ArchiverTestCase): self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') marker = 'cached responses left in RemoteRepository' with changedir('output'): - res = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', "--debug", '--strip-components', '3') + res = self.cmd(f'--repo={self.repository_location}', 'extract', 'test', "--debug", '--strip-components', '3') assert marker not in res with self.assert_creates_file('file'): - res = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', "--debug", '--strip-components', '2') + res = self.cmd(f'--repo={self.repository_location}', 'extract', 'test', "--debug", '--strip-components', '2') assert marker not in res with self.assert_creates_file('dir/file'): - res = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', "--debug", '--strip-components', '1') + res = self.cmd(f'--repo={self.repository_location}', 'extract', 'test', "--debug", '--strip-components', '1') assert marker not in res with self.assert_creates_file('input/dir/file'): - res = self.cmd(f'--repo={self.repository_location}', 'extract', '--name=test', "--debug", '--strip-components', '0') + res = self.cmd(f'--repo={self.repository_location}', 'extract', 'test', "--debug", '--strip-components', '0') assert marker not in res diff --git a/src/borg/testsuite/benchmark.py b/src/borg/testsuite/benchmark.py index fac828c13..0a9a4373a 100644 --- a/src/borg/testsuite/benchmark.py +++ b/src/borg/testsuite/benchmark.py @@ -76,7 +76,7 @@ def test_create_lz4(benchmark, cmd, repo, testdata): def test_extract(benchmark, cmd, repo_archive, tmpdir): repo, archive = repo_archive with changedir(str(tmpdir)): - result, out = benchmark.pedantic(cmd, (f'--repo={repo}', 'extract', '--name', archive)) + result, out = benchmark.pedantic(cmd, (f'--repo={repo}', 'extract', archive)) assert result == 0 From 75b53de37ef2244180a35e693709ba08de1779e5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 20 Jun 2022 16:50:36 +0200 Subject: [PATCH 059/160] borg diff ARCH1 ARCH2 --- src/borg/archiver.py | 6 +++--- src/borg/testsuite/archiver.py | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 8dd84d56a..b3bd9de68 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1477,7 +1477,7 @@ class Archiver: print_output = print_json_output if args.json_lines else print_text_output archive1 = archive - archive2 = Archive(repository, key, manifest, args.name2, + archive2 = Archive(repository, key, manifest, args.other_name, consider_part_files=args.consider_part_files) can_compare_chunk_ids = archive1.metadata.get('chunker_params', False) == archive2.metadata.get( @@ -4206,10 +4206,10 @@ class Archiver: help='Sort the output lines by file path.') subparser.add_argument('--json-lines', action='store_true', help='Format output as JSON Lines. ') - subparser.add_argument('--name', metavar='ARCHIVE1', + subparser.add_argument('name', metavar='ARCHIVE1', type=archivename_validator(), help='ARCHIVE1 name') - subparser.add_argument('--name2', metavar='ARCHIVE2', + subparser.add_argument('other_name', metavar='ARCHIVE2', type=archivename_validator(), help='ARCHIVE2 name') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 5093ee511..f31813264 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1704,14 +1704,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}', 'diff', '--name=a', '--name2=b', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'diff', 'a', 'b', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd(f'--repo={self.repository_location}', 'diff', '--name=a', '--name=b') + self.cmd(f'--repo={self.repository_location}', 'diff', 'a', 'b') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd(f'--repo={self.repository_location}', 'diff', '--name=a', '--name2=b', '--bypass-lock') + self.cmd(f'--repo={self.repository_location}', 'diff', 'a', 'b', '--bypass-lock') def test_readonly_export_tar(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -4288,10 +4288,10 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): if are_hardlinks_supported(): assert not any(get_changes('input/hardlink_target_replaced', joutput)) - do_asserts(self.cmd(f'--repo={self.repository_location}', 'diff', '--name=test0', '--name2=test1a'), True) + do_asserts(self.cmd(f'--repo={self.repository_location}', 'diff', 'test0', 'test1a'), True) # We expect exit_code=1 due to the chunker params warning - do_asserts(self.cmd(f'--repo={self.repository_location}', 'diff', '--name=test0', '--name2=test1b', exit_code=1), False) - do_json_asserts(self.cmd(f'--repo={self.repository_location}', 'diff', '--name=test0', '--name2=test1a', '--json-lines'), True) + do_asserts(self.cmd(f'--repo={self.repository_location}', 'diff', 'test0', 'test1b', exit_code=1), False) + do_json_asserts(self.cmd(f'--repo={self.repository_location}', 'diff', 'test0', 'test1a', '--json-lines'), True) def test_sort_option(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -4312,7 +4312,7 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('d_file_added', size=256) self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input') - output = self.cmd(f'--repo={self.repository_location}', 'diff', '--name=test0', '--name2=test1', + output = self.cmd(f'--repo={self.repository_location}', 'diff', 'test0', 'test1', '--sort') expected = [ 'a_file_removed', From 1ed7e5b2921674aaadbc408afc5246bc537b6ce4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 20 Jun 2022 19:40:06 +0200 Subject: [PATCH 060/160] borg dump-archive NAME / dump-archive-items NAME --- src/borg/archiver.py | 4 ++-- src/borg/testsuite/archiver.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index b3bd9de68..dd9f0e20d 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -3952,7 +3952,7 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='dump archive items (metadata) (debug)') subparser.set_defaults(func=self.do_debug_dump_archive_items) - subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + subparser.add_argument('name', metavar='NAME', type=NameSpec, help='specify the archive name') debug_dump_archive_epilog = process_epilog(""" @@ -3964,7 +3964,7 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='dump decoded archive metadata (debug)') subparser.set_defaults(func=self.do_debug_dump_archive) - subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + subparser.add_argument('name', metavar='NAME', type=NameSpec, help='specify the archive name') subparser.add_argument('path', metavar='PATH', type=str, help='file to dump data into') diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index f31813264..5512d0bfb 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2804,7 +2804,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): - output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-archive-items', '--name=test') + output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-archive-items', 'test') output_dir = sorted(os.listdir('output')) assert len(output_dir) > 0 and output_dir[0].startswith('000000_') assert 'Done.' in output @@ -3283,7 +3283,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') dump_file = self.output_path + '/dump' - output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-archive', '--name=test', dump_file) + output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-archive', 'test', dump_file) assert output == "" with open(dump_file) as f: result = json.load(f) From e6a8984c999102ef29221b2949435c215c1eacf4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 20 Jun 2022 19:43:13 +0200 Subject: [PATCH 061/160] borg (import|export)-tar NAME ... --- src/borg/archiver.py | 6 +++--- src/borg/testsuite/archiver.py | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index dd9f0e20d..fd6356268 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -4266,8 +4266,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='create tarball from archive') subparser.set_defaults(func=self.do_export_tar) - subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, - help='specify the archive name') 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', @@ -4275,6 +4273,8 @@ class Archiver: subparser.add_argument('--tar-format', metavar='FMT', dest='tar_format', default='GNU', choices=('BORG', 'PAX', 'GNU'), help='select tar format: BORG, PAX or GNU') + subparser.add_argument('name', metavar='NAME', type=NameSpec, + help='specify the archive name') subparser.add_argument('tarfile', metavar='FILE', help='output tar file. "-" to write to stdout instead.') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, @@ -5214,7 +5214,7 @@ class Archiver: help='select compression algorithm, see the output of the ' '"borg help compression" command for details.') - subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, + subparser.add_argument('name', metavar='NAME', type=NameSpec, help='specify the archive name') subparser.add_argument('tarfile', metavar='TARFILE', help='input tar file. "-" to read from stdin instead.') diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 5512d0bfb..71fb0a064 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1719,14 +1719,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'test.tar', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'export-tar', 'test', 'test.tar', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'test.tar') + self.cmd(f'--repo={self.repository_location}', 'export-tar', 'test', 'test.tar') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'test.tar', '--bypass-lock') + self.cmd(f'--repo={self.repository_location}', 'export-tar', 'test', 'test.tar', '--bypass-lock') def test_readonly_extract(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -3364,7 +3364,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'simple.tar', '--progress', '--tar-format=GNU') + self.cmd(f'--repo={self.repository_location}', 'export-tar', 'test', 'simple.tar', '--progress', '--tar-format=GNU') with changedir('output'): # This probably assumes GNU tar. Note -p switch to extract permissions regardless of umask. subprocess.check_call(['tar', 'xpf', '../simple.tar', '--warning=no-timestamp']) @@ -3379,7 +3379,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - list = self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'simple.tar.gz', + list = self.cmd(f'--repo={self.repository_location}', 'export-tar', 'test', 'simple.tar.gz', '--list', '--tar-format=GNU') assert 'input/file1\n' in list assert 'input/dir2\n' in list @@ -3395,7 +3395,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - list = self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'simple.tar', + list = self.cmd(f'--repo={self.repository_location}', 'export-tar', 'test', 'simple.tar', '--strip-components=1', '--list', '--tar-format=GNU') # --list's path are those before processing with --strip-components assert 'input/file1\n' in list @@ -3408,7 +3408,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 @requires_gnutar def test_export_tar_strip_components_links(self): self._extract_hardlinks_setup() - self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'output.tar', + self.cmd(f'--repo={self.repository_location}', 'export-tar', 'test', 'output.tar', '--strip-components=2', '--tar-format=GNU') with changedir('output'): subprocess.check_call(['tar', 'xpf', '../output.tar', '--warning=no-timestamp']) @@ -3421,7 +3421,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 @requires_gnutar def test_extract_hardlinks_tar(self): self._extract_hardlinks_setup() - self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=test', 'output.tar', 'input/dir1', '--tar-format=GNU') + self.cmd(f'--repo={self.repository_location}', 'export-tar', 'test', 'output.tar', 'input/dir1', '--tar-format=GNU') with changedir('output'): subprocess.check_call(['tar', 'xpf', '../output.tar', '--warning=no-timestamp']) assert os.stat('input/dir1/hardlink').st_nlink == 2 @@ -3434,8 +3434,8 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self.cmd(f'--repo={self.repository_location}', 'create', 'src', 'input') - self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=src', 'simple.tar', f'--tar-format={tar_format}') - self.cmd(f'--repo={self.repository_location}', 'import-tar', '--name=dst', 'simple.tar') + self.cmd(f'--repo={self.repository_location}', 'export-tar', 'src', 'simple.tar', f'--tar-format={tar_format}') + self.cmd(f'--repo={self.repository_location}', 'import-tar', 'dst', 'simple.tar') with changedir(self.output_path): self.cmd(f'--repo={self.repository_location}', 'extract', 'dst') self.assert_dirs_equal('input', 'output/input', ignore_ns=True, ignore_xattrs=True) @@ -3448,8 +3448,8 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 os.unlink('input/flagfile') self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self.cmd(f'--repo={self.repository_location}', 'create', 'src', 'input') - self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=src', 'simple.tgz', f'--tar-format={tar_format}') - self.cmd(f'--repo={self.repository_location}', 'import-tar', '--name=dst', 'simple.tgz') + self.cmd(f'--repo={self.repository_location}', 'export-tar', 'src', 'simple.tgz', f'--tar-format={tar_format}') + self.cmd(f'--repo={self.repository_location}', 'import-tar', 'dst', 'simple.tgz') with changedir(self.output_path): self.cmd(f'--repo={self.repository_location}', 'extract', 'dst') self.assert_dirs_equal('input', 'output/input', ignore_ns=True, ignore_xattrs=True) @@ -3458,8 +3458,8 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self.cmd(f'--repo={self.repository_location}', 'create', 'src', 'input') - self.cmd(f'--repo={self.repository_location}', 'export-tar', '--name=src', 'simple.tar', '--tar-format=BORG') - self.cmd(f'--repo={self.repository_location}', 'import-tar', '--name=dst', 'simple.tar') + self.cmd(f'--repo={self.repository_location}', 'export-tar', 'src', 'simple.tar', '--tar-format=BORG') + self.cmd(f'--repo={self.repository_location}', 'import-tar', 'dst', 'simple.tar') with changedir(self.output_path): self.cmd(f'--repo={self.repository_location}', 'extract', 'dst') self.assert_dirs_equal('input', 'output/input') From 6addafd784ddfaddf4adbbce29a069c37d9d5cd7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 20 Jun 2022 20:11:26 +0200 Subject: [PATCH 062/160] borg mount -a ARCHIVE_GLOB mountpoint ... --- src/borg/archiver.py | 2 -- src/borg/fuse.py | 26 ++++++++++--------------- src/borg/testsuite/archiver.py | 35 +++++++++++++++++----------------- 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index fd6356268..c463f1809 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -3264,8 +3264,6 @@ class Archiver: def define_borg_mount(parser): parser.set_defaults(func=self.do_mount) - parser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, - help='specify the archive name') parser.add_argument('--consider-checkpoints', action='store_true', dest='consider_checkpoints', help='Show checkpoint archives in the repository contents list (default: hidden).') parser.add_argument('mountpoint', metavar='MOUNTPOINT', type=str, diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 5203a2959..b6349da7d 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -35,7 +35,7 @@ from .crypto.low_level import blake2b_128 from .archiver import Archiver from .archive import Archive, get_item_uid_gid from .hashindex import FuseVersionsIndex -from .helpers import daemonize, daemonizing, signal_handler, format_file_size, Error +from .helpers import daemonize, daemonizing, signal_handler, format_file_size from .helpers import HardLinkManager from .helpers import msgpack from .item import Item @@ -272,22 +272,16 @@ class FuseBackend: def _create_filesystem(self): self._create_dir(parent=1) # first call, create root dir (inode == 1) - if self._args.name: + self.versions_index = FuseVersionsIndex() + for archive in self._manifest.archives.list_considering(self._args): if self.versions: - raise Error("for versions view, do not specify a single archive, " - "but always give the repository as location.") - self._process_archive(self._args.name) - else: - self.versions_index = FuseVersionsIndex() - for archive in self._manifest.archives.list_considering(self._args): - if self.versions: - # process archives immediately - self._process_archive(archive.name) - else: - # lazily load archives, create archive placeholder inode - archive_inode = self._create_dir(parent=1, mtime=int(archive.ts.timestamp() * 1e9)) - self.contents[1][os.fsencode(archive.name)] = archive_inode - self.pending_archives[archive_inode] = archive.name + # process archives immediately + self._process_archive(archive.name) + else: + # lazily load archives, create archive placeholder inode + archive_inode = self._create_dir(parent=1, mtime=int(archive.ts.timestamp() * 1e9)) + self.contents[1][os.fsencode(archive.name)] = archive_inode + self.pending_archives[archive_inode] = archive.name def get_item(self, inode): item = self._inode_cache.get(inode) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 71fb0a064..a73580c1c 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -847,22 +847,22 @@ class ArchiverTestCase(ArchiverTestCaseBase): ignore_perms = ['-o', 'ignore_permissions,defer_permissions'] else: ignore_perms = ['-o', 'ignore_permissions'] - with self.fuse_mount(self.repository_location, mountpoint, '--name=test', '--strip-components=2', *ignore_perms), \ - changedir(mountpoint): + with self.fuse_mount(self.repository_location, mountpoint, '-a', 'test', '--strip-components=2', *ignore_perms), \ + changedir(os.path.join(mountpoint, 'test')): assert os.stat('hardlink').st_nlink == 2 assert os.stat('subdir/hardlink').st_nlink == 2 assert open('subdir/hardlink', 'rb').read() == b'123456' assert os.stat('aaaa').st_nlink == 2 assert os.stat('source2').st_nlink == 2 - with self.fuse_mount(self.repository_location, mountpoint, 'input/dir1', '--name=test', *ignore_perms), \ - changedir(mountpoint): + with self.fuse_mount(self.repository_location, mountpoint, 'input/dir1', '-a', 'test', *ignore_perms), \ + changedir(os.path.join(mountpoint, 'test')): assert os.stat('input/dir1/hardlink').st_nlink == 2 assert os.stat('input/dir1/subdir/hardlink').st_nlink == 2 assert open('input/dir1/subdir/hardlink', 'rb').read() == b'123456' assert os.stat('input/dir1/aaaa').st_nlink == 2 assert os.stat('input/dir1/source2').st_nlink == 2 - with self.fuse_mount(self.repository_location, mountpoint, '--name=test', *ignore_perms), \ - changedir(mountpoint): + with self.fuse_mount(self.repository_location, mountpoint, '-a', 'test', *ignore_perms), \ + changedir(os.path.join(mountpoint, 'test')): assert os.stat('input/source').st_nlink == 4 assert os.stat('input/abba').st_nlink == 4 assert os.stat('input/dir1/hardlink').st_nlink == 4 @@ -2527,13 +2527,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): ignore_flags=True, ignore_xattrs=True) self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive2', 'input'), ignore_flags=True, ignore_xattrs=True) - # mount only 1 archive, its contents shall show up directly in mountpoint: - with self.fuse_mount(self.repository_location, mountpoint, '--name=archive'): - self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input'), + with self.fuse_mount(self.repository_location, mountpoint, '-a', 'archive'): + self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive', 'input'), ignore_flags=True, ignore_xattrs=True) # regular file in_fn = 'input/file1' - out_fn = os.path.join(mountpoint, 'input', 'file1') + out_fn = os.path.join(mountpoint, 'archive', 'input', 'file1') # stat sti1 = os.stat(in_fn) sto1 = os.stat(out_fn) @@ -2554,7 +2553,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # hardlink (to 'input/file1') if are_hardlinks_supported(): in_fn = 'input/hardlink' - out_fn = os.path.join(mountpoint, 'input', 'hardlink') + out_fn = os.path.join(mountpoint, 'archive', 'input', 'hardlink') sti2 = os.stat(in_fn) sto2 = os.stat(out_fn) assert sti2.st_nlink == sto2.st_nlink == 2 @@ -2562,7 +2561,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # symlink if are_symlinks_supported(): in_fn = 'input/link1' - out_fn = os.path.join(mountpoint, 'input', 'link1') + out_fn = os.path.join(mountpoint, 'archive', 'input', 'link1') sti = os.stat(in_fn, follow_symlinks=False) sto = os.stat(out_fn, follow_symlinks=False) assert sti.st_size == len('somewhere') @@ -2572,13 +2571,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert os.readlink(in_fn) == os.readlink(out_fn) # FIFO if are_fifos_supported(): - out_fn = os.path.join(mountpoint, 'input', 'fifo1') + out_fn = os.path.join(mountpoint, 'archive', 'input', 'fifo1') sto = os.stat(out_fn) assert stat.S_ISFIFO(sto.st_mode) # list/read xattrs try: in_fn = 'input/fusexattr' - out_fn = os.fsencode(os.path.join(mountpoint, 'input', 'fusexattr')) + out_fn = os.fsencode(os.path.join(mountpoint, 'archive', 'input', 'fusexattr')) if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path): assert sorted(no_selinux(xattr.listxattr(out_fn))) == [b'user.empty', b'user.foo', ] assert xattr.getxattr(out_fn, b'user.foo') == b'bar' @@ -2648,12 +2647,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'check', '--repair', exit_code=0) mountpoint = os.path.join(self.tmpdir, 'mountpoint') - with self.fuse_mount(self.repository_location, mountpoint, '--name=archive'): + with self.fuse_mount(self.repository_location, mountpoint, '-a', 'archive'): with pytest.raises(OSError) as excinfo: - open(os.path.join(mountpoint, path)) + open(os.path.join(mountpoint, 'archive', path)) assert excinfo.value.errno == errno.EIO - with self.fuse_mount(self.repository_location, mountpoint, '--name=archive', '-o', 'allow_damaged_files'): - open(os.path.join(mountpoint, path)).close() + with self.fuse_mount(self.repository_location, mountpoint, '-a', 'archive', '-o', 'allow_damaged_files'): + open(os.path.join(mountpoint, 'archive', path)).close() @unittest.skipUnless(llfuse, 'llfuse not installed') def test_fuse_mount_options(self): From 1bf8f71e69093c93347b096d3f1a981a4b1e418e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 21 Jun 2022 00:35:05 +0200 Subject: [PATCH 063/160] borg list ARCHIVE, borg rlist --- src/borg/archiver.py | 110 +++++++++++++---------- src/borg/testsuite/archiver.py | 155 ++++++++++++++++----------------- 2 files changed, 137 insertions(+), 128 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index c463f1809..37d7464b8 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1700,19 +1700,7 @@ class Archiver: @with_repository(compatibility=(Manifest.Operation.READ,)) def do_list(self, args, repository, manifest, key): - """List archive or repository contents""" - if args.name: - 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) - 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, repository, manifest, key) - - def _list_archive(self, args, repository, manifest, key): + """List archive contents""" matcher = self.build_matcher(args.patterns, args.paths) if args.format is not None: format = args.format @@ -1738,7 +1726,9 @@ class Archiver: return self.exit_code - def _list_repository(self, args, repository, manifest, key): + @with_repository(compatibility=(Manifest.Operation.READ,)) + def do_rlist(self, args, repository, manifest, key): + """List the archives contained in a repository""" if args.format is not None: format = args.format elif args.short: @@ -4686,7 +4676,7 @@ class Archiver: # borg list list_epilog = process_epilog(""" - This command lists the contents of a repository or an archive. + This command lists the contents of an archive. For more help on include/exclude patterns, see the :ref:`borg_patterns` command output. @@ -4701,35 +4691,20 @@ class Archiver: Examples: :: - $ borg list --format '{archive}{NL}' /path/to/repo - ArchiveFoo - ArchiveBar - ... - - # {VAR:NUMBER} - pad to NUMBER columns. - # Strings are left-aligned, numbers are right-aligned. - # Note: time columns except ``isomtime``, ``isoctime`` and ``isoatime`` cannot be padded. - $ borg list --format '{archive:36} {time} [{id}]{NL}' /path/to/repo - ArchiveFoo Thu, 2021-12-09 10:22:28 [0b8e9a312bef3f2f6e2d0fc110c196827786c15eba0188738e81697a7fa3b274] - $ borg list --format '{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}' /path/to/repo::ArchiveFoo + $ borg list --format '{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}' ArchiveFoo -rw-rw-r-- user user 1024 Thu, 2021-12-09 10:22:17 file-foo ... # {VAR:NUMBER} - pad to NUMBER columns right-aligned. - $ borg list --format '{mode} {user:>6} {group:>6} {size:<8} {mtime} {path}{extra}{NL}' /path/to/repo::ArchiveFoo + $ borg list --format '{mode} {user:>6} {group:>6} {size:<8} {mtime} {path}{extra}{NL}' ArchiveFoo -rw-rw-r-- user user 1024 Thu, 2021-12-09 10:22:17 file-foo ... The following keys are always available: - """) + BaseFormatter.keys_help() + textwrap.dedent(""" - Keys available only when listing archives in a repository: - - """) + ArchiveFormatter.keys_help() + textwrap.dedent(""" - Keys available only when listing files in an archive: """) + ItemFormatter.keys_help() @@ -4737,35 +4712,74 @@ class Archiver: description=self.do_list.__doc__, epilog=list_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help='list archive or repository contents') + help='list archive contents') subparser.set_defaults(func=self.do_list) - subparser.add_argument('--consider-checkpoints', action='store_true', dest='consider_checkpoints', - help='Show checkpoint archives in the repository contents list (default: hidden).') subparser.add_argument('--short', dest='short', action='store_true', help='only print file/directory names, nothing else') subparser.add_argument('--format', metavar='FORMAT', dest='format', - help='specify format for file or archive listing ' - '(default for files: "{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}"; ' - 'for archives: "{archive:<36} {time} [{id}]{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, ' - '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.') + help='specify format for file listing ' + '(default: "{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}")') subparser.add_argument('--json-lines', action='store_true', - help='Only valid for listing archive contents. Format output as JSON Lines. ' + help='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('--name', dest='name', metavar='NAME', type=NameSpec, + subparser.add_argument('name', metavar='NAME', type=NameSpec, help='specify the archive name') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to list; patterns are supported') - define_archive_filters_group(subparser) define_exclusion_group(subparser) + # borg rlist + rlist_epilog = process_epilog(""" + This command lists the archives contained in a repository. + .. man NOTES + The FORMAT specifier syntax + +++++++++++++++++++++++++++ + The ``--format`` option uses python's `format string syntax + `_. + Examples: + :: + $ borg rlist --format '{archive}{NL}' + ArchiveFoo + ArchiveBar + ... + + # {VAR:NUMBER} - pad to NUMBER columns. + # Strings are left-aligned, numbers are right-aligned. + # Note: time columns except ``isomtime``, ``isoctime`` and ``isoatime`` cannot be padded. + $ borg rlist --format '{archive:36} {time} [{id}]{NL}' /path/to/repo + ArchiveFoo Thu, 2021-12-09 10:22:28 [0b8e9a312bef3f2f6e2d0fc110c196827786c15eba0188738e81697a7fa3b274] + ... + + The following keys are always available: + + """) + BaseFormatter.keys_help() + textwrap.dedent(""" + Keys available only when listing archives in a repository: + + """) + ArchiveFormatter.keys_help() + subparser = subparsers.add_parser('rlist', parents=[common_parser], add_help=False, + description=self.do_rlist.__doc__, + epilog=rlist_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='list repository contents') + subparser.set_defaults(func=self.do_rlist) + subparser.add_argument('--consider-checkpoints', action='store_true', dest='consider_checkpoints', + help='Show checkpoint archives in the repository contents list (default: hidden).') + subparser.add_argument('--short', dest='short', action='store_true', + help='only print the archive names, nothing else') + subparser.add_argument('--format', metavar='FORMAT', dest='format', + help='specify format for archive listing ' + '(default: "{archive:<36} {time} [{id}]{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 "barchive" key is therefore not available.') + define_archive_filters_group(subparser) + subparser = subparsers.add_parser('mount', parents=[common_parser], add_help=False, description=self.do_mount.__doc__, epilog=mount_epilog, @@ -5280,7 +5294,7 @@ class Archiver: if not getattr(args, 'lock', True): # Option --bypass-lock sets args.lock = False bypass_allowed = {self.do_check, self.do_config, self.do_diff, self.do_export_tar, self.do_extract, self.do_info, - self.do_list, self.do_mount, self.do_umount} + self.do_list, self.do_rlist, self.do_mount, self.do_umount} if func not in bypass_allowed: raise Error('Not allowed to bypass locking mechanism for chosen command') if getattr(args, 'timestamp', None): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index a73580c1c..ce97c3fe7 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -231,9 +231,9 @@ def test_disk_full(cmd): # now some error happened, likely we are out of disk space. # free some space so we can expect borg to be able to work normally: shutil.rmtree(reserve, ignore_errors=True) - rc, out = cmd(f'--repo={repo}', 'list') + rc, out = cmd(f'--repo={repo}', 'rlist') if rc != EXIT_SUCCESS: - print('list', rc, out) + print('rlist', rc, out) rc, out = cmd(f'--repo={repo}', 'check', '--repair') if rc != EXIT_SUCCESS: print('check', rc, out) @@ -400,7 +400,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('This archive: ', output) with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', 'test') - list_output = self.cmd(f'--repo={self.repository_location}', 'list', '--short') + list_output = self.cmd(f'--repo={self.repository_location}', 'rlist', '--short') self.assert_in('test', list_output) self.assert_in('test.2', list_output) expected = [ @@ -427,7 +427,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # remove the file we did not backup, so input and output become equal expected.remove('input/flagfile') # this file is UF_NODUMP os.remove(os.path.join('input', 'flagfile')) - list_output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--short') + list_output = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--short') for name in expected: self.assert_in(name, list_output) self.assert_dirs_equal('input', 'output/input') @@ -465,7 +465,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', 'input') # give input twice! # test if created archive has 'input' contents twice: - archive_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines') + archive_list = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--json-lines') paths = [json.loads(line)['path'] for line in archive_list.split('\n') if line] # we have all fs items exactly once! assert sorted(paths) == ['input', 'input/a', 'input/a/hardlink', 'input/b', 'input/b/hardlink'] @@ -1096,7 +1096,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') input_data = b'\x00foo\n\nbar\n \n' self.cmd(f'--repo={self.repository_location}', 'create', 'test', '-', input=input_data) - item = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines')) + item = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--json-lines')) assert item['uid'] == 0 assert item['gid'] == 0 assert item['size'] == len(input_data) @@ -1110,7 +1110,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): name = 'a/b/c' self.cmd(f'--repo={self.repository_location}', 'create', '--stdin-name', name, '--content-from-command', 'test', '--', 'echo', input_data) - item = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines')) + item = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--json-lines')) assert item['uid'] == 0 assert item['gid'] == 0 assert item['size'] == len(input_data) + 1 # `echo` adds newline @@ -1123,7 +1123,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): output = self.cmd(f'--repo={self.repository_location}', 'create', '--content-from-command', 'test', '--', 'sh', '-c', 'exit 73;', exit_code=2) assert output.endswith("Command 'sh' exited with status 73\n") - archive_list = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--json')) + archive_list = json.loads(self.cmd(f'--repo={self.repository_location}', 'rlist', '--json')) assert archive_list['archives'] == [] def test_create_content_from_command_missing_command(self): @@ -1141,7 +1141,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): input_data = b'input/file1\0input/dir1\0input/file4' self.cmd(f'--repo={self.repository_location}', 'create', 'test', '--paths-from-stdin', '--paths-delimiter', '\\0', input=input_data) - archive_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines') + archive_list = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--json-lines') paths = [json.loads(line)['path'] for line in archive_list.split('\n') if line] assert paths == ['input/file1', 'input/dir1', 'input/file4'] @@ -1155,7 +1155,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): input_data = 'input/file1\ninput/file2\ninput/file3' self.cmd(f'--repo={self.repository_location}', 'create', '--paths-from-command', 'test', '--', 'echo', input_data) - archive_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines') + archive_list = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--json-lines') paths = [json.loads(line)['path'] for line in archive_list.split('\n') if line] assert paths == ['input/file1', 'input/file2', 'input/file3'] @@ -1164,7 +1164,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): output = self.cmd(f'--repo={self.repository_location}', 'create', '--paths-from-command', 'test', '--', 'sh', '-c', 'exit 73;', exit_code=2) assert output.endswith("Command 'sh' exited with status 73\n") - archive_list = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--json')) + archive_list = json.loads(self.cmd(f'--repo={self.repository_location}', 'rlist', '--json')) assert archive_list['archives'] == [] def test_create_paths_from_command_missing_command(self): @@ -1264,7 +1264,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # list the archive and verify that the "intermediate" folders appear before # their contents - out = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{type} {path}{NL}') + out = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--format', '{type} {path}{NL}') out_list = out.splitlines() self.assert_in('d x/a', out_list) @@ -1453,7 +1453,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with changedir('input/dir1/dir2'): self.cmd(f'--repo={self.repository_location}', 'create', 'test', '../../../input/dir1/../dir1/dir2/..') - output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') + output = self.cmd(f'--repo={self.repository_location}', 'list', 'test') self.assert_not_in('..', output) self.assert_in(' input/dir1/dir2/file', output) @@ -1618,7 +1618,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test1', 'test2') self.cmd(f'--repo={self.repository_location}', 'extract', 'test3', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test3') - assert not self.cmd(f'--repo={self.repository_location}', 'list') + assert not self.cmd(f'--repo={self.repository_location}', 'rlist') def test_delete_repo(self): self.create_regular_file('file1', size=1024 * 80) @@ -1650,7 +1650,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): output = self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test', '--force') self.assert_in('deleted archive was corrupted', output) self.cmd(f'--repo={self.repository_location}', 'check', '--repair') - output = self.cmd(f'--repo={self.repository_location}', 'list') + output = self.cmd(f'--repo={self.repository_location}', 'rlist') self.assert_not_in('test', output) def test_delete_double_force(self): @@ -1664,7 +1664,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): repository.commit(compact=False) self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test', '--force', '--force') self.cmd(f'--repo={self.repository_location}', 'check', '--repair') - output = self.cmd(f'--repo={self.repository_location}', 'list') + output = self.cmd(f'--repo={self.repository_location}', 'rlist') self.assert_not_in('test', output) def test_corrupted_repository(self): @@ -1764,14 +1764,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}', 'list', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'rlist', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd(f'--repo={self.repository_location}', 'list') + self.cmd(f'--repo={self.repository_location}', 'rlist') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd(f'--repo={self.repository_location}', 'list', '--bypass-lock') + self.cmd(f'--repo={self.repository_location}', 'rlist', '--bypass-lock') @unittest.skipUnless(llfuse, 'llfuse not installed') def test_readonly_mount(self): @@ -1847,7 +1847,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with changedir('output'): self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'extract', 'test']) - self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'list']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'rlist']) self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'info', '--name=test']) def test_unknown_feature_on_rename(self): @@ -2093,7 +2093,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.symlink('somewhere does not exist', os.path.join(self.input_path, 'link')) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', '--read-special', 'test', 'input') - output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') + output = self.cmd(f'--repo={self.repository_location}', 'list', 'test') assert 'input/link -> somewhere does not exist' in output # def test_cmdline_compatibility(self): @@ -2117,14 +2117,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert re.search(r'Keeping archive \(rule: daily #1\):\s+test2', output) # must keep the latest checkpoint archive: assert re.search(r'Keeping checkpoint archive:\s+test4.checkpoint', output) - output = self.cmd(f'--repo={self.repository_location}', 'list', '--consider-checkpoints') + output = self.cmd(f'--repo={self.repository_location}', 'rlist', '--consider-checkpoints') self.assert_in('test1', output) self.assert_in('test2', output) self.assert_in('test3.checkpoint', output) self.assert_in('test3.checkpoint.1', output) self.assert_in('test4.checkpoint', output) self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=1') - output = self.cmd(f'--repo={self.repository_location}', 'list', '--consider-checkpoints') + output = self.cmd(f'--repo={self.repository_location}', 'rlist', '--consider-checkpoints') self.assert_not_in('test1', output) # the latest non-checkpoint archive must be still there: self.assert_in('test2', output) @@ -2135,7 +2135,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # now we supersede the latest checkpoint by a successful backup: self.cmd(f'--repo={self.repository_location}', 'create', 'test5', src_dir) self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=2') - output = self.cmd(f'--repo={self.repository_location}', 'list', '--consider-checkpoints') + output = self.cmd(f'--repo={self.repository_location}', 'rlist', '--consider-checkpoints') # all checkpoints should be gone now: self.assert_not_in('checkpoint', output) # the latest archive must be still there @@ -2199,12 +2199,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert re.search(r'Keeping archive \(rule: monthly #' + str(i) + r'\):\s+test' + ("%02d" % (8-i)), output) for i in range(1, 15): assert re.search(r'Keeping archive \(rule: daily #' + str(i) + r'\):\s+test' + ("%02d" % (22-i)), output) - output = self.cmd(f'--repo={self.repository_location}', 'list') + output = self.cmd(f'--repo={self.repository_location}', 'rlist') # Nothing pruned after dry run for i in range(1, 25): self.assert_in('test%02d' % i, output) self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=14', '--keep-monthly=6', '--keep-yearly=1') - output = self.cmd(f'--repo={self.repository_location}', 'list') + output = self.cmd(f'--repo={self.repository_location}', 'rlist') # All matching backups plus oldest kept for i in range(1, 22): self.assert_in('test%02d' % i, output) @@ -2243,11 +2243,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1') assert re.search(r'Keeping archive \(rule: daily #1\):\s+test2', output) assert re.search(r'Would prune:\s+test1', output) - output = self.cmd(f'--repo={self.repository_location}', 'list') + output = self.cmd(f'--repo={self.repository_location}', 'rlist') self.assert_in('test1', output) self.assert_in('test2', output) self.cmd(f'--repo={self.repository_location}', 'prune', '--save-space', '--keep-daily=1') - output = self.cmd(f'--repo={self.repository_location}', 'list') + output = self.cmd(f'--repo={self.repository_location}', 'rlist') self.assert_not_in('test1', output) self.assert_in('test2', output) @@ -2260,13 +2260,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1', '--prefix=foo-') assert re.search(r'Keeping archive \(rule: daily #1\):\s+foo-2015-08-12-20:00', output) assert re.search(r'Would prune:\s+foo-2015-08-12-10:00', output) - output = self.cmd(f'--repo={self.repository_location}', 'list') + output = self.cmd(f'--repo={self.repository_location}', 'rlist') self.assert_in('foo-2015-08-12-10:00', output) self.assert_in('foo-2015-08-12-20:00', output) self.assert_in('bar-2015-08-12-10:00', output) self.assert_in('bar-2015-08-12-20:00', output) self.cmd(f'--repo={self.repository_location}', 'prune', '--keep-daily=1', '--prefix=foo-') - output = self.cmd(f'--repo={self.repository_location}', 'list') + output = self.cmd(f'--repo={self.repository_location}', 'rlist') self.assert_not_in('foo-2015-08-12-10:00', output) self.assert_in('foo-2015-08-12-20:00', output) self.assert_in('bar-2015-08-12-10:00', output) @@ -2281,13 +2281,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1', '--glob-archives=2015-*-foo') assert re.search(r'Keeping archive \(rule: daily #1\):\s+2015-08-12-20:00-foo', output) assert re.search(r'Would prune:\s+2015-08-12-10:00-foo', output) - output = self.cmd(f'--repo={self.repository_location}', 'list') + output = self.cmd(f'--repo={self.repository_location}', 'rlist') 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(f'--repo={self.repository_location}', 'prune', '--keep-daily=1', '--glob-archives=2015-*-foo') - output = self.cmd(f'--repo={self.repository_location}', 'list') + output = self.cmd(f'--repo={self.repository_location}', 'rlist') 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) @@ -2298,7 +2298,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test-1', src_dir) self.cmd(f'--repo={self.repository_location}', 'create', 'something-else-than-test-1', src_dir) self.cmd(f'--repo={self.repository_location}', 'create', 'test-2', src_dir) - output = self.cmd(f'--repo={self.repository_location}', 'list', '--prefix=test-') + output = self.cmd(f'--repo={self.repository_location}', 'rlist', '--prefix=test-') self.assert_in('test-1', output) self.assert_in('test-2', output) self.assert_not_in('something-else', output) @@ -2306,24 +2306,24 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_list_format(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', src_dir) - output_1 = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') - output_2 = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{mode} {user:6} {group:6} {size:8d} {mtime} {path}{extra}{NEWLINE}') - output_3 = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{mtime:%s} {path}{NL}') + output_1 = self.cmd(f'--repo={self.repository_location}', 'list', 'test') + output_2 = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--format', '{mode} {user:6} {group:6} {size:8d} {mtime} {path}{extra}{NEWLINE}') + output_3 = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--format', '{mtime:%s} {path}{NL}') self.assertEqual(output_1, output_2) self.assertNotEqual(output_1, output_3) - def test_list_repository_format(self): + def test_archives_format(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', '--comment', 'comment 1', 'test-1', src_dir) self.cmd(f'--repo={self.repository_location}', 'create', '--comment', 'comment 2', 'test-2', src_dir) - output_1 = self.cmd(f'--repo={self.repository_location}', 'list') - output_2 = self.cmd(f'--repo={self.repository_location}', 'list', '--format', '{archive:<36} {time} [{id}]{NL}') + output_1 = self.cmd(f'--repo={self.repository_location}', 'rlist') + output_2 = self.cmd(f'--repo={self.repository_location}', 'rlist', '--format', '{archive:<36} {time} [{id}]{NL}') self.assertEqual(output_1, output_2) - output_1 = self.cmd(f'--repo={self.repository_location}', 'list', '--short') + output_1 = self.cmd(f'--repo={self.repository_location}', 'rlist', '--short') self.assertEqual(output_1, 'test-1\ntest-2\n') - output_1 = self.cmd(f'--repo={self.repository_location}', 'list', '--format', '{barchive}/') + output_1 = self.cmd(f'--repo={self.repository_location}', 'rlist', '--format', '{barchive}/') self.assertEqual(output_1, 'test-1/test-2/') - output_3 = self.cmd(f'--repo={self.repository_location}', 'list', '--format', '{name} {comment}{NL}') + output_3 = self.cmd(f'--repo={self.repository_location}', 'rlist', '--format', '{name} {comment}{NL}') self.assert_in('test-1 comment 1\n', output_3) self.assert_in('test-2 comment 2\n', output_3) @@ -2332,7 +2332,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('amb', contents=b'a' * 1000000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{sha256} {path}{NL}') + output = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--format', '{sha256} {path}{NL}') assert "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0 input/amb" in output assert "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 input/empty_file" in output @@ -2342,11 +2342,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): # these are not really a checkpoints, but they look like some: self.cmd(f'--repo={self.repository_location}', 'create', 'test2.checkpoint', src_dir) self.cmd(f'--repo={self.repository_location}', 'create', 'test3.checkpoint.1', src_dir) - output = self.cmd(f'--repo={self.repository_location}', 'list') + output = self.cmd(f'--repo={self.repository_location}', 'rlist') assert "test1" in output assert "test2.checkpoint" not in output assert "test3.checkpoint.1" not in output - output = self.cmd(f'--repo={self.repository_location}', 'list', '--consider-checkpoints') + output = self.cmd(f'--repo={self.repository_location}', 'rlist', '--consider-checkpoints') assert "test1" in output assert "test2.checkpoint" in output assert "test3.checkpoint.1" in output @@ -2359,7 +2359,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(b'baab' * 2000000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{num_chunks} {unique_chunks} {path}{NL}') + output = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--format', '{num_chunks} {unique_chunks} {path}{NL}') assert "0 0 input/empty_file" in output assert "2 2 input/two_chunks" in output @@ -2367,7 +2367,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('compressible_file', size=10000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', '-C', 'lz4', 'test', 'input') - output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--format', '{size} {path}{NL}') + output = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--format', '{size} {path}{NL}') size, path = output.split("\n")[1].split(" ") assert int(size) == 10000 @@ -2375,7 +2375,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - list_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', '--json')) + list_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'rlist', '--json')) repository = list_repo['repository'] assert len(repository['id']) == 64 assert datetime.strptime(repository['last_modified'], ISO_FORMAT) # must not raise @@ -2384,7 +2384,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): archive0 = list_repo['archives'][0] assert datetime.strptime(archive0['time'], ISO_FORMAT) # must not raise - list_archive = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines') + list_archive = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--json-lines') items = [json.loads(s) for s in list_archive.splitlines()] assert len(items) == 2 file1 = items[1] @@ -2392,18 +2392,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert file1['size'] == 81920 assert datetime.strptime(file1['mtime'], ISO_FORMAT) # must not raise - list_archive = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', '--json-lines', '--format={sha256}') + list_archive = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--json-lines', '--format={sha256}') items = [json.loads(s) for s in list_archive.splitlines()] assert len(items) == 2 file1 = items[1] assert file1['path'] == 'input/file1' assert file1['sha256'] == 'b2915eb69f260d8d3c25249195f2c8f4f716ea82ec760ae929732c0262442b2b' - def test_list_json_args(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'list', '--json-lines', exit_code=2) - self.cmd(f'--repo={self.repository_location}', 'list', '--name=archive', '--json', exit_code=2) - def test_log_json(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -2448,7 +2443,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # here we have both BORG_PASSPHRASE and BORG_NEW_PASSPHRASE set: self.cmd(f'--repo={self.repository_location}', 'key', 'change-passphrase') os.environ['BORG_PASSPHRASE'] = 'newpassphrase' - self.cmd(f'--repo={self.repository_location}', 'list') + self.cmd(f'--repo={self.repository_location}', 'rlist') def test_change_location_to_keyfile(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -2919,15 +2914,15 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.check_cache() self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') self.check_cache() - original_archive = self.cmd(f'--repo={self.repository_location}', 'list') + original_archive = self.cmd(f'--repo={self.repository_location}', 'rlist') self.cmd(f'--repo={self.repository_location}', 'recreate', 'test0', 'input/dir2', '-e', 'input/dir2/file3', '--target=new-archive') self.check_cache() - archives = self.cmd(f'--repo={self.repository_location}', 'list') + archives = self.cmd(f'--repo={self.repository_location}', 'rlist') assert original_archive in archives assert 'new-archive' in archives - listing = self.cmd(f'--repo={self.repository_location}', 'list', '--name=new-archive', '--short') + listing = self.cmd(f'--repo={self.repository_location}', 'list', 'new-archive', '--short') assert 'file1' not in listing assert 'dir2/file2' in listing assert 'dir2/file3' not in listing @@ -2939,7 +2934,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') self.cmd(f'--repo={self.repository_location}', 'recreate', 'test0', 'input/dir2', '-e', 'input/dir2/file3') self.check_cache() - listing = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test0', '--short') + listing = self.cmd(f'--repo={self.repository_location}', 'list', 'test0', '--short') assert 'file1' not in listing assert 'dir2/file2' in listing assert 'dir2/file3' not in listing @@ -2968,7 +2963,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input', '--chunker-params', '7,9,8,128') self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input', '--files-cache=disabled') - list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test1', 'input/large_file', + list = self.cmd(f'--repo={self.repository_location}', 'list', 'test1', 'input/large_file', '--format', '{num_chunks} {unique_chunks}') num_chunks, unique_chunks = map(int, list.split(' ')) # test1 and test2 do not deduplicate @@ -2976,20 +2971,20 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'recreate', '--chunker-params', 'default') self.check_cache() # test1 and test2 do deduplicate after recreate - assert int(self.cmd(f'--repo={self.repository_location}', 'list', '--name=test1', 'input/large_file', '--format={size}')) - assert not int(self.cmd(f'--repo={self.repository_location}', 'list', '--name=test1', 'input/large_file', + assert int(self.cmd(f'--repo={self.repository_location}', 'list', 'test1', 'input/large_file', '--format={size}')) + assert not int(self.cmd(f'--repo={self.repository_location}', 'list', 'test1', 'input/large_file', '--format', '{unique_chunks}')) def test_recreate_recompress(self): self.create_regular_file('compressible', size=10000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', '-C', 'none') - file_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', 'input/compressible', + file_list = self.cmd(f'--repo={self.repository_location}', 'list', 'test', 'input/compressible', '--format', '{size} {sha256}') size, sha256_before = file_list.split(' ') self.cmd(f'--repo={self.repository_location}', 'recreate', '-C', 'lz4', '--recompress') self.check_cache() - file_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test', 'input/compressible', + file_list = self.cmd(f'--repo={self.repository_location}', 'list', 'test', 'input/compressible', '--format', '{size} {sha256}') size, sha256_after = file_list.split(' ') assert sha256_before == sha256_after @@ -3011,10 +3006,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('compressible', size=10000) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - archives_before = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') + archives_before = self.cmd(f'--repo={self.repository_location}', 'list', 'test') self.cmd(f'--repo={self.repository_location}', 'recreate', '-n', '-e', 'input/compressible') self.check_cache() - archives_after = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') + archives_after = self.cmd(f'--repo={self.repository_location}', 'list', 'test') assert archives_after == archives_before def test_recreate_skips_nothing_to_do(self): @@ -3519,12 +3514,12 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 # The repo should still be readable repo_info = self.cmd(f'--repo={self.repository_location}', 'info') assert 'All archives:' in repo_info - repo_list = self.cmd(f'--repo={self.repository_location}', 'list') + repo_list = self.cmd(f'--repo={self.repository_location}', 'rlist') assert 'test' in repo_list # The archive should still be readable archive_info = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') assert 'Archive name: test\n' in archive_info - archive_list = self.cmd(f'--repo={self.repository_location}', 'list', '--name=test') + archive_list = self.cmd(f'--repo={self.repository_location}', 'list', 'test') assert 'file1' in archive_list # Extracting the archive should work with changedir('output'): @@ -3697,7 +3692,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): output = self.cmd(f'--repo={self.repository_location}', 'check', '--repair', exit_code=0) self.assert_in('New missing file chunk detected', output) self.cmd(f'--repo={self.repository_location}', 'check', exit_code=0) - output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=archive1', '--format={health}#{path}{LF}', exit_code=0) + output = self.cmd(f'--repo={self.repository_location}', 'list', 'archive1', '--format={health}#{path}{LF}', 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'): @@ -3728,7 +3723,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): else: self.fail('should not happen') # list is also all-healthy again - output = self.cmd(f'--repo={self.repository_location}', 'list', '--name=archive1', '--format={health}#{path}{LF}', exit_code=0) + output = self.cmd(f'--repo={self.repository_location}', 'list', 'archive1', '--format={health}#{path}{LF}', exit_code=0) self.assert_not_in('broken#', output) def test_missing_archive_item_chunk(self): @@ -3811,7 +3806,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): repository.commit(compact=False) self.cmd(f'--repo={self.repository_location}', 'check', exit_code=1) self.cmd(f'--repo={self.repository_location}', 'check', '--repair', exit_code=0) - output = self.cmd(f'--repo={self.repository_location}', 'list') + output = self.cmd(f'--repo={self.repository_location}', 'rlist') self.assert_in('archive1', output) self.assert_in('archive1.1', output) self.assert_in('archive2', output) @@ -3887,7 +3882,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): repository.commit(compact=False) with pytest.raises(TAMRequiredError): - self.cmd(f'--repo={self.repository_location}', 'list') + self.cmd(f'--repo={self.repository_location}', 'rlist') def test_not_required(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -3903,23 +3898,23 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): del manifest['tam'] repository.put(Manifest.MANIFEST_ID, key.encrypt(Manifest.MANIFEST_ID, msgpack.packb(manifest))) repository.commit(compact=False) - output = self.cmd(f'--repo={self.repository_location}', 'list', '--debug') + output = self.cmd(f'--repo={self.repository_location}', 'rlist', '--debug') assert 'archive1234' in output assert 'TAM not found and not required' in output # Run upgrade self.cmd(f'--repo={self.repository_location}', 'upgrade', '--tam') # Manifest must be authenticated now - output = self.cmd(f'--repo={self.repository_location}', 'list', '--debug') + output = self.cmd(f'--repo={self.repository_location}', 'rlist', '--debug') 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(f'--repo={self.repository_location}', 'list') + self.cmd(f'--repo={self.repository_location}', 'rlist') # Force upgrade self.cmd(f'--repo={self.repository_location}', 'upgrade', '--tam', '--force') - self.cmd(f'--repo={self.repository_location}', 'list') + self.cmd(f'--repo={self.repository_location}', 'rlist') def test_disable(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -3927,7 +3922,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'upgrade', '--disable-tam') repository = Repository(self.repository_path, exclusive=True) self.spoof_manifest(repository) - assert not self.cmd(f'--repo={self.repository_location}', 'list') + assert not self.cmd(f'--repo={self.repository_location}', 'rlist') def test_disable2(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -3935,7 +3930,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): repository = Repository(self.repository_path, exclusive=True) self.spoof_manifest(repository) self.cmd(f'--repo={self.repository_location}', 'upgrade', '--disable-tam') - assert not self.cmd(f'--repo={self.repository_location}', 'list') + assert not self.cmd(f'--repo={self.repository_location}', 'rlist') class RemoteArchiverTestCase(ArchiverTestCase): @@ -4043,7 +4038,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): def test_chunks_archive(self): self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input') # Find ID of test1 so we can corrupt it later :) - target_id = self.cmd(f'--repo={self.repository_location}', 'list', '--format={id}{LF}').strip() + target_id = self.cmd(f'--repo={self.repository_location}', 'rlist', '--format={id}{LF}').strip() self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input') # Force cache sync, creating archive chunks of test1 and test2 in chunks.archive.d From 9e5a8a352f1256b792d76431a7a705eb0490e756 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 21 Jun 2022 01:04:41 +0200 Subject: [PATCH 064/160] borg info -a ARCH_GLOB, borg rinfo --- src/borg/archiver.py | 107 ++++++++++++++++++--------------- src/borg/testsuite/archiver.py | 96 ++++++++++++++--------------- 2 files changed, 108 insertions(+), 95 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 37d7464b8..a1ad72b67 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -1752,23 +1752,48 @@ class Archiver: return self.exit_code + @with_repository(cache=True, compatibility=(Manifest.Operation.READ,)) + def do_rinfo(self, args, repository, manifest, key, cache): + """Show repository infos""" + info = basic_json_data(manifest, cache=cache, extra={ + 'security_dir': cache.security_manager.dir, + }) + + if args.json: + json_print(info) + else: + encryption = 'Encrypted: ' + if key.NAME in ('plaintext', 'authenticated'): + encryption += 'No' + else: + encryption += 'Yes (%s)' % key.NAME + if key.NAME.startswith('key file'): + encryption += '\nKey file: %s' % key.find_key() + info['encryption'] = encryption + + print(textwrap.dedent(""" + Repository ID: {id} + Location: {location} + {encryption} + Cache: {cache.path} + Security dir: {security_dir} + """).strip().format( + id=bin_to_hex(repository.id), + location=repository._location.canonical_path(), + **info)) + print(DASHES) + print(STATS_HEADER) + print(str(cache)) + return self.exit_code + @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.name, args.first, args.last, args.prefix is not None, args.glob_archives)): - return self._info_archives(args, repository, manifest, key, cache) - else: - return self._info_repository(args, repository, manifest, key, 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.name: - archive_names = (args.name,) - else: - args.consider_checkpoints = True - archive_names = tuple(x.name for x in manifest.archives.list_considering(args)) + args.consider_checkpoints = True + archive_names = tuple(x.name for x in manifest.archives.list_considering(args)) output_data = [] @@ -1809,38 +1834,6 @@ class Archiver: })) return self.exit_code - 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: - json_print(info) - else: - encryption = 'Encrypted: ' - if key.NAME in ('plaintext', 'authenticated'): - encryption += 'No' - else: - encryption += 'Yes (%s)' % key.NAME - if key.NAME.startswith('key file'): - encryption += '\nKey file: %s' % key.find_key() - info['encryption'] = encryption - - print(textwrap.dedent(""" - Repository ID: {id} - Location: {location} - {encryption} - Cache: {cache.path} - Security dir: {security_dir} - """).strip().format( - id=bin_to_hex(repository.id), - location=repository._location.canonical_path(), - **info)) - print(DASHES) - print(STATS_HEADER) - print(str(cache)) - return self.exit_code - @with_repository(exclusive=True, compatibility=(Manifest.Operation.DELETE,)) def do_prune(self, args, repository, manifest, key): """Prune repository archives according to specified rules""" @@ -4335,9 +4328,31 @@ class Archiver: subparser.add_argument('topic', metavar='TOPIC', type=str, nargs='?', help='additional help on TOPIC') + # borg rinfo + rinfo_epilog = process_epilog(""" + This command displays detailed information about the repository. + + 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('rinfo', parents=[common_parser], add_help=False, + description=self.do_rinfo.__doc__, + epilog=rinfo_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='show repository information') + subparser.set_defaults(func=self.do_rinfo) + subparser.add_argument('--json', action='store_true', + help='format output as JSON') + # borg info info_epilog = process_epilog(""" - This command displays detailed information about the specified archive or repository. + This command displays 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 @@ -4359,8 +4374,6 @@ class Archiver: formatter_class=argparse.RawDescriptionHelpFormatter, help='show repository or archive information') subparser.set_defaults(func=self.do_info) - subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, - help='specify the archive name') subparser.add_argument('--json', action='store_true', help='format output as JSON') define_archive_filters_group(subparser) @@ -5293,7 +5306,7 @@ class Archiver: parser.error('Need at least one PATH argument.') if not getattr(args, 'lock', True): # Option --bypass-lock sets args.lock = False bypass_allowed = {self.do_check, self.do_config, self.do_diff, - self.do_export_tar, self.do_extract, self.do_info, + self.do_export_tar, self.do_extract, self.do_info, self.do_rinfo, self.do_list, self.do_rlist, self.do_mount, self.do_umount} if func not in bypass_allowed: raise Error('Not allowed to bypass locking mechanism for chosen command') diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index ce97c3fe7..c5b5c10a5 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -431,11 +431,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): for name in expected: self.assert_in(name, list_output) self.assert_dirs_equal('input', 'output/input') - info_output = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') + info_output = self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test') item_count = 5 if has_lchflags else 6 # one file is UF_NODUMP self.assert_in('Number of files: %d' % item_count, info_output) shutil.rmtree(self.cache_path) - info_output2 = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') + info_output2 = self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test') def filter(output): # filter for interesting "info" output, ignore cache rebuilding related stuff @@ -761,16 +761,16 @@ class ArchiverTestCase(ArchiverTestCaseBase): security_dir = self.get_security_dir() os.rename(self.repository_path, self.repository_path + '_new') with environment_variable(BORG_RELOCATED_REPO_ACCESS_IS_OK='yes'): - self.cmd(f'--repo={self.repository_location}_new', 'info') + self.cmd(f'--repo={self.repository_location}_new', 'rinfo') 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(f'--repo={self.repository_location}_new', 'info') + self.cmd(f'--repo={self.repository_location}_new', 'rinfo') shutil.rmtree(self.cache_path) - self.cmd(f'--repo={self.repository_location}_new', 'info') + self.cmd(f'--repo={self.repository_location}_new', 'rinfo') shutil.rmtree(security_dir) - self.cmd(f'--repo={self.repository_location}_new', 'info') + self.cmd(f'--repo={self.repository_location}_new', 'rinfo') for file in ('location', 'key-type', 'manifest-timestamp'): assert os.path.exists(os.path.join(security_dir, file)) @@ -780,27 +780,27 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}', 'info') + self.cmd(f'--repo={self.repository_location}', 'rinfo') def test_unknown_unencrypted(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') # Ok: repository is known - self.cmd(f'--repo={self.repository_location}', 'info') + self.cmd(f'--repo={self.repository_location}', 'rinfo') # Ok: repository is still known (through security_dir) shutil.rmtree(self.cache_path) - self.cmd(f'--repo={self.repository_location}', 'info') + self.cmd(f'--repo={self.repository_location}', 'rinfo') # Needs confirmation: cache and security dir both gone (eg. another host or rm -rf ~) shutil.rmtree(self.cache_path) shutil.rmtree(self.get_security_dir()) if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}', 'info', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'rinfo', exit_code=EXIT_ERROR) else: with pytest.raises(Cache.CacheInitAbortedError): - self.cmd(f'--repo={self.repository_location}', 'info') + self.cmd(f'--repo={self.repository_location}', 'rinfo') with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='yes'): - self.cmd(f'--repo={self.repository_location}', 'info') + self.cmd(f'--repo={self.repository_location}', 'rinfo') def test_strip_components(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -1280,13 +1280,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): create_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'create', '--no-cache-sync', '--json', '--error', 'test', 'input')) # ignore experimental warning - info_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--name=test', '--json')) + info_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test', '--json')) create_stats = create_json['cache']['stats'] info_stats = info_json['cache']['stats'] assert create_stats == info_stats self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') self.cmd(f'--repo={self.repository_location}', 'create', '--no-cache-sync', 'test2', 'input') - self.cmd(f'--repo={self.repository_location}', 'info') + self.cmd(f'--repo={self.repository_location}', 'rinfo') self.cmd(f'--repo={self.repository_location}', 'check') def test_extract_pattern_opt(self): @@ -1524,9 +1524,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - info_repo = self.cmd(f'--repo={self.repository_location}', 'info') + info_repo = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert 'All archives:' in info_repo - info_archive = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') + info_archive = self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test') assert 'Archive name: test\n' in info_archive info_archive = self.cmd(f'--repo={self.repository_location}', 'info', '--first', '1') assert 'Archive name: test\n' in info_archive @@ -1535,7 +1535,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - info_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--json')) + info_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'rinfo', '--json')) repository = info_repo['repository'] assert len(repository['id']) == 64 assert 'last_modified' in repository @@ -1547,7 +1547,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert all(isinstance(o, int) for o in stats.values()) assert all(key in stats for key in ('total_chunks', 'total_size', 'total_unique_chunks', 'unique_size')) - info_archive = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--name=test', '--json')) + info_archive = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test', '--json')) assert info_repo['repository'] == info_archive['repository'] assert info_repo['cache'] == info_archive['cache'] archives = info_archive['archives'] @@ -1576,17 +1576,17 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input', '--comment', 'this is the comment') self.cmd(f'--repo={self.repository_location}', 'create', 'test3', 'input', '--comment', '"deleted" comment') self.cmd(f'--repo={self.repository_location}', 'create', 'test4', 'input', '--comment', 'preserved comment') - assert 'Comment: \n' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test1') - assert 'Comment: this is the comment' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test2') + assert 'Comment: \n' in self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test1') + assert 'Comment: this is the comment' in self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test2') self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test1', '--comment', 'added comment') self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test2', '--comment', 'modified comment') self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test3', '--comment', '') self.cmd(f'--repo={self.repository_location}', 'recreate', '-a', 'test4', '12345') - assert 'Comment: added comment' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test1') - assert 'Comment: modified comment' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test2') - assert 'Comment: \n' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test3') - assert 'Comment: preserved comment' in self.cmd(f'--repo={self.repository_location}', 'info', '--name=test4') + assert 'Comment: added comment' in self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test1') + assert 'Comment: modified comment' in self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test2') + assert 'Comment: \n' in self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test3') + assert 'Comment: preserved comment' in self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test4') def test_delete(self): self.create_regular_file('file1', size=1024 * 80) @@ -1749,14 +1749,14 @@ class ArchiverTestCase(ArchiverTestCaseBase): with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}', 'info', exit_code=EXIT_ERROR) + self.cmd(f'--repo={self.repository_location}', 'rinfo', exit_code=EXIT_ERROR) else: with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - self.cmd(f'--repo={self.repository_location}', 'info') + self.cmd(f'--repo={self.repository_location}', 'rinfo') if isinstance(excinfo.value, RemoteRepository.RPCError): assert excinfo.value.exception_class == 'LockFailed' # verify that command works with read-only repo when using --bypass-lock - self.cmd(f'--repo={self.repository_location}', 'info', '--bypass-lock') + self.cmd(f'--repo={self.repository_location}', 'rinfo', '--bypass-lock') def test_readonly_list(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') @@ -1848,7 +1848,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'extract', 'test']) self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'rlist']) - self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'info', '--name=test']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'info', '-a', 'test']) def test_unknown_feature_on_rename(self): print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) @@ -2447,34 +2447,34 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_change_location_to_keyfile(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - log = self.cmd(f'--repo={self.repository_location}', 'info') + log = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert '(repokey)' in log self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'keyfile') - log = self.cmd(f'--repo={self.repository_location}', 'info') + log = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert '(key file)' in log def test_change_location_to_b2keyfile(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey-blake2') - log = self.cmd(f'--repo={self.repository_location}', 'info') + log = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert '(repokey BLAKE2b)' in log self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'keyfile') - log = self.cmd(f'--repo={self.repository_location}', 'info') + log = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert '(key file BLAKE2b)' in log def test_change_location_to_repokey(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=keyfile') - log = self.cmd(f'--repo={self.repository_location}', 'info') + log = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert '(key file)' in log self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'repokey') - log = self.cmd(f'--repo={self.repository_location}', 'info') + log = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert '(repokey)' in log def test_change_location_to_b2repokey(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=keyfile-blake2') - log = self.cmd(f'--repo={self.repository_location}', 'info') + log = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert '(key file BLAKE2b)' in log self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'repokey') - log = self.cmd(f'--repo={self.repository_location}', 'info') + log = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert '(repokey BLAKE2b)' in log def test_break_lock(self): @@ -2996,7 +2996,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') self.cmd(f'--repo={self.repository_location}', 'recreate', 'test0', '--timestamp', "1970-01-02T00:00:00", '--comment', 'test') - info = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test0').splitlines() + info = self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test0').splitlines() dtime = datetime(1970, 1, 2) + local_timezone.utcoffset(None) s_time = dtime.strftime("%Y-%m-%d") assert any([re.search(r'Time \(start\).+ %s' % s_time, item) for item in info]) @@ -3016,10 +3016,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') - info_before = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') + info_before = self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test') self.cmd(f'--repo={self.repository_location}', 'recreate', '--chunker-params', 'default') self.check_cache() - info_after = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') + info_after = self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test') assert info_before == info_after # includes archive ID def test_with_lock(self): @@ -3492,7 +3492,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_do_not_mention_archive_if_you_can_not_find_repo(self): """https://github.com/borgbackup/borg/issues/6014""" - output = self.cmd(f'--repo={self.repository_location}-this-repository-does-not-exist', 'info', '--name=test', + output = self.cmd(f'--repo={self.repository_location}-this-repository-does-not-exist', 'info', '-a', 'test', exit_code=2, fork=True) self.assert_in('this-repository-does-not-exist', output) self.assert_not_in('this-repository-does-not-exist::test', output) @@ -3512,12 +3512,12 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 os.remove(os.path.join(self.get_security_dir(), 'nonce')) # The repo should still be readable - repo_info = self.cmd(f'--repo={self.repository_location}', 'info') + repo_info = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert 'All archives:' in repo_info repo_list = self.cmd(f'--repo={self.repository_location}', 'rlist') assert 'test' in repo_list # The archive should still be readable - archive_info = self.cmd(f'--repo={self.repository_location}', 'info', '--name=test') + archive_info = self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test') assert 'Archive name: test\n' in archive_info archive_list = self.cmd(f'--repo={self.repository_location}', 'list', 'test') assert 'file1' in archive_list @@ -3597,7 +3597,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 with Repository(self.repository_path) as repository: _, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) assert key._encrypted_key_algorithm == 'argon2 chacha20-poly1305' - self.cmd(f'--repo={self.repository_location}', 'info') + self.cmd(f'--repo={self.repository_location}', 'rinfo') @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available') @@ -4009,7 +4009,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): super().setUp() self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cache_path = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--json'))['cache']['path'] + self.cache_path = json.loads(self.cmd(f'--repo={self.repository_location}', 'rinfo', '--json'))['cache']['path'] def corrupt(self, file, amount=1): with open(file, 'r+b') as fd: @@ -4022,11 +4022,11 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): self.corrupt(os.path.join(self.cache_path, 'chunks')) if self.FORK_DEFAULT: - out = self.cmd(f'--repo={self.repository_location}', 'info', exit_code=2) + out = self.cmd(f'--repo={self.repository_location}', 'rinfo', exit_code=2) assert 'failed integrity check' in out else: with pytest.raises(FileIntegrityError): - self.cmd(f'--repo={self.repository_location}', 'info') + self.cmd(f'--repo={self.repository_location}', 'rinfo') def test_cache_files(self): self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') @@ -4043,7 +4043,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): # Force cache sync, creating archive chunks of test1 and test2 in chunks.archive.d self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') - self.cmd(f'--repo={self.repository_location}', 'info', '--json') + self.cmd(f'--repo={self.repository_location}', 'rinfo', '--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 @@ -4074,7 +4074,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): with open(config_path, 'w') as fd: config.write(fd) - out = self.cmd(f'--repo={self.repository_location}', 'info') + out = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert 'Cache integrity data not available: old Borg version modified the cache.' in out From 34b6248d755784048353de256faf0119804fe184 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 21 Jun 2022 02:10:48 +0200 Subject: [PATCH 065/160] borg delete -a ARCH_GLOB, borg rdelete --- src/borg/archiver.py | 205 +++++++++++++++++---------------- src/borg/testsuite/archiver.py | 39 ++++--- 2 files changed, 125 insertions(+), 119 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index a1ad72b67..436aa8ddc 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -679,7 +679,7 @@ class Archiver: # now build files cache rc1 = self.do_create(self.parse_args([f'--repo={repo}', 'create', compression, 'borg-benchmark-crud2', path])) - rc2 = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '--name=borg-benchmark-crud2'])) + rc2 = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '-a', 'borg-benchmark-crud2'])) assert rc1 == rc2 == 0 # measure a no-change update (archive1 is still present) t_start = time.monotonic() @@ -687,7 +687,7 @@ class Archiver: 'borg-benchmark-crud3', path])) t_end = time.monotonic() dt_update = t_end - t_start - rc2 = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '--name=borg-benchmark-crud3'])) + rc2 = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '-a', 'borg-benchmark-crud3'])) assert rc1 == rc2 == 0 # measure extraction (dry-run: without writing result to disk) t_start = time.monotonic() @@ -698,7 +698,7 @@ class Archiver: assert rc == 0 # measure archive deletion (of LAST present archive with the data) t_start = time.monotonic() - rc = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '--name=borg-benchmark-crud1'])) + rc = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '-a', 'borg-benchmark-crud1'])) t_end = time.monotonic() dt_delete = t_end - t_start assert rc == 0 @@ -1515,35 +1515,80 @@ class Archiver: return self.exit_code @with_repository(exclusive=True, manifest=False) - def do_delete(self, args, repository): - """Delete an existing repository or archives""" - archive_filter_specified = any((args.first, args.last, args.prefix is not None, args.glob_archives)) - explicit_archives_specified = args.name or args.archives + def do_rdelete(self, args, repository): + """Delete a repository""" self.output_list = args.output_list - if archive_filter_specified and explicit_archives_specified: - self.print_error('Mixing archive filters and explicitly named archives is not supported.') - return self.exit_code - if archive_filter_specified or explicit_archives_specified: - return self._delete_archives(args, repository) - else: - return self._delete_repository(args, repository) - - def _delete_archives(self, args, repository): - """Delete archives""" dry_run = args.dry_run + keep_security_info = args.keep_security_info - manifest, key = Manifest.load(repository, (Manifest.Operation.DELETE,)) + if not args.cache_only: + if args.forced == 0: # without --force, we let the user see the archives list and confirm. + id = bin_to_hex(repository.id) + location = repository._location.canonical_path() + msg = [] + try: + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) + n_archives = len(manifest.archives) + msg.append(f"You requested to completely DELETE the following repository " + f"*including* {n_archives} archives it contains:") + except NoManifestError: + n_archives = None + msg.append("You requested to completely DELETE the following repository " + "*including* all archives it may contain:") - if args.name or args.archives: - archives = list(args.archives) - if args.name: - archives.insert(0, args.name) - archive_names = tuple(archives) + msg.append(DASHES) + msg.append(f"Repository ID: {id}") + msg.append(f"Location: {location}") + + if self.output_list: + msg.append("") + msg.append("Archives:") + + if n_archives is not None: + if n_archives > 0: + for archive_info in manifest.archives.list(sort_by=['ts']): + msg.append(format_archive(archive_info)) + else: + msg.append("This repository seems to not have any archives.") + else: + msg.append("This repository seems to have no manifest, so we can't " + "tell anything about its contents.") + + msg.append(DASHES) + 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 + if not dry_run: + repository.destroy() + logger.info("Repository deleted.") + if not keep_security_info: + SecurityManager.destroy(repository) + else: + logger.info("Would delete repository.") + logger.info("Would %s security info." % ("keep" if keep_security_info else "delete")) + if not dry_run: + Cache.destroy(repository) + logger.info("Cache deleted.") else: - args.consider_checkpoints = True - archive_names = tuple(x.name for x in manifest.archives.list_considering(args)) - if not archive_names: - return self.exit_code + logger.info("Would delete cache.") + return self.exit_code + + @with_repository(exclusive=True, manifest=False) + def do_delete(self, args, repository): + """Delete archives""" + self.output_list = args.output_list + dry_run = args.dry_run + manifest, key = Manifest.load(repository, (Manifest.Operation.DELETE,)) + archive_names = tuple(x.name for x in manifest.archives.list_considering(args)) + if not archive_names: + return self.exit_code + if args.glob_archives is None and args.first == 0 and args.last == 0: + self.print_error("Aborting: if you really want to delete all archives, please use -a '*' " + "or just delete the whole repository (might be much faster).") + return EXIT_ERROR if args.forced == 2: deleted = False @@ -1605,66 +1650,6 @@ class Archiver: return self.exit_code - def _delete_repository(self, args, repository): - """Delete a repository""" - dry_run = args.dry_run - keep_security_info = args.keep_security_info - - if not args.cache_only: - if args.forced == 0: # without --force, we let the user see the archives list and confirm. - id = bin_to_hex(repository.id) - location = repository._location.canonical_path() - msg = [] - try: - manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) - n_archives = len(manifest.archives) - msg.append(f"You requested to completely DELETE the following repository " - f"*including* {n_archives} archives it contains:") - except NoManifestError: - n_archives = None - msg.append("You requested to completely DELETE the following repository " - "*including* all archives it may contain:") - - msg.append(DASHES) - msg.append(f"Repository ID: {id}") - msg.append(f"Location: {location}") - - if self.output_list: - msg.append("") - msg.append("Archives:") - - if n_archives is not None: - if n_archives > 0: - for archive_info in manifest.archives.list(sort_by=['ts']): - msg.append(format_archive(archive_info)) - else: - msg.append("This repository seems to not have any archives.") - else: - msg.append("This repository seems to have no manifest, so we can't " - "tell anything about its contents.") - - msg.append(DASHES) - 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 - if not dry_run: - repository.destroy() - logger.info("Repository deleted.") - if not keep_security_info: - SecurityManager.destroy(repository) - else: - logger.info("Would delete repository.") - logger.info("Would %s security info." % ("keep" if keep_security_info else "delete")) - if not dry_run: - Cache.destroy(repository) - logger.info("Cache deleted.") - else: - logger.info("Would delete cache.") - return self.exit_code - 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. @@ -4062,18 +4047,42 @@ class Archiver: subparser.add_argument('output', metavar='OUTPUT', type=argparse.FileType('wb'), help='Output file') - # borg delete - delete_epilog = process_epilog(""" - This command deletes an archive from the repository or the complete repository. - - Important: When deleting archives, repository disk space is **not** freed until - you run ``borg compact``. + # borg rdelete + rdelete_epilog = process_epilog(""" + This command deletes the complete repository. When you delete a complete repository, the security info and local cache for it (if any) are also deleted. Alternatively, you can delete just the local cache with the ``--cache-only`` option, or keep the security info with the ``--keep-security-info`` option. + Always first use ``--dry-run --list`` to see what would be deleted. + """) + subparser = subparsers.add_parser('rdelete', parents=[common_parser], add_help=False, + description=self.do_rdelete.__doc__, + epilog=rdelete_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='delete repository') + subparser.set_defaults(func=self.do_rdelete) + subparser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true', + help='do not change repository') + subparser.add_argument('--list', dest='output_list', action='store_true', + help='output verbose list of archives') + 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.') + subparser.add_argument('--cache-only', dest='cache_only', action='store_true', + help='delete only the local cache for the given repository') + subparser.add_argument('--keep-security-info', dest='keep_security_info', action='store_true', + help='keep the local security info when deleting a repository') + + # borg delete + delete_epilog = process_epilog(""" + This command deletes archives from the repository. + + Important: When deleting archives, repository disk space is **not** freed until + you run ``borg compact``. + When in doubt, use ``--dry-run --list`` to see what would be deleted. When using ``--stats``, you will get some statistics about how much data was @@ -4087,9 +4096,7 @@ class Archiver: (for more info on these patterns, see :ref:`borg_patterns`). Note that these two options are mutually exclusive. - To avoid accidentally deleting archives, especially when using glob patterns, - it might be helpful to use the ``--dry-run`` to test out the command without - actually making any changes to the repository. + Always first use ``--dry-run --list`` to see what would be deleted. """) subparser = subparsers.add_parser('delete', parents=[common_parser], add_help=False, description=self.do_delete.__doc__, @@ -4101,6 +4108,8 @@ class Archiver: help='do not change repository') subparser.add_argument('--list', dest='output_list', action='store_true', help='output verbose list of archives') + subparser.add_argument('--consider-checkpoints', action='store_true', dest='consider_checkpoints', + help='consider checkpoint archives for deletion (default: not considered).') subparser.add_argument('-s', '--stats', dest='stats', action='store_true', help='print statistics for the deleted archive') subparser.add_argument('--cache-only', dest='cache_only', action='store_true', @@ -4112,10 +4121,6 @@ class Archiver: help='keep the local security info when deleting a repository') subparser.add_argument('--save-space', dest='save_space', action='store_true', help='work slower, but using less space') - subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec, - help='specify the archive name') - subparser.add_argument('archives', metavar='ARCHIVE', nargs='*', - help='archives to delete') define_archive_filters_group(subparser) # borg transfer diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index c5b5c10a5..11587efef 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -710,7 +710,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') self._set_repository_id(self.repository_path, repository_id) self.assert_equal(repository_id, self._extract_repository_id(self.repository_path)) - self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') + self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only') if self.FORK_DEFAULT: self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input', exit_code=EXIT_ERROR) else: @@ -723,8 +723,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.environ['BORG_PASSPHRASE'] = 'passphrase' self.cmd(f'--repo={self.repository_location}_encrypted', 'init', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}_encrypted', 'create', 'test', 'input') - self.cmd(f'--repo={self.repository_location}_unencrypted', 'delete', '--cache-only') - self.cmd(f'--repo={self.repository_location}_encrypted', 'delete', '--cache-only') + self.cmd(f'--repo={self.repository_location}_unencrypted', 'rdelete', '--cache-only') + self.cmd(f'--repo={self.repository_location}_encrypted', 'rdelete', '--cache-only') shutil.rmtree(self.repository_path + '_encrypted') os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted') if self.FORK_DEFAULT: @@ -744,7 +744,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with environment_variable(BORG_PASSPHRASE=''): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') # Delete cache & security database, AKA switch to user perspective - self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') + self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only') shutil.rmtree(self.get_security_dir()) with environment_variable(BORG_PASSPHRASE=None): # This is the part were the user would be tricked, e.g. she assumes that BORG_PASSPHRASE @@ -1276,7 +1276,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_no_cache_sync(self): self.create_test_files() self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') + self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only') create_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'create', '--no-cache-sync', '--json', '--error', 'test', 'input')) # ignore experimental warning @@ -1284,7 +1284,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): create_stats = create_json['cache']['stats'] info_stats = info_json['cache']['stats'] assert create_stats == info_stats - self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') + self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only') self.cmd(f'--repo={self.repository_location}', 'create', '--no-cache-sync', 'test2', 'input') self.cmd(f'--repo={self.repository_location}', 'rinfo') self.cmd(f'--repo={self.repository_location}', 'check') @@ -1601,9 +1601,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'extract', 'test.2', '--dry-run') self.cmd(f'--repo={self.repository_location}', 'delete', '--prefix', 'another_') self.cmd(f'--repo={self.repository_location}', 'delete', '--last', '1') - self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test') + self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test') self.cmd(f'--repo={self.repository_location}', 'extract', 'test.2', '--dry-run') - output = self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test.2', '--stats') + output = self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test.2', '--stats') self.assert_in('Deleted data:', output) # Make sure all data except the manifest has been deleted with Repository(self.repository_path) as repository: @@ -1615,9 +1615,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'test3', 'input') - self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test1', 'test2') + self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test1') + self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test2') self.cmd(f'--repo={self.repository_location}', 'extract', 'test3', '--dry-run') - self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test3') + self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test3') assert not self.cmd(f'--repo={self.repository_location}', 'rlist') def test_delete_repo(self): @@ -1627,10 +1628,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'no' - self.cmd(f'--repo={self.repository_location}', 'delete', exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'rdelete', exit_code=2) assert os.path.exists(self.repository_path) os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'YES' - self.cmd(f'--repo={self.repository_location}', 'delete') + self.cmd(f'--repo={self.repository_location}', 'rdelete') # Make sure the repo is gone self.assertFalse(os.path.exists(self.repository_path)) @@ -1647,7 +1648,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): else: assert False # missed the file repository.commit(compact=False) - output = self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test', '--force') + output = self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test', '--force') self.assert_in('deleted archive was corrupted', output) self.cmd(f'--repo={self.repository_location}', 'check', '--repair') output = self.cmd(f'--repo={self.repository_location}', 'rlist') @@ -1662,7 +1663,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): id = archive.metadata.items[0] repository.put(id, b'corrupted items metadata stream chunk') repository.commit(compact=False) - self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test', '--force', '--force') + self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test', '--force', '--force') self.cmd(f'--repo={self.repository_location}', 'check', '--repair') output = self.cmd(f'--repo={self.repository_location}', 'rlist') self.assert_not_in('test', output) @@ -1831,7 +1832,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_unknown_feature_on_cache_sync(self): self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') - self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') + self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only') self.add_unknown_feature(Manifest.Operation.READ) self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'create', 'test', 'input']) @@ -1861,10 +1862,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.add_unknown_feature(Manifest.Operation.DELETE) # delete of an archive raises - self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'delete', '--name=test']) + self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'delete', '-a', 'test']) self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'prune', '--keep-daily=3']) # delete of the whole repository ignores features - self.cmd(f'--repo={self.repository_location}', 'delete') + self.cmd(f'--repo={self.repository_location}', 'rdelete') @unittest.skipUnless(llfuse, 'llfuse not installed') def test_unknown_feature_on_mount(self): @@ -2784,7 +2785,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): verify_uniqueness() self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') verify_uniqueness() - self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test.2') + self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test.2') verify_uniqueness() def test_aes_counter_uniqueness_keyfile(self): @@ -4042,7 +4043,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input') # Force cache sync, creating archive chunks of test1 and test2 in chunks.archive.d - self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only') + self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only') self.cmd(f'--repo={self.repository_location}', 'rinfo', '--json') chunks_archive = os.path.join(self.cache_path, 'chunks.archive.d') From d00d650d8862d4ddf89b60f171be97174e461af7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 23 Jun 2022 09:16:29 +0200 Subject: [PATCH 066/160] borg init -> borg rcreate this is to complement borg rdelete, see also borg create / delete. --- setup_docs.py | 2 +- src/borg/archiver.py | 20 +- src/borg/testsuite/archiver.py | 408 ++++++++++++++++---------------- src/borg/testsuite/benchmark.py | 2 +- 4 files changed, 216 insertions(+), 216 deletions(-) diff --git a/setup_docs.py b/setup_docs.py index 056c23213..2f9dcb35d 100644 --- a/setup_docs.py +++ b/setup_docs.py @@ -280,7 +280,7 @@ class build_man(Command): 'recreate': ('patterns', 'placeholders', 'compression'), 'list': ('info', 'diff', 'prune', 'patterns'), 'info': ('list', 'diff'), - 'init': ('create', 'delete', 'check', 'list', 'key-import', 'key-export', 'key-change-passphrase'), + 'rcreate': ('rdelete', 'rlist', 'check', '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 diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 436aa8ddc..3273e8cb5 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -472,8 +472,8 @@ class Archiver: @with_repository(create=True, exclusive=True, manifest=False) @with_other_repository(key=True, compatibility=(Manifest.Operation.READ, )) - def do_init(self, args, repository, *, other_repository=None, other_key=None): - """Initialize an empty repository""" + def do_rcreate(self, args, repository, *, other_repository=None, other_key=None): + """Create a new, empty repository""" path = args.location.canonical_path() logger.info('Initializing repository at "%s"' % path) try: @@ -4383,9 +4383,9 @@ class Archiver: help='format output as JSON') define_archive_filters_group(subparser) - # borg init - init_epilog = process_epilog(""" - This command initializes an empty repository. A repository is a filesystem + # borg rcreate + rcreate_epilog = process_epilog(""" + This command creates a new, empty repository. A repository is a filesystem directory containing the deduplicated data from zero or more archives. Encryption mode TLDR @@ -4398,7 +4398,7 @@ class Archiver: :: - borg init --encryption repokey /path/to/repo + borg rcreate --encryption repokey /path/to/repo Borg will: @@ -4507,11 +4507,11 @@ class Archiver: Neither is inherently linked to the key derivation function, but since we were going to break backwards compatibility anyway we took the opportunity to fix all 3 issues at once. """) - subparser = subparsers.add_parser('init', parents=[common_parser], add_help=False, - description=self.do_init.__doc__, epilog=init_epilog, + subparser = subparsers.add_parser('rcreate', parents=[common_parser], add_help=False, + description=self.do_rcreate.__doc__, epilog=rcreate_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help='initialize empty repository') - subparser.set_defaults(func=self.do_init) + help='create a new, empty repository') + subparser.set_defaults(func=self.do_rcreate) subparser.add_argument('--other-repo', metavar='SRC_REPOSITORY', dest='other_location', type=location_validator(other=True), default=Location(other=True), help='reuse the key material from the other repository') diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 11587efef..ef22bf58b 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -147,7 +147,7 @@ def test_return_codes(cmd, tmpdir): input = tmpdir.mkdir('input') output = tmpdir.mkdir('output') input.join('test_file').write('content') - rc, out = cmd('--repo=%s' % str(repo), 'init', '--encryption=none') + rc, out = cmd('--repo=%s' % str(repo), 'rcreate', '--encryption=none') assert rc == EXIT_SUCCESS rc, out = cmd('--repo=%s' % repo, 'create', 'archive', str(input)) assert rc == EXIT_SUCCESS @@ -203,9 +203,9 @@ def test_disk_full(cmd): shutil.rmtree(input, ignore_errors=True) # keep some space and some inodes in reserve that we can free up later: make_files(reserve, 80, 100000, rnd=False) - rc, out = cmd(f'--repo={repo}', 'init') + rc, out = cmd(f'--repo={repo}', 'rcreate') if rc != EXIT_SUCCESS: - print('init', rc, out) + print('rcreate', rc, out) assert rc == EXIT_SUCCESS try: success, i = True, 0 @@ -391,7 +391,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(f'--repo={self.repository_location}', 'init', '--encryption=repokey', '--show-version', '--show-rc', fork=True) + output = self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey', '--show-version', '--show-rc', fork=True) self.assert_in('borgbackup version', output) self.assert_in('terminating with success status, rc 0', output) self.cmd(f'--repo={self.repository_location}', 'create', '--exclude-nodump', 'test', 'input') @@ -462,7 +462,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): hl_b = os.path.join(path_b, 'hardlink') self.create_regular_file(hl_a, contents=b'123456') os.link(hl_a, hl_b) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=none') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', 'input') # give input twice! # test if created archive has 'input' contents twice: archive_list = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--json-lines') @@ -476,13 +476,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): repository_location = self.prefix + repository_path with pytest.raises(Repository.ParentPathDoesNotExist): # normal borg init does NOT create missing parent dirs - self.cmd(f'--repo={repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={repository_location}', 'rcreate', '--encryption=none') # but if told so, it does: - self.cmd(f'--repo={repository_location}', 'init', '--encryption=none', '--make-parent-dirs') + self.cmd(f'--repo={repository_location}', 'rcreate', '--encryption=none', '--make-parent-dirs') assert os.path.exists(parent_path) def test_unix_socket(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.bind(os.path.join(self.input_path, 'unix-socket')) @@ -500,7 +500,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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', 'test') @@ -513,7 +513,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with changedir('input'): os.symlink('target', 'symlink1') os.link('symlink1', 'symlink2', follow_symlinks=False) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): output = self.cmd(f'--repo={self.repository_location}', 'extract', 'test') @@ -547,7 +547,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): atime, mtime = 123456780, 234567890 have_noatime = has_noatime('input/file1') os.utime('input/file1', (atime, mtime)) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', '--atime', 'test', 'input') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', 'test') @@ -567,7 +567,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): birthtime, mtime, atime = 946598400, 946684800, 946771200 os.utime('input/file1', (atime, birthtime)) os.utime('input/file1', (atime, mtime)) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', 'test') @@ -583,7 +583,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): birthtime, mtime, atime = 946598400, 946684800, 946771200 os.utime('input/file1', (atime, birthtime)) os.utime('input/file1', (atime, mtime)) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', '--nobirthtime') with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', 'test') @@ -644,7 +644,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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir(self.output_path): self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--sparse') @@ -663,7 +663,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): filename = os.path.join(self.input_path, filename) with open(filename, 'wb'): pass - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') for filename in filenames: with changedir('output'): @@ -673,11 +673,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_repository_swap_detection(self): self.create_test_files() os.environ['BORG_PASSPHRASE'] = 'passphrase' - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') repository_id = self._extract_repository_id(self.repository_path) self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') shutil.rmtree(self.repository_path) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=none') self._set_repository_id(self.repository_path, repository_id) self.assert_equal(repository_id, self._extract_repository_id(self.repository_path)) if self.FORK_DEFAULT: @@ -688,9 +688,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_repository_swap_detection2(self): self.create_test_files() - self.cmd(f'--repo={self.repository_location}_unencrypted', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}_unencrypted', 'rcreate', '--encryption=none') os.environ['BORG_PASSPHRASE'] = 'passphrase' - self.cmd(f'--repo={self.repository_location}_encrypted', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}_encrypted', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}_encrypted', 'create', 'test', 'input') shutil.rmtree(self.repository_path + '_encrypted') os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted') @@ -703,11 +703,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_repository_swap_detection_no_cache(self): self.create_test_files() os.environ['BORG_PASSPHRASE'] = 'passphrase' - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') repository_id = self._extract_repository_id(self.repository_path) self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') shutil.rmtree(self.repository_path) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=none') self._set_repository_id(self.repository_path, repository_id) self.assert_equal(repository_id, self._extract_repository_id(self.repository_path)) self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only') @@ -719,9 +719,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_repository_swap_detection2_no_cache(self): self.create_test_files() - self.cmd(f'--repo={self.repository_location}_unencrypted', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}_unencrypted', 'rcreate', '--encryption=none') os.environ['BORG_PASSPHRASE'] = 'passphrase' - self.cmd(f'--repo={self.repository_location}_encrypted', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}_encrypted', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}_encrypted', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}_unencrypted', 'rdelete', '--cache-only') self.cmd(f'--repo={self.repository_location}_encrypted', 'rdelete', '--cache-only') @@ -737,12 +737,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): # 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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', '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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') # Delete cache & security database, AKA switch to user perspective self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only') shutil.rmtree(self.get_security_dir()) @@ -757,7 +757,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') def test_repository_move(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') security_dir = self.get_security_dir() os.rename(self.repository_path, self.repository_path + '_new') with environment_variable(BORG_RELOCATED_REPO_ACCESS_IS_OK='yes'): @@ -775,7 +775,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert os.path.exists(os.path.join(security_dir, file)) def test_security_dir_compat(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') with open(os.path.join(self.get_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 @@ -783,7 +783,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'rinfo') def test_unknown_unencrypted(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=none') # Ok: repository is known self.cmd(f'--repo={self.repository_location}', 'rinfo') @@ -803,7 +803,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'rinfo') def test_strip_components(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('dir/file') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): @@ -832,7 +832,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.link(os.path.join(self.input_path, 'dir1/source2'), os.path.join(self.input_path, 'dir1/aaaa')) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') @requires_hardlinks @@ -909,7 +909,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): hl_b = os.path.join(path_b, 'hardlink') self.create_regular_file(hl_a, contents=b'123456') os.link(hl_a, hl_b) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=none') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', 'input') # give input twice! # now test extraction with changedir('output'): @@ -922,7 +922,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert os.stat('input/b/hardlink').st_nlink == 2 def test_extract_include_exclude(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('file3', size=1024 * 80) @@ -939,7 +939,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3']) def test_extract_include_exclude_regex(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('file3', size=1024 * 80) @@ -972,7 +972,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_equal(sorted(os.listdir('output/input')), ['file3']) def test_extract_include_exclude_regex_from_file(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('file3', size=1024 * 80) @@ -1012,7 +1012,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_equal(sorted(os.listdir('output/input')), ['file3']) def test_extract_with_pattern(self): - self.cmd(f'--repo={self.repository_location}', "init", '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file("file1", size=1024 * 80) self.create_regular_file("file2", size=1024 * 80) self.create_regular_file("file3", size=1024 * 80) @@ -1045,7 +1045,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"]) def test_extract_list_output(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('file', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') @@ -1070,7 +1070,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in("input/file", output) def test_extract_progress(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('file', size=1024 * 80) self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') @@ -1079,7 +1079,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert 'Extracting:' in output def _create_test_caches(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('cache1/%s' % CACHE_TAG_NAME, contents=CACHE_TAG_CONTENTS + b' extra stuff') @@ -1093,7 +1093,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): contents=CACHE_TAG_CONTENTS + b' extra stuff') def test_create_stdin(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') input_data = b'\x00foo\n\nbar\n \n' self.cmd(f'--repo={self.repository_location}', 'create', 'test', '-', input=input_data) item = json.loads(self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--json-lines')) @@ -1105,7 +1105,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert extracted_data == input_data def test_create_content_from_command(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') input_data = 'some test content' name = 'a/b/c' self.cmd(f'--repo={self.repository_location}', 'create', '--stdin-name', name, '--content-from-command', @@ -1119,7 +1119,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert extracted_data == input_data + '\n' def test_create_content_from_command_with_failed_command(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'create', '--content-from-command', 'test', '--', 'sh', '-c', 'exit 73;', exit_code=2) assert output.endswith("Command 'sh' exited with status 73\n") @@ -1127,12 +1127,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert archive_list['archives'] == [] def test_create_content_from_command_missing_command(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'create', 'test', '--content-from-command', exit_code=2) assert output.endswith('No command given.\n') def test_create_paths_from_stdin(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file("file1", size=1024 * 80) self.create_regular_file("dir1/file2", size=1024 * 80) self.create_regular_file("dir1/file3", size=1024 * 80) @@ -1146,7 +1146,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert paths == ['input/file1', 'input/dir1', 'input/file4'] def test_create_paths_from_command(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file("file1", size=1024 * 80) self.create_regular_file("file2", size=1024 * 80) self.create_regular_file("file3", size=1024 * 80) @@ -1160,7 +1160,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert paths == ['input/file1', 'input/file2', 'input/file3'] def test_create_paths_from_command_with_failed_command(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'create', '--paths-from-command', 'test', '--', 'sh', '-c', 'exit 73;', exit_code=2) assert output.endswith("Command 'sh' exited with status 73\n") @@ -1168,18 +1168,18 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert archive_list['archives'] == [] def test_create_paths_from_command_missing_command(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'create', 'test', '--paths-from-command', exit_code=2) assert output.endswith('No command given.\n') def test_create_without_root(self): """test create without a root""" - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', exit_code=2) def test_create_pattern_root(self): """test create with only a root pattern""" - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) output = self.cmd(f'--repo={self.repository_location}', 'create', 'test', '-v', '--list', '--pattern=R input') @@ -1188,7 +1188,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_pattern(self): """test file patterns during create""" - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') 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) @@ -1201,7 +1201,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_pattern_file(self): """test file patterns during create""" - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) self.create_regular_file('otherfile', size=1024 * 80) @@ -1220,7 +1220,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with open(self.patterns_file_path2, 'wb') as fd: fd.write(b'+ input/x/b\n- input/x*\n') - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') 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) @@ -1237,7 +1237,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with open(self.patterns_file_path2, 'wb') as fd: fd.write(b'+ input/x/b\n! input/x*\n') - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') 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) @@ -1254,7 +1254,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('x/a/foo_a', size=1024 * 80) self.create_regular_file('x/b/foo_b', size=1024 * 80) @@ -1275,7 +1275,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_no_cache_sync(self): self.create_test_files() - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only') create_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'create', '--no-cache-sync', '--json', '--error', @@ -1290,7 +1290,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'check') def test_extract_pattern_opt(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') 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) @@ -1318,7 +1318,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self._assert_test_caches() def _create_test_tagged(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('tagged1/.NOBACKUP') self.create_regular_file('tagged2/00-NOBACKUP') @@ -1343,7 +1343,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self._assert_test_tagged() def _create_test_keep_tagged(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('file0', size=1024) self.create_regular_file('tagged1/.NOBACKUP1') self.create_regular_file('tagged1/file1', size=1024) @@ -1383,7 +1383,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported') def test_recreate_hardlinked_tags(self): # test for issue #4911 - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=none') self.create_regular_file('file1', contents=CACHE_TAG_CONTENTS) # "wrong" filename, but correct tag contents os.mkdir(os.path.join(self.input_path, 'subdir')) # to make sure the tag is encountered *after* file1 os.link(os.path.join(self.input_path, 'file1'), @@ -1409,7 +1409,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(b'input/file', b'security.capability', capabilities) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): with patch.object(os, 'fchown', patched_fchown): @@ -1430,7 +1430,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file') xattr.setxattr(b'input/file', b'user.attribute', b'value') - self.cmd(f'--repo={self.repository_location}', 'init', '-e' 'none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '-e' 'none') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): input_abspath = os.path.abspath('input/file') @@ -1448,7 +1448,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert os.path.isfile(input_abspath) def test_path_normalization(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('dir1/dir2/file', size=1024 * 80) with changedir('input/dir1/dir2'): self.cmd(f'--repo={self.repository_location}', 'create', 'test', @@ -1458,7 +1458,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in(' input/dir1/dir2/file', output) def test_exclude_normalization(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('file2', size=1024 * 80) with changedir('input'): @@ -1478,13 +1478,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_repeated_files(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', '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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') # Overwriting regular files and directories should be supported os.mkdir('output/input') @@ -1503,7 +1503,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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--dry-run') @@ -1522,7 +1522,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_info(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') info_repo = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert 'All archives:' in info_repo @@ -1533,7 +1533,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_info_json(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') info_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'rinfo', '--json')) repository = info_repo['repository'] @@ -1563,7 +1563,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_info_json_of_empty_archive(self): """See https://github.com/borgbackup/borg/issues/6120""" - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') info_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--json', '--first=1')) assert info_repo["archives"] == [] info_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'info', '--json', '--last=1')) @@ -1571,7 +1571,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_comment(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input', '--comment', 'this is the comment') self.cmd(f'--repo={self.repository_location}', 'create', 'test3', 'input', '--comment', '"deleted" comment') @@ -1591,7 +1591,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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'test.3', 'input') @@ -1611,7 +1611,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_delete_multiple(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'test3', 'input') @@ -1624,7 +1624,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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input') os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'no' @@ -1636,7 +1636,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assertFalse(os.path.exists(self.repository_path)) def test_delete_force(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=none') self.create_src_archive('test') with Repository(self.repository_path, exclusive=True) as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -1655,7 +1655,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_not_in('test', output) def test_delete_double_force(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=none') self.create_src_archive('test') with Repository(self.repository_path, exclusive=True) as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -1669,7 +1669,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_not_in('test', output) def test_corrupted_repository(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('test') self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--dry-run') output = self.cmd(f'--repo={self.repository_location}', 'check', '--show-version') @@ -1684,7 +1684,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('Starting repository check', output) # --info given for root logger def test_readonly_check(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('test') with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo @@ -1699,7 +1699,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'check', '--verify-data', '--bypass-lock') def test_readonly_diff(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('a') self.create_src_archive('b') with self.read_only(self.repository_path): @@ -1715,7 +1715,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'diff', 'a', 'b', '--bypass-lock') def test_readonly_export_tar(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('test') with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo @@ -1730,7 +1730,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'export-tar', 'test', 'test.tar', '--bypass-lock') def test_readonly_extract(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('test') with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo @@ -1745,7 +1745,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'extract', 'test', '--bypass-lock') def test_readonly_info(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('test') with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo @@ -1760,7 +1760,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'rinfo', '--bypass-lock') def test_readonly_list(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('test') with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo @@ -1776,7 +1776,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(llfuse, 'llfuse not installed') def test_readonly_mount(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('test') with self.read_only(self.repository_path): # verify that command normally doesn't work with read-only repo @@ -1797,13 +1797,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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', '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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', '--dry-run', 'test', 'input') # Make sure no archive has been created with Repository(self.repository_path) as repository: @@ -1826,23 +1826,23 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert excinfo.value.args == (['unknown-feature'],) def test_unknown_feature_on_create(self): - print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) + print(self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey')) self.add_unknown_feature(Manifest.Operation.WRITE) self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'create', 'test', 'input']) def test_unknown_feature_on_cache_sync(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only') self.add_unknown_feature(Manifest.Operation.READ) self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'create', 'test', 'input']) def test_unknown_feature_on_change_passphrase(self): - print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) + print(self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey')) self.add_unknown_feature(Manifest.Operation.CHECK) self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'key', 'change-passphrase']) def test_unknown_feature_on_read(self): - print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) + print(self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey')) self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.add_unknown_feature(Manifest.Operation.READ) with changedir('output'): @@ -1852,13 +1852,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'info', '-a', 'test']) def test_unknown_feature_on_rename(self): - print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) + print(self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey')) self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.add_unknown_feature(Manifest.Operation.CHECK) self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'rename', 'test', 'other']) def test_unknown_feature_on_delete(self): - print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) + print(self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey')) self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.add_unknown_feature(Manifest.Operation.DELETE) # delete of an archive raises @@ -1869,7 +1869,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(llfuse, 'llfuse not installed') def test_unknown_feature_on_mount(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.add_unknown_feature(Manifest.Operation.READ) mountpoint = os.path.join(self.tmpdir, 'mountpoint') @@ -1884,7 +1884,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): else: path_prefix = '' - print(self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')) + print(self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey')) with Repository(self.repository_path, exclusive=True) as repository: if path_prefix: @@ -1920,13 +1920,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_progress_on(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'create', 'test4', 'input', '--progress') self.assert_in("\r", output) def test_progress_off(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'create', 'test5', 'input') self.assert_not_in("\r", output) @@ -1937,7 +1937,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'create', '--list', 'test', 'input') self.assert_in("A input/file1", output) self.assert_in("A input/file2", output) @@ -1953,7 +1953,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', contents=b'123') time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=10) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input', '--list', '--files-cache=ctime,size') # modify file1, but cheat with the mtime (and atime) and also keep same size: @@ -1970,7 +1970,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=10) time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=10) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'create', '--list', '--files-cache=mtime,size', 'test1', 'input') # change mode of file1, no content change: @@ -1986,7 +1986,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=10) time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=10) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'create', '--list', '--files-cache=rechunk,ctime', 'test1', 'input') # no changes here, but this mode rechunks unconditionally @@ -2003,7 +2003,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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'create', '--list', '--exclude-nodump', 'test', 'input') self.assert_in("A input/file1", output) self.assert_in("A input/file2", output) @@ -2019,7 +2019,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_json(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') create_info = json.loads(self.cmd(f'--repo={self.repository_location}', 'create', '--json', 'test', 'input')) # The usual keys @@ -2039,7 +2039,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_regular_file('file1', size=1024 * 80) time.sleep(1) # file2 must have newer timestamps than file1 self.create_regular_file('file2', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') # no listing by default output = self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.assert_not_in('file1', output) @@ -2069,7 +2069,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): finally: os.close(fd) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') data = b'foobar' * 1000 fifo_fn = os.path.join(self.input_path, 'fifo') @@ -2092,20 +2092,20 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_create_read_special_broken_symlink(self): os.symlink('somewhere does not exist', os.path.join(self.input_path, 'link')) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', '--read-special', 'test', 'input') output = self.cmd(f'--repo={self.repository_location}', 'list', 'test') assert 'input/link -> somewhere does not exist' in output # def test_cmdline_compatibility(self): # self.create_regular_file('file1', size=1024 * 80) - # self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + # self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') # self.cmd(f'--repo={self.repository_location}', 'create', '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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test1', src_dir) self.cmd(f'--repo={self.repository_location}', 'create', 'test2', src_dir) # these are not really a checkpoints, but they look like some: @@ -2154,7 +2154,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # This test must match docs/misc/prune-example.txt def test_prune_repository_example(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') # Archives that will be kept, per the example # Oldest archive self._create_archive_ts('test01', 2015, 1, 1) @@ -2215,7 +2215,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # With an initial and daily backup, prune daily until oldest is replaced by a monthly backup def test_prune_retain_and_expire_oldest(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') # Initial backup self._create_archive_ts('original_archive', 2020, 9, 1, 11, 15) # Archive and prune daily for 30 days @@ -2238,7 +2238,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_not_in('original_archive', output) def test_prune_repository_save_space(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test1', src_dir) self.cmd(f'--repo={self.repository_location}', 'create', 'test2', src_dir) output = self.cmd(f'--repo={self.repository_location}', 'prune', '--list', '--dry-run', '--keep-daily=1') @@ -2253,7 +2253,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('test2', output) def test_prune_repository_prefix(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'foo-2015-08-12-10:00', src_dir) self.cmd(f'--repo={self.repository_location}', 'create', 'foo-2015-08-12-20:00', src_dir) self.cmd(f'--repo={self.repository_location}', 'create', 'bar-2015-08-12-10:00', src_dir) @@ -2274,7 +2274,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('bar-2015-08-12-20:00', output) def test_prune_repository_glob(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', '2015-08-12-10:00-foo', src_dir) self.cmd(f'--repo={self.repository_location}', 'create', '2015-08-12-20:00-foo', src_dir) self.cmd(f'--repo={self.repository_location}', 'create', '2015-08-12-10:00-bar', src_dir) @@ -2295,7 +2295,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('2015-08-12-20:00-bar', output) def test_list_prefix(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test-1', src_dir) self.cmd(f'--repo={self.repository_location}', 'create', 'something-else-than-test-1', src_dir) self.cmd(f'--repo={self.repository_location}', 'create', 'test-2', src_dir) @@ -2305,7 +2305,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_not_in('something-else', output) def test_list_format(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', src_dir) output_1 = self.cmd(f'--repo={self.repository_location}', 'list', 'test') output_2 = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--format', '{mode} {user:6} {group:6} {size:8d} {mtime} {path}{extra}{NEWLINE}') @@ -2314,7 +2314,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assertNotEqual(output_1, output_3) def test_archives_format(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', '--comment', 'comment 1', 'test-1', src_dir) self.cmd(f'--repo={self.repository_location}', 'create', '--comment', 'comment 2', 'test-2', src_dir) output_1 = self.cmd(f'--repo={self.repository_location}', 'rlist') @@ -2331,14 +2331,14 @@ 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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') output = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--format', '{sha256} {path}{NL}') assert "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0 input/amb" in output assert "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 input/empty_file" in output def test_list_consider_checkpoints(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test1', src_dir) # these are not really a checkpoints, but they look like some: self.cmd(f'--repo={self.repository_location}', 'create', 'test2.checkpoint', src_dir) @@ -2358,7 +2358,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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') output = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--format', '{num_chunks} {unique_chunks} {path}{NL}') assert "0 0 input/empty_file" in output @@ -2366,7 +2366,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_list_size(self): self.create_regular_file('compressible_file', size=10000) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', '-C', 'lz4', 'test', 'input') output = self.cmd(f'--repo={self.repository_location}', 'list', 'test', '--format', '{size} {path}{NL}') size, path = output.split("\n")[1].split(" ") @@ -2374,7 +2374,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_list_json(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') list_repo = json.loads(self.cmd(f'--repo={self.repository_location}', 'rlist', '--json')) repository = list_repo['repository'] @@ -2402,7 +2402,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_log_json(self): self.create_test_files() - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') log = self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', '--log-json', '--list', '--debug') messages = {} # type -> message, one of each kind for line in log.splitlines(): @@ -2420,7 +2420,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_debug_profile(self): self.create_test_files() - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', '--debug-profile=create.prof') self.cmd('debug', 'convert-profile', 'create.prof', 'create.pyprof') stats = pstats.Stats('create.pyprof') @@ -2434,12 +2434,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_common_options(self): self.create_test_files() - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') log = self.cmd(f'--repo={self.repository_location}', '--debug', 'create', 'test', 'input') assert 'security: read previous location' in log def test_change_passphrase(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') os.environ['BORG_NEW_PASSPHRASE'] = 'newpassphrase' # here we have both BORG_PASSPHRASE and BORG_NEW_PASSPHRASE set: self.cmd(f'--repo={self.repository_location}', 'key', 'change-passphrase') @@ -2447,7 +2447,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'rlist') def test_change_location_to_keyfile(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') log = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert '(repokey)' in log self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'keyfile') @@ -2455,7 +2455,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert '(key file)' in log def test_change_location_to_b2keyfile(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey-blake2') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey-blake2') log = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert '(repokey BLAKE2b)' in log self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'keyfile') @@ -2463,7 +2463,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert '(key file BLAKE2b)' in log def test_change_location_to_repokey(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=keyfile') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=keyfile') log = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert '(key file)' in log self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'repokey') @@ -2471,7 +2471,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert '(repokey)' in log def test_change_location_to_b2repokey(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=keyfile-blake2') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=keyfile-blake2') log = self.cmd(f'--repo={self.repository_location}', 'rinfo') assert '(key file BLAKE2b)' in log self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'repokey') @@ -2479,7 +2479,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert '(repokey BLAKE2b)' in log def test_break_lock(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'break-lock') def test_usage(self): @@ -2489,9 +2489,9 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_help(self): assert 'Borg' in self.cmd('help') assert 'patterns' in self.cmd('help', 'patterns') - assert 'Initialize' in self.cmd('help', 'init') - assert 'positional arguments' not in self.cmd('help', 'init', '--epilog-only') - assert 'This command initializes' not in self.cmd('help', 'init', '--usage-only') + assert 'creates a new, empty repository' in self.cmd('help', 'rcreate') + assert 'positional arguments' not in self.cmd('help', 'rcreate', '--epilog-only') + assert 'creates a new, empty repository' not in self.cmd('help', 'rcreate', '--usage-only') @unittest.skipUnless(llfuse, 'llfuse not installed') def test_fuse(self): @@ -2506,7 +2506,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): noatime_used = flags_noatime != flags_normal return noatime_used and atime_before == atime_after - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_test_files() have_noatime = has_noatime('input/file1') self.cmd(f'--repo={self.repository_location}', 'create', '--exclude-nodump', '--atime', 'archive', 'input') @@ -2595,7 +2595,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(llfuse, 'llfuse not installed') def test_fuse_versions_view(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('test', contents=b'first') if are_hardlinks_supported(): self.create_regular_file('hardlink1', contents=b'123456') @@ -2627,7 +2627,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(llfuse, 'llfuse not installed') def test_fuse_allow_damaged_files(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('archive') # Get rid of a chunk and repair it archive, repository = self.open_archive('archive') @@ -2652,7 +2652,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): @unittest.skipUnless(llfuse, 'llfuse not installed') def test_fuse_mount_options(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('arch11') self.create_src_archive('arch12') self.create_src_archive('arch21') @@ -2726,7 +2726,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): # Decorate borg.locking.Lock.migrate_lock = write_assert_data(borg.locking.Lock.migrate_lock) try: - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=none') self.create_src_archive('arch') mountpoint = os.path.join(self.tmpdir, 'mountpoint') # In order that the decoration is kept for the borg mount process, we must not spawn, but actually fork; @@ -2779,7 +2779,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.create_test_files() os.environ['BORG_PASSPHRASE'] = 'passphrase' - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=' + method) + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=' + method) verify_uniqueness() self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') verify_uniqueness() @@ -2796,7 +2796,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_debug_dump_archive_items(self): self.create_test_files() - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-archive-items', 'test') @@ -2806,7 +2806,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_debug_dump_repo_objs(self): self.create_test_files() - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-repo-objs') @@ -2815,7 +2815,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert 'Done.' in output def test_debug_put_get_delete_obj(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') data = b'some data' hexkey = sha256(data).hexdigest() self.create_regular_file('file', contents=data) @@ -2838,19 +2838,19 @@ class ArchiverTestCase(ArchiverTestCaseBase): raise EOFError with patch.object(FlexiKey, 'create', raise_eof): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey', exit_code=1) + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey', exit_code=1) assert not os.path.exists(self.repository_location) def test_init_requires_encryption_option(self): - self.cmd(f'--repo={self.repository_location}', 'init', exit_code=2) + self.cmd(f'--repo={self.repository_location}', 'rcreate', exit_code=2) def test_init_nested_repositories(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') if self.FORK_DEFAULT: - self.cmd(f'--repo={self.repository_location}/nested', 'init', '--encryption=repokey', exit_code=2) + self.cmd(f'--repo={self.repository_location}/nested', 'rcreate', '--encryption=repokey', exit_code=2) else: with pytest.raises(Repository.AlreadyExists): - self.cmd(f'--repo={self.repository_location}/nested', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}/nested', 'rcreate', '--encryption=repokey') def test_init_refuse_to_overwrite_keyfile(self): """BORG_KEY_FILE=something borg init should quit if "something" already exists. @@ -2858,10 +2858,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): See https://github.com/borgbackup/borg/pull/6046""" keyfile = os.path.join(self.tmpdir, 'keyfile') with environment_variable(BORG_KEY_FILE=keyfile): - self.cmd(f'--repo={self.repository_location}0', 'init', '--encryption=keyfile') + self.cmd(f'--repo={self.repository_location}0', 'rcreate', '--encryption=keyfile') with open(keyfile) as file: before = file.read() - arg = (f'--repo={self.repository_location}1', 'init', '--encryption=keyfile') + arg = (f'--repo={self.repository_location}1', 'rcreate', '--encryption=keyfile') if self.FORK_DEFAULT: self.cmd(*arg, exit_code=2) else: @@ -2893,7 +2893,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert id in seen def test_check_cache(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with self.open_repository() as repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -2905,13 +2905,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.check_cache() def test_recreate_target_rc(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'recreate', '--target=asdf', exit_code=2) assert 'Need to specify single archive' in output def test_recreate_target(self): self.create_test_files() - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.check_cache() self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') self.check_cache() @@ -2931,7 +2931,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_basic(self): self.create_test_files() self.create_regular_file('dir2/file3', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') self.cmd(f'--repo={self.repository_location}', 'recreate', 'test0', 'input/dir2', '-e', 'input/dir2/file3') self.check_cache() @@ -2961,7 +2961,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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input', '--chunker-params', '7,9,8,128') self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input', '--files-cache=disabled') list = self.cmd(f'--repo={self.repository_location}', 'list', 'test1', 'input/large_file', @@ -2978,7 +2978,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_recompress(self): self.create_regular_file('compressible', size=10000) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input', '-C', 'none') file_list = self.cmd(f'--repo={self.repository_location}', 'list', 'test', 'input/compressible', '--format', '{size} {sha256}') @@ -2993,7 +2993,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_timestamp(self): local_timezone = datetime.now(timezone(timedelta(0))).astimezone().tzinfo self.create_test_files() - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') self.cmd(f'--repo={self.repository_location}', 'recreate', 'test0', '--timestamp', "1970-01-02T00:00:00", '--comment', 'test') @@ -3005,7 +3005,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_dry_run(self): self.create_regular_file('compressible', size=10000) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') archives_before = self.cmd(f'--repo={self.repository_location}', 'list', 'test') self.cmd(f'--repo={self.repository_location}', 'recreate', '-n', '-e', 'input/compressible') @@ -3015,7 +3015,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_recreate_skips_nothing_to_do(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') info_before = self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test') self.cmd(f'--repo={self.repository_location}', 'recreate', '--chunker-params', 'default') @@ -3024,13 +3024,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert info_before == info_after # includes archive ID def test_with_lock(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') 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(f'--repo={self.repository_location}', 'with-lock', *cmd, fork=True, exit_code=42) def test_recreate_list_output(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('file1', size=0) self.create_regular_file('file2', size=0) self.create_regular_file('file3', size=0) @@ -3060,13 +3060,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_not_in("x input/file5", output) def test_bad_filters(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}', 'delete', '--first', '1', '--last', '1', fork=True, exit_code=2) def test_key_export_keyfile(self): export_file = self.output_path + '/exported' - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'keyfile') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption', 'keyfile') repo_id = self._extract_repository_id(self.repository_path) self.cmd(f'--repo={self.repository_location}', 'key', 'export', export_file) @@ -3092,7 +3092,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert key_contents2 == key_contents def test_key_import_keyfile_with_borg_key_file(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'keyfile') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption', 'keyfile') exported_key_file = os.path.join(self.output_path, 'exported') self.cmd(f'--repo={self.repository_location}', 'key', 'export', exported_key_file) @@ -3113,7 +3113,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_key_export_repokey(self): export_file = self.output_path + '/exported' - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption', 'repokey') repo_id = self._extract_repository_id(self.repository_path) self.cmd(f'--repo={self.repository_location}', 'key', 'export', export_file) @@ -3144,7 +3144,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): def test_key_export_qr(self): export_file = self.output_path + '/exported.html' - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption', 'repokey') repo_id = self._extract_repository_id(self.repository_path) self.cmd(f'--repo={self.repository_location}', 'key', 'export', '--qr-html', export_file) @@ -3159,13 +3159,13 @@ class ArchiverTestCase(ArchiverTestCaseBase): export_directory = self.output_path + '/exported' os.mkdir(export_directory) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption', 'repokey') self.cmd(f'--repo={self.repository_location}', 'key', 'export', export_directory, exit_code=EXIT_ERROR) def test_key_import_errors(self): export_file = self.output_path + '/exported' - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'keyfile') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption', 'keyfile') self.cmd(f'--repo={self.repository_location}', 'key', 'import', export_file, exit_code=EXIT_ERROR) @@ -3191,7 +3191,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239' export_file = self.output_path + '/exported' - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'keyfile') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption', 'keyfile') self._set_repository_id(self.repository_path, unhexlify(repo_id)) key_file = self.keys_path + '/' + os.listdir(self.keys_path)[0] @@ -3215,7 +3215,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_key_import_paperkey(self): repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239' - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption', 'keyfile') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption', 'keyfile') self._set_repository_id(self.repository_path, unhexlify(repo_id)) key_file = self.keys_path + '/' + os.listdir(self.keys_path)[0] @@ -3260,7 +3260,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_debug_dump_manifest(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') dump_file = self.output_path + '/dump' output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-manifest', dump_file) @@ -3275,7 +3275,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_debug_dump_archive(self): self.create_regular_file('file1', size=1024 * 80) - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') dump_file = self.output_path + '/dump' output = self.cmd(f'--repo={self.repository_location}', 'debug', 'dump-archive', 'test', dump_file) @@ -3288,7 +3288,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 assert '_items' in result def test_debug_refcount_obj(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'debug', 'refcount-obj', '0' * 64).strip() assert output == 'object 0000000000000000000000000000000000000000000000000000000000000000 not found [info from chunks cache].' @@ -3307,14 +3307,14 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 assert 'Python' in output def test_benchmark_crud(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') with environment_variable(_BORG_BENCHMARK_CRUD_TEST='YES'): self.cmd(f'--repo={self.repository_location}', 'benchmark', 'crud', self.input_path) def test_config(self): self.create_test_files() os.unlink('input/flagfile') - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') output = self.cmd(f'--repo={self.repository_location}', 'config', '--list') self.assert_in('[repository]', output) self.assert_in('version', output) @@ -3357,7 +3357,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_export_tar(self): self.create_test_files() os.unlink('input/flagfile') - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') self.cmd(f'--repo={self.repository_location}', 'export-tar', 'test', 'simple.tar', '--progress', '--tar-format=GNU') with changedir('output'): @@ -3372,7 +3372,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 pytest.skip('gzip is not installed') self.create_test_files() os.unlink('input/flagfile') - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') list = self.cmd(f'--repo={self.repository_location}', 'export-tar', 'test', 'simple.tar.gz', '--list', '--tar-format=GNU') @@ -3388,7 +3388,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 pytest.skip('gzip is not installed') self.create_test_files() os.unlink('input/flagfile') - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') list = self.cmd(f'--repo={self.repository_location}', 'export-tar', 'test', 'simple.tar', '--strip-components=1', '--list', '--tar-format=GNU') @@ -3427,7 +3427,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_import_tar(self, tar_format='PAX'): self.create_test_files(create_hardlinks=False) # hardlinks become separate files os.unlink('input/flagfile') - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=none') self.cmd(f'--repo={self.repository_location}', 'create', 'src', 'input') self.cmd(f'--repo={self.repository_location}', 'export-tar', 'src', 'simple.tar', f'--tar-format={tar_format}') self.cmd(f'--repo={self.repository_location}', 'import-tar', 'dst', 'simple.tar') @@ -3441,7 +3441,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 pytest.skip('gzip is not installed') self.create_test_files(create_hardlinks=False) # hardlinks become separate files os.unlink('input/flagfile') - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=none') self.cmd(f'--repo={self.repository_location}', 'create', 'src', 'input') self.cmd(f'--repo={self.repository_location}', 'export-tar', 'src', 'simple.tgz', f'--tar-format={tar_format}') self.cmd(f'--repo={self.repository_location}', 'import-tar', 'dst', 'simple.tgz') @@ -3451,7 +3451,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_roundtrip_pax_borg(self): self.create_test_files() - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=none') self.cmd(f'--repo={self.repository_location}', 'create', 'src', 'input') self.cmd(f'--repo={self.repository_location}', 'export-tar', 'src', 'simple.tar', '--tar-format=BORG') self.cmd(f'--repo={self.repository_location}', 'import-tar', 'dst', 'simple.tar') @@ -3469,7 +3469,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.create_regular_file('file') xattr.setxattr(b'input/file', b'user.attribute%p', b'value') - self.cmd('init', self.repository_location, '-e' 'none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '-e' 'none') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): with patch.object(xattr, 'setxattr', patched_setxattr_EACCES): @@ -3485,7 +3485,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 os.makedirs(os.path.join(self.input_path, 'dir%p')) xattr.setxattr(b'input/dir%p', b'user.attribute', b'value') - self.cmd(f'--repo={self.repository_location}', 'init', '-e' 'none') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '-e' 'none') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') with changedir('output'): with patch.object(xattr, 'setxattr', patched_setxattr_EACCES): @@ -3504,7 +3504,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 It should be possible to retrieve the data from an archive even if both the client and the server forget the nonce""" self.create_regular_file('file1', contents=b'Hello, borg') - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') # Oops! We have removed the repo-side memory of the nonce! # See https://github.com/borgbackup/borg/issues/5858 @@ -3535,7 +3535,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 repo. Otherwise we can just use our own copy of the nonce. """ self.create_regular_file('file1', contents=b'Hello, borg') - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') # Oops! We have removed the repo-side memory of the nonce! # See https://github.com/borgbackup/borg/issues/5858 @@ -3547,20 +3547,20 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 def test_init_defaults_to_argon2(self): """https://github.com/borgbackup/borg/issues/747#issuecomment-1076160401""" - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') with Repository(self.repository_path) as repository: key = msgpack.unpackb(a2b_base64(repository.load_key())) assert key['algorithm'] == 'argon2 chacha20-poly1305' def test_init_with_explicit_key_algorithm(self): """https://github.com/borgbackup/borg/issues/747#issuecomment-1076160401""" - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey', '--key-algorithm=pbkdf2') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey', '--key-algorithm=pbkdf2') with Repository(self.repository_path) as repository: key = msgpack.unpackb(a2b_base64(repository.load_key())) assert key['algorithm'] == 'sha256' def verify_change_passphrase_does_not_change_algorithm(self, given_algorithm, expected_algorithm): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey', '--key-algorithm', given_algorithm) + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey', '--key-algorithm', given_algorithm) os.environ['BORG_NEW_PASSPHRASE'] = 'newpassphrase' self.cmd(f'--repo={self.repository_location}', 'key', 'change-passphrase') @@ -3576,7 +3576,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.verify_change_passphrase_does_not_change_algorithm('pbkdf2', 'sha256') def verify_change_location_does_not_change_algorithm(self, given_algorithm, expected_algorithm): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=keyfile', '--key-algorithm', given_algorithm) + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=keyfile', '--key-algorithm', given_algorithm) self.cmd(f'--repo={self.repository_location}', 'key', 'change-location', 'repokey') @@ -3591,7 +3591,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 self.verify_change_location_does_not_change_algorithm('pbkdf2', 'sha256') def test_key_change_algorithm(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey', '--key-algorithm=pbkdf2') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey', '--key-algorithm=pbkdf2') self.cmd(f'--repo={self.repository_location}', 'key', 'change-algorithm', 'argon2') @@ -3650,7 +3650,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): def setUp(self): super().setUp() with patch.object(ChunkBuffer, 'BUFFER_SIZE', 10): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('archive1') self.create_src_archive('archive2') @@ -3825,7 +3825,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): def _test_verify_data(self, *init_args): shutil.rmtree(self.repository_path) - self.cmd(f'--repo={self.repository_location}', 'init', *init_args) + self.cmd(f'--repo={self.repository_location}', 'rcreate', *init_args) self.create_src_archive('archive1') archive, repository = self.open_archive('archive1') with repository: @@ -3871,7 +3871,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): repository.commit(compact=False) def test_fresh_init_tam_required(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') repository = Repository(self.repository_path, exclusive=True) with repository: manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -3886,7 +3886,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'rlist') def test_not_required(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('archive1234') repository = Repository(self.repository_path, exclusive=True) with repository: @@ -3918,7 +3918,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'rlist') def test_disable(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('archive1234') self.cmd(f'--repo={self.repository_location}', 'upgrade', '--disable-tam') repository = Repository(self.repository_path, exclusive=True) @@ -3926,7 +3926,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): assert not self.cmd(f'--repo={self.repository_location}', 'rlist') def test_disable2(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_src_archive('archive1234') repository = Repository(self.repository_path, exclusive=True) self.spoof_manifest(repository) @@ -3943,32 +3943,32 @@ 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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') # 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]): with pytest.raises(PathNotAllowed): - self.cmd(f'--repo={self.repository_location}_0', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}_0', 'rcreate', '--encryption=repokey') # restricted to a completely different path: with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo']): with pytest.raises(PathNotAllowed): - self.cmd(f'--repo={self.repository_location}_1', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}_1', 'rcreate', '--encryption=repokey') 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(f'--repo={self.repository_location}_2', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}_2', 'rcreate', '--encryption=repokey') # 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(f'--repo={self.repository_location}_3', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}_3', 'rcreate', '--encryption=repokey') 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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') 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(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') @unittest.skip('only works locally') def test_debug_put_get_delete_obj(self): @@ -3983,7 +3983,7 @@ class RemoteArchiverTestCase(ArchiverTestCase): pass def test_remote_repo_strip_components_doesnt_leak(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') 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") @@ -4009,7 +4009,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase): def setUp(self): super().setUp() self.create_test_files() - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cache_path = json.loads(self.cmd(f'--repo={self.repository_location}', 'rinfo', '--json'))['cache']['path'] def corrupt(self, file, amount=1): @@ -4103,7 +4103,7 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): os.link('input/file_removed', 'input/hardlink_removed') os.link('input/file_removed2', 'input/hardlink_target_removed') - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') # Create the first snapshot self.cmd(f'--repo={self.repository_location}', 'create', 'test0', 'input') @@ -4289,7 +4289,7 @@ class DiffArchiverTestCase(ArchiverTestCaseBase): do_json_asserts(self.cmd(f'--repo={self.repository_location}', 'diff', 'test0', 'test1a', '--json-lines'), True) def test_sort_option(self): - self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey') + self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.create_regular_file('a_file_removed', size=8) self.create_regular_file('f_file_removed', size=16) @@ -4350,7 +4350,7 @@ def test_get_args(): assert args.restrict_to_repositories == ['/r1', '/r2'] # trying to cheat - try to execute different subcommand args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ], - 'borg --repo=/ init --encryption=repokey') + 'borg --repo=/ rcreate --encryption=repokey') assert args.func == archiver.do_serve # Check that environment variables in the forced command don't cause issues. If the command diff --git a/src/borg/testsuite/benchmark.py b/src/borg/testsuite/benchmark.py index 0a9a4373a..7b8d93168 100644 --- a/src/borg/testsuite/benchmark.py +++ b/src/borg/testsuite/benchmark.py @@ -28,7 +28,7 @@ def repo_url(request, tmpdir, monkeypatch): @pytest.fixture(params=["none", "repokey"]) def repo(request, cmd, repo_url): - cmd(f'--repo={repo_url}', 'init', '--encryption', request.param) + cmd(f'--repo={repo_url}', 'rcreate', '--encryption', request.param) return repo_url From f578c20b22841838901603d144be45801fadd159 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 23 Jun 2022 09:50:48 +0200 Subject: [PATCH 067/160] fix benchmark tests --- src/borg/testsuite/benchmark.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/borg/testsuite/benchmark.py b/src/borg/testsuite/benchmark.py index 7b8d93168..b23a7d800 100644 --- a/src/borg/testsuite/benchmark.py +++ b/src/borg/testsuite/benchmark.py @@ -57,7 +57,7 @@ def testdata(request, tmpdir_factory): @pytest.fixture(params=['none', 'lz4']) def repo_archive(request, cmd, repo, testdata): archive = 'test' - cmd(f'--repo={repo}', 'create', f'{archive}', '--compression', request.param, testdata) + cmd(f'--repo={repo}', 'create', '--compression', request.param, archive, testdata) return repo, archive @@ -82,19 +82,19 @@ def test_extract(benchmark, cmd, repo_archive, tmpdir): def test_delete(benchmark, cmd, repo_archive): repo, archive = repo_archive - result, out = benchmark.pedantic(cmd, (f'--repo={repo}', 'delete', '--name', archive)) + result, out = benchmark.pedantic(cmd, (f'--repo={repo}', 'delete', '-a', archive)) assert result == 0 def test_list(benchmark, cmd, repo_archive): repo, archive = repo_archive - result, out = benchmark(cmd, f'--repo={repo}', 'list', '--name', archive) + result, out = benchmark(cmd, f'--repo={repo}', 'list', archive) assert result == 0 def test_info(benchmark, cmd, repo_archive): repo, archive = repo_archive - result, out = benchmark(cmd, f'--repo={repo}', 'info', '--name', archive) + result, out = benchmark(cmd, f'--repo={repo}', 'info', '-a', archive) assert result == 0 From 16b91a41ad996aa9c04be36484e178d8f2ab5eb5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 23 Jun 2022 12:08:30 +0200 Subject: [PATCH 068/160] fix accidental nesting of subparsers python 3.11-dev threw a warning that this is deprecated. --- src/borg/archiver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 5df8cc1be..f6a6bcc79 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -3242,6 +3242,8 @@ class Archiver: group.add_argument('--last', metavar='N', dest='last', default=0, type=positive_int_validator, help='consider last N archives after other filters were applied') + return filters_group + def define_borg_mount(parser): parser.set_defaults(func=self.do_mount) parser.add_argument('--consider-checkpoints', action='store_true', dest='consider_checkpoints', @@ -4982,8 +4984,7 @@ class Archiver: define_exclusion_group(subparser, tag_files=True) - archive_group = subparser.add_argument_group('Archive options') - define_archive_filters_group(archive_group) + archive_group = define_archive_filters_group(subparser) 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 ' From 31a081f6958aa89004c8c6ab95879ac55d14ff3a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 23 Jun 2022 14:13:19 +0200 Subject: [PATCH 069/160] simplify stats output also: - move stats related stuff to Statistics class - repo ops give repo / overall stats - archive ops give archive stats - adapt tests --- src/borg/archive.py | 17 ++++++-------- src/borg/archiver.py | 42 ++++++++++------------------------ src/borg/cache.py | 9 ++++---- src/borg/testsuite/archive.py | 14 ++++++++---- src/borg/testsuite/archiver.py | 7 +++--- 5 files changed, 36 insertions(+), 53 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index f9dd2e486..016544da6 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -84,10 +84,12 @@ class Statistics: stats.nfiles_parts = self.nfiles_parts + other.nfiles_parts return stats - summary = "{label:15} {stats.osize_fmt:>20s} {stats.usize_fmt:>20s}" - def __str__(self): - return self.summary.format(stats=self, label='This archive:') + return """\ +Number of files: {stats.nfiles} +Original size: {stats.osize_fmt} +Deduplicated size: {stats.usize_fmt} +""".format(stats=self) def __repr__(self): return "<{cls} object at {hash:#x} ({self.osize}, {self.usize})>".format( @@ -538,7 +540,6 @@ Archive fingerprint: {0.fpr} Time (start): {start} Time (end): {end} Duration: {0.duration} -Number of files: {0.stats.nfiles} Utilization of max. archive size: {csize_max:.0%} '''.format( self, @@ -2141,12 +2142,8 @@ class ArchiveRecreater: if self.stats: target.start = _start target.end = datetime.utcnow() - log_multi(DASHES, - str(target), - DASHES, - str(target.stats), - str(self.cache), - DASHES) + log_multi(str(target), + str(target.stats)) def matcher_add_tagged_dirs(self, archive): """Add excludes to the matcher created by exclude_cache and exclude_if_present.""" diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f6a6bcc79..8218ca1cf 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -993,13 +993,9 @@ class Archiver: 'archive': archive, })) else: - log_multi(DASHES, - str(archive), - DASHES, - STATS_HEADER, + log_multi(str(archive), str(archive.stats), - str(cache), - DASHES, logger=logging.getLogger('borg.output.stats')) + logger=logging.getLogger('borg.output.stats')) self.output_filter = args.output_filter self.output_list = args.output_list @@ -1643,11 +1639,8 @@ class Archiver: repository.commit(compact=False, save_space=args.save_space) 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')) + log_multi(str(stats), + logger=logging.getLogger('borg.output.stats')) return self.exit_code @@ -1767,8 +1760,6 @@ class Archiver: id=bin_to_hex(repository.id), location=repository._location.canonical_path(), **info)) - print(DASHES) - print(STATS_HEADER) print(str(cache)) return self.exit_code @@ -1801,14 +1792,12 @@ class Archiver: Time (start): {start} Time (end): {end} Duration: {duration} - Number of files: {stats[nfiles]} Command line: {command_line} Utilization of maximum supported archive size: {limits[max_archive_size]:.0%} - ------------------------------------------------------------------------------ - Original size Deduplicated size - This archive: {stats[original_size]:>20s} {stats[deduplicated_size]:>20s} - {cache} - """).strip().format(cache=cache, **info)) + Number of files: {stats[nfiles]} + Original size: {stats[original_size]} + Deduplicated size: {stats[deduplicated_size]} + """).strip().format(**info)) if self.exit_code: break if not args.json and len(archive_names) - i: @@ -1899,11 +1888,8 @@ class Archiver: repository.commit(compact=False, save_space=args.save_space) 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')) + log_multi(str(stats), + logger=logging.getLogger('borg.output.stats')) return self.exit_code @with_repository(fake=('tam', 'disable_tam'), invert_fake=True, manifest=False, exclusive=True) @@ -2066,13 +2052,9 @@ class Archiver: 'archive': archive, })) else: - log_multi(DASHES, - str(archive), - DASHES, - STATS_HEADER, + log_multi(str(archive), str(archive.stats), - str(archive.cache), - DASHES, logger=logging.getLogger('borg.output.stats')) + logger=logging.getLogger('borg.output.stats')) @with_repository(manifest=False, exclusive=True) def do_with_lock(self, args, repository): diff --git a/src/borg/cache.py b/src/borg/cache.py index 2c84b754e..5addd34e1 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -405,10 +405,11 @@ class Cache: class CacheStatsMixin: str_format = """\ -All archives: {0.total_size:>20s} {0.unique_size:>20s} - - Unique chunks Total chunks -Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" +Original size: {0.total_size} +Deduplicated size: {0.unique_size} +Unique chunks: {0.total_unique_chunks} +Total chunks: {0.total_chunks} +""" def __init__(self, iec=False): self.iec = iec diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index 6bfb933ab..cf19a5821 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -20,6 +20,7 @@ from ..platform import uid2user, gid2group def stats(): stats = Statistics() stats.update(20, unique=True) + stats.nfiles = 1 return stats @@ -35,26 +36,29 @@ def tests_stats_progress(stats, monkeypatch, columns=80): monkeypatch.setenv('COLUMNS', str(columns)) out = StringIO() stats.show_progress(stream=out) - s = '20 B O 20 B U 0 N ' + s = '20 B O 20 B U 1 N ' buf = ' ' * (columns - len(s)) assert out.getvalue() == s + buf + "\r" out = StringIO() stats.update(10 ** 3, unique=False) stats.show_progress(item=Item(path='foo'), final=False, stream=out) - s = '1.02 kB O 20 B U 0 N foo' + s = '1.02 kB O 20 B U 1 N foo' buf = ' ' * (columns - len(s)) assert out.getvalue() == s + buf + "\r" out = StringIO() stats.show_progress(item=Item(path='foo'*40), final=False, stream=out) - s = '1.02 kB O 20 B U 0 N foofoofoofoofoofoofoofoofo...foofoofoofoofoofoofoofoofoofoo' + s = '1.02 kB O 20 B U 1 N foofoofoofoofoofoofoofoofo...foofoofoofoofoofoofoofoofoofoo' buf = ' ' * (columns - len(s)) assert out.getvalue() == s + buf + "\r" def test_stats_format(stats): assert str(stats) == """\ -This archive: 20 B 20 B""" +Number of files: 1 +Original size: 20 B +Deduplicated size: 20 B +""" s = f"{stats.osize_fmt}" assert s == "20 B" # kind of redundant, but id is variable so we can't match reliably @@ -72,7 +76,7 @@ def test_stats_progress_json(stats): assert result['finished'] is False assert result['path'] == 'foo' assert result['original_size'] == 20 - assert result['nfiles'] == 0 # this counter gets updated elsewhere + assert result['nfiles'] == 1 out = StringIO() stats.show_progress(stream=out, final=True) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index ef22bf58b..a3907c2c9 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -397,7 +397,6 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'create', '--exclude-nodump', 'test', 'input') output = self.cmd(f'--repo={self.repository_location}', 'create', '--exclude-nodump', '--stats', 'test.2', 'input') self.assert_in('Archive name: test.2', output) - self.assert_in('This archive: ', output) with changedir('output'): self.cmd(f'--repo={self.repository_location}', 'extract', 'test') list_output = self.cmd(f'--repo={self.repository_location}', 'rlist', '--short') @@ -1525,7 +1524,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'rcreate', '--encryption=repokey') self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input') info_repo = self.cmd(f'--repo={self.repository_location}', 'rinfo') - assert 'All archives:' in info_repo + assert 'Original size:' in info_repo info_archive = self.cmd(f'--repo={self.repository_location}', 'info', '-a', 'test') assert 'Archive name: test\n' in info_archive info_archive = self.cmd(f'--repo={self.repository_location}', 'info', '--first', '1') @@ -1604,7 +1603,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test') self.cmd(f'--repo={self.repository_location}', 'extract', 'test.2', '--dry-run') output = self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test.2', '--stats') - self.assert_in('Deleted data:', output) + self.assert_in('Original size: -', output) # negative size == deleted data # Make sure all data except the manifest has been deleted with Repository(self.repository_path) as repository: self.assert_equal(len(repository), 1) @@ -3514,7 +3513,7 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 # The repo should still be readable repo_info = self.cmd(f'--repo={self.repository_location}', 'rinfo') - assert 'All archives:' in repo_info + assert 'Original size:' in repo_info repo_list = self.cmd(f'--repo={self.repository_location}', 'rlist') assert 'test' in repo_list # The archive should still be readable From 7f99aa155e301bcf3abd6ab190a9e86ce6475b14 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 24 Jun 2022 00:28:18 +0200 Subject: [PATCH 070/160] update CHANGES, split changelog by major version --- docs/changes.rst | 5228 +----------------------------------------- docs/changes_0.x.rst | 807 +++++++ docs/changes_1.x.rst | 4362 +++++++++++++++++++++++++++++++++++ docs/index.rst | 2 + 4 files changed, 5283 insertions(+), 5116 deletions(-) create mode 100644 docs/changes_0.x.rst create mode 100644 docs/changes_1.x.rst diff --git a/docs/changes.rst b/docs/changes.rst index a0ca5d4fb..e6ac6bc1d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,228 +1,127 @@ .. _important_notes: -Important notes -=============== +Important notes 2.x +=================== This section provides information about security and corruption issues. -.. _hashindex_set_bug: - -Pre-1.1.11 potential index corruption / data loss issue -------------------------------------------------------- - -A bug was discovered in our hashtable code, see issue #4829. -The code is used for the client-side chunks cache and the server-side repo index. - -Although borg uses the hashtables very heavily, the index corruption did not -happen too frequently, because it needed specific conditions to happen. - -Data loss required even more specific conditions, so it should be rare (and -also detectable via borg check). - -You might be affected if borg crashed with / complained about: - -- AssertionError: Corrupted segment reference count - corrupted index or hints -- ObjectNotFound: Object with key ... not found in repository ... -- Index mismatch for key b'...'. (..., ...) != (-1, -1) -- ValueError: stats_against: key contained in self but not in master_index. - -Advised procedure to fix any related issue in your indexes/caches: - -- install fixed borg code (on client AND server) -- for all of your clients and repos remove the cache by: - - borg delete --cache-only YOURREPO - - (later, the cache will be re-built automatically) -- for all your repos, rebuild the repo index by: - - borg check --repair YOURREPO - - This will also check all archives and detect if there is any data-loss issue. - -Affected branches / releases: - -- fd06497 introduced the bug into 1.1-maint branch - it affects all borg 1.1.x since 1.1.0b4. -- fd06497 introduced the bug into master branch - it affects all borg 1.2.0 alpha releases. -- c5cd882 introduced the bug into 1.0-maint branch - it affects all borg 1.0.x since 1.0.11rc1. - -The bug was fixed by: - -- 701159a fixes the bug in 1.1-maint branch - will be released with borg 1.1.11. -- fa63150 fixes the bug in master branch - will be released with borg 1.2.0a8. -- 7bb90b6 fixes the bug in 1.0-maint branch. Branch is EOL, no new release is planned as of now. - -.. _broken_validator: - -Pre-1.1.4 potential data corruption issue ------------------------------------------ - -A data corruption bug was discovered in borg check --repair, see issue #3444. - -This is a 1.1.x regression, releases < 1.1 (e.g. 1.0.x) are not affected. - -To avoid data loss, you must not run borg check --repair using an unfixed version -of borg 1.1.x. The first official release that has the fix is 1.1.4. - -Package maintainers may have applied the fix to updated packages of 1.1.x (x<4) -though, see the package maintainer's package changelog to make sure. - -If you never had missing item metadata chunks, the bug has not affected you -even if you did run borg check --repair with an unfixed version. - -When borg check --repair tried to repair corrupt archives that miss item metadata -chunks, the resync to valid metadata in still present item metadata chunks -malfunctioned. This was due to a broken validator that considered all (even valid) -item metadata as invalid. As they were considered invalid, borg discarded them. -Practically, that means the affected files, directories or other fs objects were -discarded from the archive. - -Due to the malfunction, the process was extremely slow, but if you let it -complete, borg would have created a "repaired" archive that has lost a lot of items. -If you interrupted borg check --repair because it was so strangely slow (killing -borg somehow, e.g. Ctrl-C) the transaction was rolled back and no corruption occurred. - -The log message indicating the precondition for the bug triggering looks like: - - item metadata chunk missing [chunk: 001056_bdee87d...a3e50d] - -If you never had that in your borg check --repair runs, you're not affected. - -But if you're unsure or you actually have seen that, better check your archives. -By just using "borg list repo::archive" you can see if all expected filesystem -items are listed. - -.. _tam_vuln: - -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 - -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 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-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: - -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 ------------------------------------ - -Some external errors (like network or disk I/O errors) could lead to -corruption of the backup repository due to issue #1138. - -A sign that this happened is if "E" status was reported for a file that can -not be explained by problems with the source file. If you still have logs from -"borg create -v --list", you can check for "E" status. - -Here is what could cause corruption and what you can do now: - -1) I/O errors (e.g. repo disk errors) while writing data to repo. - -This could lead to corrupted segment files. - -Fix:: - - # check for corrupt chunks / segments: - borg check -v --repository-only REPO - - # repair the repo: - borg check -v --repository-only --repair REPO - - # make sure everything is fixed: - borg check -v --repository-only REPO - -2) Unreliable network / unreliable connection to the repo. - -This could lead to archive metadata corruption. - -Fix:: - - # check for corrupt archives: - borg check -v --archives-only REPO - - # delete the corrupt archives: - borg delete --force REPO::CORRUPT_ARCHIVE - - # make sure everything is fixed: - borg check -v --archives-only REPO - -3) In case you want to do more intensive checking. - -The best check that everything is ok is to run a dry-run extraction:: - - borg extract -v --dry-run REPO::ARCHIVE +(nothing to see here yet) .. _changelog: -Change Log -========== +Change Log 2.x +============== + +Version 2.0.0a2 (not released yet) +---------------------------------- + +Please note: + +This is an alpha release, only for testing - do not use for production repos. + +Compatibility notes: + +- this is a major "breaking" release that is not compatible with existing repos. + + We tried to put all the necessary "breaking" changes into this release, so we + hopefully do not need another breaking release in the near future. The changes + were necessary for improved security, improved speed, unblocking future + improvements, getting rid of legacy crap / design limitations, having less and + simpler code to maintain. + + You can use "borg transfer" to transfer archives from borg 1.1/1.2 repos to + a new borg 2.0 repo, but it will need some time and space. + +- command line syntax was changed, scripts and wrappers will need changes: + + - you will usually either export BORG_REPO= into your environment or + call borg like: borg -r command (-r ... usually omitted for brevity) + - the scp-style REPO syntax was removed, please use ssh://..., #6697 + - differently than with borg 1.x you ONLY give the repo there, never a ::archive. + - the archive name is either given as a positional parameter, like: + + - borg create myarchive2 /some/path + - borg diff myarchive1 myarchive2 + - or, if the command makes sense for an arbitrary amount of archives, archives + can be selected using a glob pattern, like: + + - borg delete -a 'myarchive*' + - borg recreate -a 'myarchive*' + - some borg 1.x commands that supported working on a repo AND on an archive + were split into 2 commands, some others were renamed: + + - borg 2 repo commands: + + - borg rcreate # "repo create", was: borg init + - borg rlist # "repo list" + - borg rinfo # "repo info" + - borg rdelete # "repo delete" + + - borg 2 archive commands: + + - borg create ARCHIVE ... + - borg list ARCHIVE + - borg info -a ARCH_GLOB + - borg delete -a ARCH_GLOB + - borg diff ARCH1 ARCH2 + - borg extract ARCHIVE ... + - borg recreate -a ARCH_GLOB ... + - borg rename OLDNAME NEWNAME + - borg mount -a ARCH_GLOB mountpoint ... + + For more details, please consult the docs or --help option output. + +Changes: + +- split repo and archive name into separate args, #948 + + - use -r or --repo or BORG_REPO env var to give the repository + - use --other-repo or BORG_OTHER_REPO to give another repo (e.g. borg transfer) + - use positional argument for archive name or `-a ARCH_GLOB` + +- remove support for scp-style repo specification, use ssh://... +- simplify stats output: repo ops -> repo stats, archive ops -> archive stats + +- repository index: add payload size (==csize) and flags to NSIndex entries +- repository index: set/query flags, iteration over flagged items (NSIndex) +- repository: sync write file in get_fd + +- stats: deduplicated size now, was deduplicated compressed size in borg 1.x +- remove csize support at most places in the code (chunks index, stats, get_size, + Item.chunks) + +- replace problematic/ugly hardlink_master approach of borg 1.x by: + + - symmetric hlid (all hardlinks pointing to same inode have same hlid) + - all archived hardlinked regular files have a chunks list + +- borg init --other-repo=OTHER_REPO: reuse key material from OTHER_REPO, #6554 +- borg transfer: + + - efficiently copy archives from borg 1.1/1.2 repo to new repo. uses + deduplication and does not decompress/recompress file content data. + - does some cleanups / fixes / conversions: + + - disallow None value for .user/group/chunks/chunks_healthy + - cleanup msgpack related str/bytes mess, use new msgpack spec, #968 + - obfuscation: fix byte order for size, #6701 + - compression: use the 2 bytes for type and level, #6698 + - use version 2 for new archives + - convert timestamps int/bigint -> msgpack.Timestamp, see #2323 + - all hardlinks have chunks, maybe chunks_healty, hlid + - remove the zlib type bytes hack + - make sure items with chunks have precomputed size + - clean item of attic 0.13 'acl' bug remnants + +- crypto: see 1.3.0a1 log entry + Version 1.3.0a1 (2022-04-15) ---------------------------- -Please note: - -This is an alpha release, only for testing - do not use this with production repos. +Although this was released as 1.3.0a1, it can be also seen as 2.0.0a1 as it was +later decided to do breaking changes and thus the major release number had to +be increased (thus, there will not be a 1.3.0 release, but 2.0.0). New features: @@ -261,4906 +160,3 @@ Other changes: - OpenBSD: build borg with OpenSSL (not: LibreSSL), #6474 - remove support for LibreSSL, #6474 - remove support for OpenSSL < 1.1.1 - - -Version 1.2.0 (2022-02-22 22:02:22 :-) --------------------------------------- - -Please note: - -This is the first borg 1.2 release, so be careful and read the notes below. - -Upgrade notes: - -Strictly taken, nothing special is required for upgrading to 1.2, but some -things can be recommended: - -- do you already want to upgrade? 1.1.x also will get fixes for a while. -- be careful, first upgrade your less critical / smaller repos. -- first upgrade to a recent 1.1.x release - especially if you run some older - 1.1.* or even 1.0.* borg release. -- using that, run at least one `borg create` (your normal backup), `prune` - and especially a `check` to see everything is in a good state. -- check the output of `borg check` - if there is anything special, consider - a `borg check --repair` followed by another `borg check`. -- if everything is fine so far (borg check reports no issues), you can consider - upgrading to 1.2.0. if not, please first fix any already existing issue. -- if you want to play safer, first **create a backup of your borg repository**. -- upgrade to latest borg 1.2.x release (you could use the fat binary from - github releases page) -- run `borg compact --cleanup-commits` to clean up a ton of 17 bytes long files - in your repo caused by a borg 1.1 bug -- run `borg check` again (now with borg 1.2.x) and check if there is anything - special. -- run `borg info` (with borg 1.2.x) to build the local pre12-meta cache (can - take significant time, but after that it will be fast) - for more details - see below. -- check the compatibility notes (see below) and adapt your scripts, if needed. -- if you run into any issues, please check the github issue tracker before - posting new issues there or elsewhere. - -If you follow this procedure, you can help avoiding that we get a lot of -"borg 1.2" issue reports that are not really 1.2 issues, but existed before -and maybe just were not noticed. - -Compatibility notes: - -- matching of path patterns has been aligned with borg storing relative paths. - Borg archives file paths without leading slashes. Previously, include/exclude - patterns could contain leading slashes. You should check your patterns and - remove leading slashes. -- dropped support / testing for older Pythons, minimum requirement is 3.8. - In case your OS does not provide Python >= 3.8, consider using our binary, - which does not need an external Python interpreter. Or continue using - borg 1.1.x, which is still supported. -- freeing repository space only happens when "borg compact" is invoked. -- mount: the default for --numeric-ids is False now (same as borg extract) -- borg create --noatime is deprecated. Not storing atime is the default behaviour - now (use --atime if you want to store the atime). -- list: corrected mix-up of "isomtime" and "mtime" formats. - Previously, "isomtime" was the default but produced a verbose human format, - while "mtime" produced a ISO-8601-like format. - The behaviours have been swapped (so "mtime" is human, "isomtime" is ISO-like), - and the default is now "mtime". - "isomtime" is now a real ISO-8601 format ("T" between date and time, not a space). -- create/recreate --list: file status for all files used to get announced *AFTER* - the file (with borg < 1.2). Now, file status is announced *BEFORE* the file - contents are processed. If the file status changes later (e.g. due to an error - or a content change), the updated/final file status will be printed again. -- removed deprecated-since-long stuff (deprecated since): - - - command "borg change-passphrase" (2017-02), use "borg key ..." - - option "--keep-tag-files" (2017-01), use "--keep-exclude-tags" - - option "--list-format" (2017-10), use "--format" - - option "--ignore-inode" (2017-09), use "--files-cache" w/o "inode" - - option "--no-files-cache" (2017-09), use "--files-cache=disabled" -- removed BORG_HOSTNAME_IS_UNIQUE env var. - to use borg you must implement one of these 2 scenarios: - - - 1) the combination of FQDN and result of uuid.getnode() must be unique - and stable (this should be the case for almost everybody, except when - having duplicate FQDN *and* MAC address or all-zero MAC address) - - 2) if you are aware that 1) is not the case for you, you must set - BORG_HOST_ID env var to something unique. -- exit with 128 + signal number, #5161. - if you have scripts expecting rc == 2 for a signal exit, you need to update - them to check for >= 128. - -Fixes: - -- diff: reduce memory consumption, fix is_hardlink_master, #6295 -- compact: fix / improve freeable / freed space log output - - - derive really freed space from quota use before/after, #5679 - - do not say "freeable", but "maybe freeable" (based on hint, unsure) -- fix race conditions in internal SaveFile function, #6306 #6028 -- implement internal safe_unlink (was: truncate_and_unlink) function more safely: - usually it does not truncate any more, only under "disk full" circumstances - and only if there is only one hardlink. - see: https://github.com/borgbackup/borg/discussions/6286 - -Other changes: - -- info: use a pre12-meta cache to accelerate stats for borg < 1.2 archives. - the first time borg info is invoked on a borg 1.1 repo, it can take a - rather long time computing and caching some stats values for 1.1 archives, - which borg 1.2 archives have in their archive metadata structure. - be patient, esp. if you have lots of old archives. - following invocations are much faster due to the cache. - related change: add archive name to calc_stats progress display. -- docs: - - - add borg 1.2 upgrade notes, #6217 - - link to borg placeholders and borg patterns help - - init: explain the encryption modes better - - clarify usage of patternfile roots - - put import-tar docs into same file as export-tar docs - - explain the difference between a path that ends with or without a slash, - #6297 - - -Version 1.2.0rc1 (2022-02-05) ------------------------------ - -Fixes: - -- repo::archive location placeholder expansion fixes, #5826, #5998 -- repository: fix intermediate commits, shall be at end of current segment -- delete: don't commit if nothing was deleted, avoid cache sync, #6060 -- argument parsing: accept some options only once, #6026 -- disallow overwriting of existing keyfiles on init, #6036 -- if ensure_dir() fails, give more informative error message, #5952 - -New features: - -- delete --force: do not ask when deleting a repo, #5941 - -Other changes: - -- requirements: exclude broken or incompatible-with-pyinstaller setuptools -- add a requirements.d/development.lock.txt and use it for vagrant -- tests: - - - added nonce-related tests - - refactor: remove assert_true - - vagrant: macos box tuning, netbsd box fixes, #5370, #5922 -- docs: - - - update install docs / requirements docs, #6180 - - borg mount / FUSE "versions" view is not experimental any more - - --pattern* is not experimental any more, #6134 - - impact of deleting path/to/repo/nonce, #5858 - - key export: add examples, #6204 - - ~/.config/borg/keys is not used for repokey keys, #6107 - - excluded parent dir's metadata can't restore - - -Version 1.2.0b4 (2022-01-23) ----------------------------- - -Fixes: - -- create: fix passing device nodes and symlinks to --paths-from-stdin, #6009 -- create --dry-run: fix display of kept tagfile, #5834 -- check --repair: fix missing parameter in "did not consistently fail" msg, #5822 -- fix hardlinkable file type check, #6037 -- list: remove placeholders for shake_* hashes, #6082 -- prune: handle case of calling prune_split when there are no archives, #6015 -- benchmark crud: make sure cleanup of borg-test-data files/dir happens, #5630 -- do not show archive name in repository-related error msgs, #6014 -- prettier error msg (no stacktrace) if exclude file is missing, #5734 -- do not require BORG_CONFIG_DIR if BORG_{SECURITY,KEYS}_DIR are set, #5979 -- fix pyinstaller detection for dir-mode, #5897 -- atomically create the CACHE_TAG file, #6028 -- deal with the SaveFile/SyncFile race, docs, see #6056 708a5853 -- avoid expanding path into LHS of formatting operation + tests, #6064 #6063 -- repository: quota / compactable computation fixes -- info: emit repo info even if repo has 0 archives + test, #6120 - -New features: - -- check --repair: significantly speed up search for next valid object in segment, #6022 -- check: add progress indicator for archive check, #5809 -- create: add retry_erofs workaround for O_NOATIME issue on volume shadow copies in WSL1, #6024 -- create: allow --files-cache=size (this is potentially dangerous, use on your own risk), #5686 -- import-tar: implement import-tar to complement export-tar, #2233 -- implement BORG_SELFTEST env variable (can be carefully used to speedup borg hosting), #5871 -- key export: print key if path is '-' or not given, #6092 -- list --format: Add command_line to format keys - -Other changes: - -- pypi metadata: alpha -> beta -- require python 3.8+, #5975 -- use pyinstaller 4.7 -- allow msgpack 1.0.3 -- upgrade to bundled xxhash to 0.8.1 -- import-tar / export-tar: tar file related changes: - - - check for short tarfile extensions - - add .lz4 and .zstd - - fix docs about extensions and decompression commands -- add github codeql analysis, #6148 -- vagrant: - - - box updates / add new boxes / remove outdated and broken boxes - - use Python 3.9.10 (incl. binary builds) and 3.10.0 - - fix pyenv initialisation, #5798 - - fix vagrant scp on macOS, #5921 - - use macfuse instead of osxfuse -- shell completions: - - - update shell completions to 1.1.17, #5923 - - remove BORG_LIBC completion, since 9914968 borg no longer uses find_library(). -- docs: - - - fixed readme.rst irc webchat link (we use libera chat now, not freenode) - - fix exceptions thrown by `setup.py build_man` - - check --repair: recommend checking hw before check --repair, #5855 - - check --verify-data: clarify and document conflict with --repository-only, #5808 - - serve: improve ssh forced commands docs, #6083 - - list: improve docs for `borg list` --format, #6061 - - list: remove --list-format from borg list - - FAQ: fix manifest-timestamp path (inside security dir) - - fix the broken link to .nix file - - document behavior for filesystems with inconsistent inodes, #5770 - - clarify user_id vs uid for fuse, #5723 - - clarify pattern usage with commands, #5176 - - clarify pp vs. pf pattern type, #5300 - - update referenced freebsd/macOS versions used for binary build, #5942 - - pull mode: add some warnings, #5827 - - clarify "you will need key and passphrase" borg init warning, #4622 - - add missing leading slashes in help patterns, #5857 - - add info on renaming repositories, #5240 - - check: add notice about defective hardware, #5753 - - mention tar --compare (compare archive to fs files), #5880 - - add note about grandfather-father-son backup retention policy / rotation scheme, #6006 - - permissions note rewritten to make it less confusing - - create github security policy - - remove leftovers of BORG_HOSTNAME_IS_UNIQUE - - excluded parent dir's metadata can't restore. (#6062) - - if parent dir is not extracted, we do not have its metadata - - clarify who starts the remote agent - - -Version 1.2.0b3 (2021-05-12) ----------------------------- - -Fixes: - -- create: fix --progress --log-json, #4360#issuecomment-774580052 -- do not load files cache for commands not using it, #5673 -- fix repeated cache tag file writing bug - -New features: - -- create/recreate: print preliminary file status early, #5417 -- create/extract: add --noxattrs and --noacls options, #3955 -- create: verbose files cache logging via --debug-topic=files_cache, #5659 -- mount: implement --numeric-ids (default: False!), #2377 -- diff: add --json-lines option -- info / create --stats: add --iec option to print sizes in powers of 1024. - -Other changes: - -- create: add --upload-(ratelimit|buffer), deprecate --remote-* options, #5611 -- create/extract/mount: add --numeric-ids, deprecate --numeric-owner option, #5724 -- config: accept non-int value for max_segment_size / storage_quota -- use PyInstaller v4.3, #5671 -- vagrant: use Python 3.9.5 to build binaries -- tox.ini: modernize and enable execution without preinstalling deps -- cleanup code style checks -- get rid of distutils, use setuptools+packaging -- github CI: test on Python 3.10-dev -- check: missing / healed chunks: always tell chunk ID, #5704 -- docs: - - - remove bad /var/cache exclusion in example commands, #5625 - - misc. fixes and improvements, esp. for macOS - - add unsafe workaround to use an old repo copy, #5722 - - -Version 1.2.0b2 (2021-02-06) ----------------------------- - -Fixes: - -- create: do not recurse into duplicate roots, #5603 -- create: only print stats if not ctrl-c'ed, fixes traceback, #5668 -- extract: - improve exception handling when setting xattrs, #5092. - emit a warning message giving the path, xattr key and error message. - continue trying to restore other xattrs and bsdflags of the same file - after an exception with xattr-setting happened. -- export-tar: - fix memory leak with ssh: remote repository, #5568. - fix potential memory leak with ssh: remote repository with partial extraction. -- remove empty shadowed_segments lists, #5275 -- fix bad default: manifest.archives.list(consider_checkpoints=False), - fixes tracebacks / KeyErros for missing objects in ChunkIndex, #5668 - -New features: - -- create: improve sparse file support - - - create --sparse (detect sparse file holes) and file map support, - only for the "fixed" chunker, #14 - - detect all-zero chunks in read data in "buzhash" and "fixed" chunkers - - cached_hash: use a small LRU cache to accelerate all-zero chunks hashing - - use cached_hash also to generate all-zero replacement chunks -- create --remote-buffer, add a upload buffer for remote repos, #5574 -- prune: keep oldest archive when retention target not met - -Other changes: - -- use blake2 from python 3.6+ hashlib - (this removes the requirement for libb2 and the bundled blake2 code) -- also accept msgpack up to 1.0.2. - exclude 1.0.1 though, which had some issues (not sure they affect borg). -- create: add repository location to --stats output, #5491 -- check: debug log the segment filename -- delete: add a --list switch to borg delete, #5116 -- borg debug dump-hints - implemented to e.g. to look at shadow_index -- Tab completion support for additional archives for 'borg delete' -- refactor: have one borg.constants.zero all-zero bytes object -- refactor shadow_index updating repo.put/delete, #5661, #5636. -- docs: - - - add another case of attempted hardlink usage - - fix description of borg upgrade hardlink usage, #5518 - - use HTTPS everywhere - - add examples for --paths-from-stdin, --paths-from-command, --paths-separator, #5644 - - fix typos/grammar - - update docs for dev environment installation instructions - - recommend running tests only on installed versions for setup - - add badge with current status of package -- vagrant: - - - use brew install --cask ..., #5557 - - use Python 3.9.1 and PyInstaller 4.1 to build the borg binary - - -Version 1.2.0b1 (2020-12-06) ----------------------------- - -Fixes: - -- BORG_CACHE_DIR crashing borg if empty, atomic handling of - recursive directory creation, #5216 -- fix --dry-run and --stats coexistence, #5415 -- allow EIO with warning when trying to hardlink, #4336 -- export-tar: set tar format to GNU_FORMAT explicitly, #5274 -- use --timestamp for {utcnow} and {now} if given, #5189 -- make timestamp helper timezone-aware - -New features: - -- create: implement --paths-from-stdin and --paths-from-command, see #5492. - These switches read paths to archive from stdin. Delimiter can specified - by --paths-delimiter=DELIM. Paths read will be added honoring every - option but exclusion options and --one-file-system. borg won't recurse - into directories. -- 'obfuscate' pseudo compressor obfuscates compressed chunk size in repo -- add pyfuse3 (successor of llfuse) as an alternative lowlevel fuse - implementation to llfuse (deprecated), #5407. - FUSE implementation can be switched via env var BORG_FUSE_IMPL. -- allow appending to the files cache filename with BORG_FILES_CACHE_SUFFIX -- create: implement --stdin-mode, --stdin-user and --stdin-group, #5333 - -Other changes: - -- split recursive directory walking/processing into directory walking and - item processing. -- fix warning by importing setuptools before distutils. -- debug info: include infos about FUSE implementation, #5546 -- testing: - - - add a test for the hashindex corruption bug, #5531 #4829 - - move away from travis-ci, use github actions, #5528 #5467 - - test both on fuse2 and fuse3 - - upload coverage reports to codecov - - fix spurious failure in test_cache_files, #5438 - - add tests for Location.with_timestamp - - tox: add a non-fuse env to the envlist -- vagrant: - - - use python 3.7.latest and pyinstaller 4.0 for binary creation - - pyinstaller: compute basepath from spec file location - - vagrant: updates/fixes for archlinux box, #5543 -- docs: - - - "filename with spaces" example added to exclude file, #5236 - - add a hint about sleeping computer, #5301 - - how to adjust macOS >= Catalina security settings, #5303 - - process/policy for adding new compression algorithms - - updated docs about hacked backup client, #5480 - - improve ansible deployment docs, make it more generic - - how to approach borg speed issues, give speed example, #5371 - - fix mathematical inaccuracy about chunk size, #5336 - - add example for excluding content using --pattern cli option - - clarify borg create's '--one-file-system' option, #4009 - - improve docs/FAQ about append-only remote repos, #5497 - - fix reST markup issues, labels - - add infos about contributor retirement status - - -Version 1.2.0a9 (2020-10-05) ----------------------------- - -Fixes: - -- fix memory leak related to preloading, #5202 -- check --repair: fix potential data loss, #5325 -- persist shadow_index in between borg runs, #4830 -- fix hardlinked CACHEDIR.TAG processing, #4911 -- --read-special: .part files also should be regular files, #5217 -- allow server side enforcing of umask, --umask is for the local borg - process only (see docs), #4947 -- exit with 128 + signal number, #5161 -- borg config --list does not show last_segment_checked, #5159 -- locking: - - - fix ExclusiveLock race condition bug, #4923 - - fix race condition in lock migration, #4953 - - fix locking on openindiana, #5271 - -New features: - -- --content-from-command: create archive using stdout of given command, #5174 -- allow key-import + BORG_KEY_FILE to create key files -- build directory-based binary for macOS to avoid Gatekeeper delays - -Other changes: - -- upgrade bundled zstd to 1.4.5 -- upgrade bundled xxhash to 0.8.0, #5362 -- if self test fails, also point to OS and hardware, #5334 -- misc. shell completions fixes/updates, rewrite zsh completion -- prettier error message when archive gets too big, #5307 -- stop relying on `false` exiting with status code 1 -- rephrase some warnings, #5164 -- parseformat: unnecessary calls removed, #5169 -- testing: - - - enable Python3.9 env for test suite and VMs, #5373 - - drop python 3.5, #5344 - - misc. vagrant fixes/updates - - misc. testing fixes, #5196 -- docs: - - - add ssh-agent pull backup method to doc, #5288 - - mention double --force in prune docs - - update Homebrew install instructions, #5185 - - better description of how cache and rebuilds of it work - and how the workaround applies to that - - point to borg create --list item flags in recreate usage, #5165 - - add a note to create from stdin regarding files cache, #5180 - - add security faq explaining AES-CTR crypto issues, #5254 - - clarify --exclude-if-present in recreate, #5193 - - add socat pull mode, #5150, #900 - - move content of resources doc page to community project, #2088 - - explain hash collision, #4884 - - clarify --recompress option, #5154 - - -Version 1.2.0a8 (2020-04-22) ----------------------------- - -Fixes: - -- fixed potential index corruption / data loss issue due to bug in hashindex_set, #4829. - Please read and follow the more detailed notes close to the top of this document. -- fix crash when upgrading erroneous hints file, #4922 -- commit-time free space calc: ignore bad compact map entries, #4796 -- info: if the archive doesn't exist, print a pretty message, #4793 -- --prefix / -P: fix processing, avoid argparse issue, #4769 -- ignore EACCES (errno 13) when hardlinking, #4730 -- add a try catch when formatting the info string, #4818 -- check: do not stumble over invalid item key, #4845 -- update prevalence of env vars to set config and cache paths -- mount: fix FUSE low linear read speed on large files, #5032 -- extract: fix confusing output of borg extract --list --strip-components, #4934 -- recreate: support --timestamp option, #4745 -- fix ProgressIndicator msgids (JSON output), #4935 -- fuse: set f_namemax in statfs result, #2684 -- accept absolute paths on windows -- pyinstaller: work around issue with setuptools > 44 - -New features: - -- chunker speedup (plus regression test) -- added --consider-checkpoints and related test, #4788 -- added --noflags option, deprecate --nobsdflags option, #4489 -- compact: add --threshold option, #4674 -- mount: add birthtime to FUSE entries -- support platforms with no os.link, #4901 - if we don't have os.link, - we just extract another copy instead of making a hardlink. -- move sync_file_range to its own extension for better platform compatibility. -- new --bypass-lock option to bypass locking, e.g. for read-only repos -- accept absolute paths by removing leading slashes in patterns of all - sorts but re: style, #4029 -- delete: new --keep-security-info option - -Other changes: - -- support msgpack 0.6.2 and 1.0.0, #5065 -- upgrade bundled zstd to 1.4.4 -- upgrade bundled lz4 to 1.9.2 -- upgrade xxhash to 0.7.3 -- require recent enough llfuse for birthtime support, #5064 -- only store compressed data if the result actually is smaller, #4516 -- check: improve error output for matching index size, see #4829 -- ignore --stats when given with --dry-run, but continue, #4373 -- replaced usage of os.statvfs with shutil.disk_usage (better cross-platform support). -- fuse: remove unneeded version check and compat code, micro opts -- docs: - - - improve description of path variables - - document how to completely delete data, #2929 - - add FAQ about Borg config dir, #4941 - - add docs about errors not printed as JSON, #4073 - - update usage_general.rst.inc - - added "Will move with BORG_CONFIG_DIR variable unless specified." to BORG_SECURITY_DIR info. - - put BORG_SECURITY_DIR immediately below BORG_CONFIG_DIR (and moved BORG_CACHE_DIR up before them). - - add paragraph regarding cache security assumptions, #4900 - - tell about borg cache security precautions - - add FAQ describing difference between a local repo vs. repo on a server. - - document how to test exclusion patterns without performing an actual backup - - create: tell that "Calculating size" time and space needs are caused by --progress - - fix/improve documentation for @api decorator, #4674 - - add a pull backup / push restore how-to, #1552 - - fix man pages creation, #4752 - - more general FAQ for backup and retain original paths, #4532 - - explain difference between --exclude and --pattern, #4118 - - add FAQ for preventing SSH timeout in extract, #3866 - - improve password FAQ (decrease pw length, add -w 0 option to base64 to prevent line wrap), #4591 - - add note about patterns and stored paths, #4160 - - add upgrade of tools to pip installation how-to, #5090 - - document one cause of orphaned chunks in check command, #2295 - - clean up the whole check usage paragraph - - FAQ: linked recommended restrictions to ssh public keys on borg servers, #4946 - - fixed "doc downplays severity of Nonce reuse issue", #4883 - - borg repo restore instructions needed, #3428 - - new FAQ: A repo is corrupt and must be replaced with an older repo. - - clarify borg init's encryption modes -- native windows port: - - - update README_WINDOWS.rst - - updated pyinstaller spec file to support windows builds -- testing / CI: - - - improved travis config / install script, improved macOS builds - - allow osx builds to fail, #4955 - - Windows 10 build on Appveyor CI -- vagrant: - - - upgrade pyinstaller to v3.5 + patch - - use py369 for binary build, add py380 for tests - - fix issue in stretch VM hanging at grub installation - - add a debian buster and a ubuntu focal VM - - update darwin box to 10.12 - - upgrade FreeBSD box to 12.1 - - fix debianoid virtualenv packages - - use pyenv in freebsd64 VM - - remove the flake8 test - - darwin: avoid error if pkg is already installed - - debianoid: don't interactively ask questions - - -Version 1.2.0a7 (2019-09-07) ----------------------------- - -Fixes: - -- slave hardlinks extraction issue, see #4350 -- extract: fix KeyError for "partial" extraction, #4607 -- preload chunks for hardlink slaves w/o preloaded master, #4350 -- fix preloading for old remote servers, #4652 -- fix partial extract for hardlinked contentless file types, #4725 -- Repository.open: use stat() to check for repo dir, #4695 -- Repository.check_can_create_repository: use stat() to check, ~ #4695. -- SecurityManager.known(): check all files, #4614 -- after double-force delete, warn about necessary repair, #4704 -- cope with ANY error when importing pytest into borg.testsuite, #4652 -- fix invalid archive error message -- setup.py: fix detection of missing Cython -- filter out selinux xattrs, #4574 -- location arg - should it be optional? #4541 -- enable placeholder usage in --comment, #4559 -- use whitelist approach for borg serve, #4097 - -New features: - -- minimal native Windows support, see windows readme (work in progress) -- create: first ctrl-c (SIGINT) triggers checkpoint and abort, #4606 -- new BORG_WORKAROUNDS mechanism, basesyncfile, #4710 -- remove WSL autodetection. if WSL still has this problem, you need to - set BORG_WORKAROUNDS=basesyncfile in the borg process environment to - work around it. -- support xxh64 checksum in addition to the hashlib hashes in borg list -- enable placeholder usage in all extra archive arguments -- enable placeholder usage in --comment, #4559 -- enable placeholder usage in --glob-archives, #4495 -- ability to use a system-provided version of "xxhash" -- create: - - - changed the default behaviour to not store the atime of fs items. atime is - often rather not interesting and fragile - it easily changes even if nothing - else has changed and, if stored into the archive, spoils deduplication of - the archive metadata stream. - - if you give the --noatime option, borg will output a deprecation warning - because it is currently ignored / does nothing. - Please remove the --noatime option when using borg 1.2. - - added a --atime option for storing files' atime into an archive - -Other changes: - -- argparser: always use REPOSITORY in metavar -- do not check python/libc for borg serve, #4483 -- small borg compact improvements, #4522 -- compact: log freed space at INFO level -- tests: - - - tox / travis: add testing on py38-dev - - fix broken test that relied on improper zlib assumptions - - pure-py msgpack warning shall not make a lot of tests fail, #4558 - - rename test_mount_hardlinks to test_fuse_mount_hardlinks (master) - - vagrant: add up-to-date openindiana box (py35, openssl10) - - get rid of confusing coverage warning, #2069 -- docs: - - - reiterate that 'file cache names are absolute' in FAQ, - mention bind mount solution, #4738 - - add restore docs, #4670 - - updated docs to cover use of temp directory on remote, #4545 - - add a push-style example to borg-create(1), #4613 - - timestamps in the files cache are now usually ctime, #4583 - - benchmark crud: clarify that space is used until compact - - update documentation of borg create, - corrects a mention of borg 1.1 as a future version. - - fix osxfuse github link in installation docs - - how to supply a passphrase, use crypto devices, #4549 - - extract: document limitation "needs empty destination", #4598 - - update macOS Brew link - - add note about software for automating backup - - compact: improve docs, - - README: new URL for funding options - - -Version 1.2.0a6 (2019-04-22) ----------------------------- - -Fixes: - -- delete / prune: consider part files correctly for stats, #4507 -- fix "all archives" stats considering part files, #4329 -- create: only run stat_simple_attrs() once -- create: --stats does not work with --dry-run, exit with error msg, #4373 -- give "invalid repo" error msg if repo config not found, #4411 - -New features: - -- display msgpack version as part of sysinfo (e.g. in tracebacks) - -Other changes: - -- docs: - - - sdd "SSH Configuration" section, #4493, #3988, #636, #4485 - - better document borg check --max-duration, #4473 - - sorted commands help in multiple steps, #4471 -- testing: - - - travis: use py 3.5.3 and 3.6.7 on macOS to get a pyenv-based python - build with openssl 1.1 - - vagrant: use py 3.5.3 and 3.6.8 on darwin64 VM to build python and - borg with openssl 1.1 - - pytest: -v and default XDISTN to 1, #4481 - - -Version 1.2.0a5 (2019-03-21) ----------------------------- - -Fixes: - -- warn if a file has changed while being backed up, #1750 -- lrucache: regularly remove old FDs, #4427 -- borg command shall terminate with rc 2 for ImportErrors, #4424 -- make freebsd xattr platform code api compatible with linux, #3952 - -Other changes: - -- major setup code refactoring (especially how libraries like openssl, liblz4, - libzstd, libb2 are discovered and how it falls back to code bundled with - borg), new: uses pkg-config now (and needs python "pkgconfig" package - installed), #1925 - - if you are a borg package maintainer, please try packaging this - (see comments in setup.py). -- Vagrantfile: add zstd, reorder, build env vars, #4444 -- travis: install script improvements -- update shell completions -- docs: - - - add a sample logging.conf in docs/misc, #4380 - - fix spelling errors - - update requirements / install docs, #4374 - - -Version 1.2.0a4 (2019-03-11) ----------------------------- - -Fixes: - -- do not use O_NONBLOCK for special files, like FIFOs, block and char devices - when using --read-special. fixes backing up FIFOs. fixes to test. #4394 -- more LibreSSL build fixes: LibreSSL has HMAC_CTX_free and HMAC_CTX_new - -New features: - -- check: incremental repo check (only checks crc32 for segment entries), #1657 - borg check --repository-only --max-duration SECONDS ... -- delete: timestamp for borg delete --info added, #4359 - -Other changes: - -- redo stale lock handling, #3986 - drop BORG_HOSTNAME_IS_UNIQUE (please use BORG_HOST_ID if needed). - borg now always assumes it has a unique host id - either automatically - from fqdn plus uuid.getnode() or overridden via BORG_HOST_ID. -- docs: - - - added Alpine Linux to distribution list - - elaborate on append-only mode docs -- vagrant: - - - darwin: new 10.12 box - - freebsd: new 12.0 box - - openbsd: new 6.4 box - - misc. updates / fixes - - -Version 1.2.0a3 (2019-02-26) ----------------------------- - -Fixes: - -- LibreSSL build fixes, #4403 -- dummy ACL/xattr code fixes (used by OpenBSD and others), #4403 -- create: fix openat/statat issues for root directory, #4405 - - -Version 1.2.0a2 and earlier (2019-02-24) ----------------------------------------- - -New features: - -- compact: "borg compact" needs to be used to free repository space by - compacting the segments (reading sparse segments, rewriting still needed - data to new segments, deleting the sparse segments). - Borg < 1.2 invoked compaction automatically at the end of each repository - writing command. - Borg >= 1.2 does not do that any more to give better speed, more control, - more segment file stability (== less stuff moving to newer segments) and - more robustness. - See the docs about "borg compact" for more details. -- "borg compact --cleanup-commits" is to cleanup the tons of 17byte long - commit-only segment files caused by borg 1.1.x issue #2850. - Invoke this once after upgrading (the server side) borg to 1.2. - Compaction now automatically removes unneeded commit-only segment files. -- prune: Show which rule was applied to keep archive, #2886 -- add fixed blocksize chunker (see --chunker-params docs), #1086 - -Fixes: - -- avoid stale filehandle issues, #3265 -- use more FDs, avoid race conditions on active fs, #906, #908, #1038 -- add O_NOFOLLOW to base flags, #908 -- compact: - - - require >10% freeable space in a segment, #2985 - - repository compaction now automatically removes unneeded 17byte - commit-only segments, #2850 -- make swidth available on all posix platforms, #2667 - -Other changes: - -- repository: better speed and less stuff moving around by using separate - segment files for manifest DELETEs and PUTs, #3947 -- use pyinstaller v3.3.1 to build binaries -- update bundled zstd code to 1.3.8, #4210 -- update bundled lz4 code to 1.8.3, #4209 -- msgpack: - - - switch to recent "msgpack" pypi pkg name, #3890 - - wrap msgpack to avoid future compat complications, #3632, #2738 - - support msgpack 0.6.0 and 0.6.1, #4220, #4308 - -- llfuse: modernize / simplify llfuse version requirements -- code refactorings / internal improvements: - - - include size/csize/nfiles[_parts] stats into archive, #3241 - - calc_stats: use archive stats metadata, if available - - crypto: refactored crypto to use an AEAD style API - - crypto: new AES-OCB, CHACHA20-POLY1305 - - create: use less syscalls by not using a python file obj, #906, #3962 - - diff: refactor the diff functionality to new ItemDiff class, #2475 - - archive: create FilesystemObjectProcessors class - - helpers: make a package, split into smaller modules - - xattrs: move to platform package, use cython instead ctypes, #2495 - - xattrs/acls/bsdflags: misc. code/api optimizations - - FUSE: separate creation of filesystem from implementation of llfuse funcs, #3042 - - FUSE: use unpacker.tell() instead of deprecated write_bytes, #3899 - - setup.py: move build_man / build_usage code to setup_docs.py - - setup.py: update to use a newer Cython/setuptools API for compiling .pyx -> .c, #3788 - - use python 3.5's os.scandir / os.set_blocking - - multithreading preparations (not used yet): - - - item.to_optr(), Item.from_optr() - - fix chunker holding the GIL during blocking I/O - - C code portability / basic MSC compatibility, #4147, #2677 -- testing: - - - vagrant: new VMs for linux/bsd/darwin, most with OpenSSL 1.1 and py36 - - -Version 1.1.18 (2022-06-05) ---------------------------- - -Compatibility notes: - -- When upgrading from borg 1.0.x to 1.1.x, please note: - - - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1. - - borg upgrade: you do not need to and you also should not run it. - - borg might ask some security-related questions once after upgrading. - You can answer them either manually or via environment variable. - One known case is if you use unencrypted repositories, then it will ask - about a unknown unencrypted repository one time. - - your first backup with 1.1.x might be significantly slower (it might - completely read, chunk, hash a lot files) - this is due to the - --files-cache mode change (and happens every time you change mode). - You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible - mode (but that is less safe for detecting changed files than the default). - See the --files-cache docs for details. -- 1.1.11 removes WSL autodetection (Windows 10 Subsystem for Linux). - If WSL still has a problem with sync_file_range, you need to set - BORG_WORKAROUNDS=basesyncfile in the borg process environment to - work around the WSL issue. -- 1.1.14 changes return codes due to a bug fix: - In case you have scripts expecting rc == 2 for a signal exit, you need to - update them to check for >= 128 (as documented since long). -- 1.1.15 drops python 3.4 support, minimum requirement is 3.5 now. -- 1.1.17 install_requires the "packaging" pypi package now. - -New features: - -- check --repair: significantly speed up search for next valid object in segment, #6022 -- create: add retry_erofs workaround for O_NOATIME issue on volume shadow copies in WSL1, #6024 -- key export: display key if path is '-' or not given, #6092 -- list --format: add command_line to format keys, #6108 - -Fixes: - -- check: improve error handling for corrupt archive metadata block, - make robust_iterator more robust, #4777 -- diff: support presence change for blkdev, chrdev and fifo items, #6483 -- diff: reduce memory consumption, fix is_hardlink_master -- init: disallow overwriting of existing keyfiles -- info: fix authenticated mode repo to show "Encrypted: No", #6462 -- info: emit repo info even if repo has 0 archives, #6120 -- list: remove placeholders for shake_* hashes, #6082 -- mount -o versions: give clear error msg instead of crashing -- show_progress: add finished=true/false to archive_progress json, #6570 -- fix hardlinkable file type check, #6037 -- do not show archive name in error msgs referring to the repository, #6023 -- prettier error msg (no stacktrace) if exclude file is missing, #5734 -- do not require BORG_CONFIG_DIR if BORG_{SECURITY,KEYS}_DIR are set, #5979 -- atomically create the CACHE_TAG file, #6028 -- deal with the SaveFile/SyncFile race, docs, see #6176 5c5b59bc9 -- avoid expanding path into LHS of formatting operation + tests, #6064 #6063 -- repository: quota / compactable computation fixes, #6119. - This is mainly to keep the repo code in sync with borg 1.2. As borg 1.1 - compacts immediately, there was not really an issue with this in 1.1. -- fix transaction rollback: use files cache filename as found in txn.active, #6353 -- do not load files cache for commands not using it, fixes #5673 -- fix scp repo url parsing for ip v6 addrs, #6526 -- repo::archive location placeholder expansion fixes, #5826, #5998 - - - use expanded location for log output - - support placeholder expansion for BORG_REPO env var -- respect umask for created directory and file modes, #6400 -- safer truncate_and_unlink implementation - -Other changes: - -- upgrade bundled xxhash code to 0.8.1 -- fix xxh64 related build (setup.py and post-0.8.1 patch for static_assert). - The patch was required to build the bundled xxhash code on FreeBSD, see - https://github.com/Cyan4973/xxHash/pull/670 -- msgpack build: remove endianness macro, #6105 -- update and fix shell completions -- fuse: remove unneeded version check and compat code -- delete --force: do not ask when deleting a repo, #5941 -- delete: don't commit if nothing was deleted, avoid cache sync, #6060 -- delete: add repository id and location to prompt -- compact segments: improve freeable / freed space log output, #5679 -- if ensure_dir() fails, give more informative error message, #5952 -- load_key: no key is same as empty key, #6441 -- better error msg for defect or unsupported repo configs, #6566 -- use hmac.compare_digest instead of ==, #6470 -- implement more standard hashindex.setdefault behaviour -- remove stray punctuation from secure-erase message -- add development.lock.txt, use a real python 3.5 to generate frozen reqs -- setuptools 60.7.0 breaks pyinstaller, #6246 -- setup.py clean2 was added to work around some setuptools customizability limitation. -- allow extra compiler flags for every extension build -- C code: make switch fallthrough explicit -- Cython code: fix "useless trailing comma" cython warnings -- requirements.lock.txt: use the latest cython 0.29.30 -- fix compilation warnings: ‘PyUnicode_AsUnicode’ is deprecated -- docs: - - - ~/.config/borg/keys is not used for repokey keys, #6107 - - excluded parent dir's metadata can't restore, #6062 - - permissions note rewritten to make it less confusing, #5490 - - add note about grandfather-father-son backup retention policy / rotation scheme - - clarify who starts the remote agent (borg serve) - - test/improve pull backup docs, #5903 - - document the socat pull mode described in #900 #515ß - - borg serve: improve ssh forced commands docs, #6083 - - improve docs for borg list --format, #6080 - - fix the broken link to .nix file - - clarify pattern usage with commands, #5176 - - clarify user_id vs uid for fuse, #5723 - - fix binary build freebsd/macOS version, #5942 - - FAQ: fix manifest-timestamp path, #6016 - - remove duplicate faq entries, #5926 - - fix sphinx warnings, #5919 - - virtualisation speed tips - - fix values of TAG bytes, #6515 - - recommend umask for passphrase file perms - - update link to ubuntu packages, #6485 - - clarify on-disk order and size of log entry fields, #6357 - - do not transform --/--- to unicode dashes - - improve linking inside docs, link to borg_placeholders, link to borg_patterns - - use same phrasing in misc. help texts - - borg init: explain the encryption modes better - - explain the difference between a path that ends with or without a slash, #6297 - - clarify usage of patternfile roots, #6242 - - borg key export: add examples - - updates about features not experimental any more: FUSE "versions" view, --pattern*, #6134 - - fix/update cygwin package requirements - - impact of deleting path/to/repo/nonce, #5858 - - warn about tampered server nonce - - mention BORG_FILES_CACHE_SUFFIX as alternative to BORG_FILES_CACHE_TTL, #5602 - - add a troubleshooting note about "is not a valid repository" to the FAQ -- vagrant / CI / testing: - - - misc. fixes and updates, new python versions - - macOS on github: re-enable fuse2 testing by downgrading to older macOS, #6099 - - fix OpenBSD symlink mode test failure, #2055 - - use the generic/openbsd6 box - - strengthen the test: we can read data w/o nonces - - add tests for path/to/repo/nonce deletion - - darwin64: backport some tunings from master - - darwin64: remove fakeroot, #6314 - - darwin64: fix vagrant scp, #5921 - - darwin64: use macfuse instead of osxfuse - - add ubuntu "jammy" 22.04 LTS VM - - adapt memory for openindiana64 and darwin64 - - -Version 1.1.17 (2021-07-12) ---------------------------- - -Compatibility notes: - -- When upgrading from borg 1.0.x to 1.1.x, please note: - - - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1. - - borg upgrade: you do not need to and you also should not run it. - - borg might ask some security-related questions once after upgrading. - You can answer them either manually or via environment variable. - One known case is if you use unencrypted repositories, then it will ask - about a unknown unencrypted repository one time. - - your first backup with 1.1.x might be significantly slower (it might - completely read, chunk, hash a lot files) - this is due to the - --files-cache mode change (and happens every time you change mode). - You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible - mode (but that is less safe for detecting changed files than the default). - See the --files-cache docs for details. -- 1.1.11 removes WSL autodetection (Windows 10 Subsystem for Linux). - If WSL still has a problem with sync_file_range, you need to set - BORG_WORKAROUNDS=basesyncfile in the borg process environment to - work around the WSL issue. -- 1.1.14 changes return codes due to a bug fix: - In case you have scripts expecting rc == 2 for a signal exit, you need to - update them to check for >= 128 (as documented since long). -- 1.1.15 drops python 3.4 support, minimum requirement is 3.5 now. -- 1.1.17 install_requires the "packaging" pypi package now. - -Fixes: - -- pyinstaller dir-mode: fix pyi detection / LIBPATH treatment, #5897 -- handle crash due to kill stale lock race, #5828 -- fix BORG_CACHE_DIR crashing borg if empty, #5216 -- create --dry-run: fix display of kept tagfile, #5834 -- fix missing parameter in "did not consistently fail" msg, #5822 -- missing / healed chunks: always tell chunk ID, #5704 -- benchmark: make sure cleanup happens even on exceptions, #5630 - -New features: - -- implement BORG_SELFTEST env variable, #5871. - this can be used to accelerate borg startup a bit. not recommended for - normal usage, but borg mass hosters with a lot of borg invocations can - save some resources with this. on my laptop, this saved ~100ms cpu time - (sys+user) per borg command invocation. -- implement BORG_LIBC env variable to give the libc filename, #5870. - you can use this if a borg does not find your libc. -- check: add progress indicator for archive check. -- allow --files-cache=size (not recommended, make sure you know what you do) - -Other changes: - -- Python 3.10 now officially supported! - we test on py310-dev on github CI since a while and now also on the vagrant - machines, so it should work ok. -- github CI: test on py310 (again) -- get rid of distutils, use packaging and setuptools. - distutils is deprecated and gives warnings on py 3.10. -- setup.py: rename "clean" to "clean2" to avoid shadowing the "clean" command. -- remove libc filename fallback for the BSDs (there is no "usual" name) -- cleanup flake8 checks, fix some pep8 violations. -- docs building: replace deprecated function ".add_stylesheet()" for Sphinx 4 compatibility -- docs: - - - add a hint on sleeping computer and ssh connections, #5301 - - update the documentation on hacked backup client, #5480 - - improve docs/FAQ about append-only remote repos, #5497 - - complement the documentation for pattern files and exclude files, #5520 - - "filename with spaces" example added to exclude file, #5236 - note: no whitespace escaping needed, processed by borg. - - add info on renaming repositories, #5240 - - clarify borg check --verify-data, #5808 - - add notice about defective hardware to check documentation, #5753 - - add paragraph added in #5855 to utility documentation source - - add missing leading slashes in help patterns, #5857 - - clarify "you will need key and passphrase" borg init warning, #4622 - - pull mode: add some warnings, #5827 - - mention tar --compare (compare archive to fs files), #5880 - - fix typos, backport of #5597 -- vagrant: - - - add py3.7.11 for binary build, also add 3.10-dev. - - use latest Cython 0.29.23 for py310 compat fixes. - - more RAM for openindiana upgrade plan resolver, it just hangs (swaps?) if - there is too little RAM. - - fix install_pyenv to adapt to recent changes in pyenv (same as in master now). - - use generic/netbsd9 box, copied from master branch. - - -Version 1.1.16 (2021-03-23) ---------------------------- - -Fixes: - -- setup.py: add special openssl prefix for Apple M1 compatibility -- do not recurse into duplicate roots, #5603 -- remove empty shadowed_segments lists, #5275, #5614 -- fix libpython load error when borg fat binary / dir-based binary is invoked - via a symlink by upgrading pyinstaller to v4.2, #5688 -- config: accept non-int value (like 500M or 100G) for max_segment_size or - storage_quota, #5639. - please note: when setting a non-int value for this in a repo config, - using the repo will require borg >= 1.1.16. - -New features: - -- bundled msgpack: drop support for old buffer protocol to support Python 3.10 -- verbose files cache logging via --debug-topic=files_cache, #5659. - Use this if you suspect that borg does not detect unmodified files as expected. -- create/extract: add --noxattrs and --noacls option, #3955. - when given with borg create, borg will not get xattrs / ACLs from input files - (and thus, it will not archive xattrs / ACLs). when given with borg extract, - borg will not read xattrs / ACLs from archive and will not set xattrs / ACLs - on extracted files. -- diff: add --json-lines option, #3765 -- check: debug log segment filename -- borg debug dump-hints - -Other changes: - -- Tab completion support for additional archives for 'borg delete' -- repository: deduplicate code of put and delete, no functional change -- tests: fix result order issue (sporadic test failure on openindiana) -- vagrant: - - - upgrade pyinstaller to v4.2, #5671 - - avoid grub-install asking interactively for device - - remove the xenial box - - update freebsd box to 12.1 -- docs: - - - update macOS install instructions, #5677 - - use macFUSE (not osxfuse) for Apple M1 compatibility - - update docs for dev environment installation instructions, #5643 - - fix grammar in faq - - recommend running tests only on installed versions for setup - - add link back to git-installation - - remove /var/cache exclusion in example commands, #5625. - This is generally a poor idea and shouldn't be promoted through examples. - - add repology.org badge with current packaging status - - explain hash collision - - add unsafe workaround to use an old repo copy, #5722 - - -Version 1.1.15 (2020-12-25) ---------------------------- - -Fixes: - -- extract: - - - improve exception handling when setting xattrs, #5092. - - emit a warning message giving the path, xattr key and error message. - - continue trying to restore other xattrs and bsdflags of the same file - after an exception with xattr-setting happened. -- export-tar: - - - set tar format to GNU_FORMAT explicitly, #5274 - - fix memory leak with ssh: remote repository, #5568 - - fix potential memory leak with ssh: remote repository with partial extraction -- create: fix --dry-run and --stats coexistence, #5415 -- use --timestamp for {utcnow} and {now} if given, #5189 - -New features: - -- create: implement --stdin-mode, --stdin-user and --stdin-group, #5333 -- allow appending the files cache filename with BORG_FILES_CACHE_SUFFIX env var - -Other changes: - -- drop python 3.4 support, minimum requirement is 3.5 now. -- enable using libxxhash instead of bundled xxh64 code -- update llfuse requirements (1.3.8) -- set cython language_level in some files to fix warnings -- allow EIO with warning when trying to hardlink -- PropDict: fail early if internal_dict is not a dict -- update shell completions -- tests / CI - - - add a test for the hashindex corruption bug, #5531 #4829 - - fix spurious failure in test_cache_files, #5438 - - added a github ci workflow - - reduce testing on travis, no macOS, no py3x-dev, #5467 - - travis: use newer dists, native py on dist -- vagrant: - - - remove jessie and trusty boxes, #5348 #5383 - - pyinstaller 4.0, build on py379 - - binary build on stretch64, #5348 - - remove easy_install based pip installation -- docs: - - - clarify '--one-file-system' for btrfs, #5391 - - add example for excluding content using the --pattern cmd line arg - - complement the documentation for pattern files and exclude files, #5524 - - made ansible playbook more generic, use package instead of pacman. also - change state from "latest" to "present". - - complete documentation on append-only remote repos, #5497 - - internals: rather talk about target size than statistics, #5336 - - new compression algorithm policy, #1633 #5505 - - faq: add a hint on sleeping computer, #5301 - - note requirements for full disk access on macOS Catalina, #5303 - - fix/improve description of borg upgrade hardlink usage, #5518 -- modernize 1.1 code: - - - drop code/workarounds only needed to support Python 3.4 - - remove workaround for pre-release py37 argparse bug - - removed some outdated comments/docstrings - - requirements: remove some restrictions, lock on current versions - - -Version 1.1.14 (2020-10-07) ---------------------------- - -Fixes: - -- check --repair: fix potential data loss when interrupting it, #5325 -- exit with 128 + signal number (as documented) when borg is killed by a signal, #5161 -- fix hardlinked CACHEDIR.TAG processing, #4911 -- create --read-special: .part files also should be regular files, #5217 -- llfuse dependency: choose least broken 1.3.6/1.3.7. - 1.3.6 is broken on python 3.9, 1.3.7 is broken on FreeBSD. - -Other changes: - -- upgrade bundled xxhash to 0.7.4 -- self test: if it fails, also point to OS and hardware, #5334 -- pyinstaller: compute basepath from spec file location -- prettier error message when archive gets too big, #5307 -- check/recreate are not "experimental" any more (but still potentially dangerous): - - - recreate: remove extra confirmation - - rephrase some warnings, update docs, #5164 -- shell completions: - - - misc. updates / fixes - - support repositories in fish tab completion, #5256 - - complete $BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING - - rewrite zsh completion: - - - completion for almost all optional and positional arguments - - completion for Borg environment variables (parameters) -- use "allow/deny list" instead of "white/black list" wording -- declare "allow_cache_wipe" marker in setup.cfg to avoid pytest warning -- vagrant / tests: - - - misc. fixes / updates - - use python 3.5.10 for binary build - - build directory-based binaries additionally to the single file binaries - - add libffi-dev, required to build python - - use cryptography<3.0, more recent versions break the jessie box - - test on python 3.9 - - do brew update with /dev/null redirect to avoid "too much log output" on travis-ci -- docs: - - - add ssh-agent pull backup method docs, #5288 - - how to approach borg speed issues, #5371 - - mention double --force in prune docs - - update Homebrew install instructions, #5185 - - better description of how cache and rebuilds of it work - - point to borg create --list item flags in recreate usage, #5165 - - add security faq explaining AES-CTR crypto issues, #5254 - - add a note to create from stdin regarding files cache, #5180 - - fix borg.1 manpage generation regression, #5211 - - clarify how exclude options work in recreate, #5193 - - add section for retired contributors - - hint about not misusing private email addresses of contributors for borg support - - -Version 1.1.13 (2020-06-06) ---------------------------- - -Compatibility notes: - -- When upgrading from borg 1.0.x to 1.1.x, please note: - - - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1. - - borg upgrade: you do not need to and you also should not run it. - - borg might ask some security-related questions once after upgrading. - You can answer them either manually or via environment variable. - One known case is if you use unencrypted repositories, then it will ask - about a unknown unencrypted repository one time. - - your first backup with 1.1.x might be significantly slower (it might - completely read, chunk, hash a lot files) - this is due to the - --files-cache mode change (and happens every time you change mode). - You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible - mode (but that is less safe for detecting changed files than the default). - See the --files-cache docs for details. -- 1.1.11 removes WSL autodetection (Windows 10 Subsystem for Linux). - If WSL still has a problem with sync_file_range, you need to set - BORG_WORKAROUNDS=basesyncfile in the borg process environment to - work around the WSL issue. - -Fixes: - -- rebuilt using a current Cython version, compatible with python 3.8, #5214 - - -Version 1.1.12 (2020-06-06) ---------------------------- - -Fixes: - -- fix preload-related memory leak, #5202. -- mount / borgfs (FUSE filesystem): - - - fix FUSE low linear read speed on large files, #5067 - - fix crash on old llfuse without birthtime attrs, #5064 - accidentally - we required llfuse >= 1.3. Now also old llfuse works again. - - set f_namemax in statfs result, #2684 -- update precedence of env vars to set config and cache paths, #4894 -- correctly calculate compression ratio, taking header size into account, too - -New features: - -- --bypass-lock option to bypass locking with read-only repositories - -Other changes: - -- upgrade bundled zstd to 1.4.5 -- travis: adding comments and explanations to Travis config / install script, - improve macOS builds. -- tests: test_delete_force: avoid sporadic test setup issues, #5196 -- misc. vagrant fixes -- the binary for macOS is now built on macOS 10.12 -- the binaries for Linux are now built on Debian 8 "Jessie", #3761 -- docs: - - - PlaceholderError not printed as JSON, #4073 - - "How important is Borg config?", #4941 - - make Sphinx warnings break docs build, #4587 - - some markup / warning fixes - - add "updating borgbackup.org/releases" to release checklist, #4999 - - add "rendering docs" to release checklist, #5000 - - clarify borg init's encryption modes - - add note about patterns and stored paths, #4160 - - add upgrade of tools to pip installation how-to - - document one cause of orphaned chunks in check command, #2295 - - linked recommended restrictions to ssh public keys on borg servers in faq, #4946 - - -Version 1.1.11 (2020-03-08) ---------------------------- - -Compatibility notes: - -- When upgrading from borg 1.0.x to 1.1.x, please note: - - - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1. - - borg upgrade: you do not need to and you also should not run it. - - borg might ask some security-related questions once after upgrading. - You can answer them either manually or via environment variable. - One known case is if you use unencrypted repositories, then it will ask - about a unknown unencrypted repository one time. - - your first backup with 1.1.x might be significantly slower (it might - completely read, chunk, hash a lot files) - this is due to the - --files-cache mode change (and happens every time you change mode). - You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible - mode (but that is less safe for detecting changed files than the default). - See the --files-cache docs for details. -- 1.1.11 removes WSL autodetection (Windows 10 Subsystem for Linux). - If WSL still has a problem with sync_file_range, you need to set - BORG_WORKAROUNDS=basesyncfile in the borg process environment to - work around the WSL issue. - -Fixes: - -- fixed potential index corruption / data loss issue due to bug in hashindex_set, #4829. - Please read and follow the more detailed notes close to the top of this document. -- upgrade bundled xxhash to 0.7.3, #4891. - 0.7.2 is the minimum requirement for correct operations on ARMv6 in non-fixup - mode, where unaligned memory accesses cause bus errors. - 0.7.3 adds some speedups and libxxhash 0.7.3 even has a pkg-config file now. -- upgrade bundled lz4 to 1.9.2 -- upgrade bundled zstd to 1.4.4 -- fix crash when upgrading erroneous hints file, #4922 -- extract: - - - fix KeyError for "partial" extraction, #4607 - - fix "partial" extract for hardlinked contentless file types, #4725 - - fix preloading for old (0.xx) remote servers, #4652 - - fix confusing output of borg extract --list --strip-components, #4934 -- delete: after double-force delete, warn about necessary repair, #4704 -- create: give invalid repo error msg if repo config not found, #4411 -- mount: fix FUSE mount missing st_birthtime, #4763 #4767 -- check: do not stumble over invalid item key, #4845 -- info: if the archive doesn't exist, print a pretty message, #4793 -- SecurityManager.known(): check all files, #4614 -- Repository.open: use stat() to check for repo dir, #4695 -- Repository.check_can_create_repository: use stat() to check, #4695 -- fix invalid archive error message -- fix optional/non-optional location arg, #4541 -- commit-time free space calc: ignore bad compact map entries, #4796 -- ignore EACCES (errno 13) when hardlinking the old config, #4730 -- --prefix / -P: fix processing, avoid argparse issue, #4769 - -New features: - -- enable placeholder usage in all extra archive arguments -- new BORG_WORKAROUNDS mechanism, basesyncfile, #4710 -- recreate: support --timestamp option, #4745 -- support platforms without os.link (e.g. Android with Termux), #4901. - if we don't have os.link, we just extract another copy instead of making a hardlink. -- support linux platforms without sync_file_range (e.g. Android 7 with Termux), #4905 - -Other: - -- ignore --stats when given with --dry-run, but continue, #4373 -- add some ProgressIndicator msgids to code / fix docs, #4935 -- elaborate on "Calculating size" message -- argparser: always use REPOSITORY in metavar, also use more consistent help phrasing. -- check: improve error output for matching index size, see #4829 -- docs: - - - changelog: add advisory about hashindex_set bug #4829 - - better describe BORG_SECURITY_DIR, BORG_CACHE_DIR, #4919 - - infos about cache security assumptions, #4900 - - add FAQ describing difference between a local repo vs. repo on a server. - - document how to test exclusion patterns without performing an actual backup - - timestamps in the files cache are now usually ctime, #4583 - - fix bad reference to borg compact (does not exist in 1.1), #4660 - - create: borg 1.1 is not future any more - - extract: document limitation "needs empty destination", #4598 - - how to supply a passphrase, use crypto devices, #4549 - - fix osxfuse github link in installation docs - - add example of exclude-norecurse rule in help patterns - - update macOS Brew link - - add note about software for automating backups, #4581 - - AUTHORS: mention copyright+license for bundled msgpack - - fix various code blocks in the docs, #4708 - - updated docs to cover use of temp directory on remote, #4545 - - add restore docs, #4670 - - add a pull backup / push restore how-to, #1552 - - add FAQ how to retain original paths, #4532 - - explain difference between --exclude and --pattern, #4118 - - add FAQs for SSH connection issues, #3866 - - improve password FAQ, #4591 - - reiterate that 'file cache names are absolute' in FAQ -- tests: - - - cope with ANY error when importing pytest into borg.testsuite, #4652 - - fix broken test that relied on improper zlib assumptions - - test_fuse: filter out selinux xattrs, #4574 -- travis / vagrant: - - - misc python versions removed / changed (due to openssl 1.1 compatibility) - or added (3.7 and 3.8, for better borg compatibility testing) - - binary building is on python 3.5.9 now -- vagrant: - - - add new boxes: ubuntu 18.04 and 20.04, debian 10 - - update boxes: openindiana, darwin, netbsd - - remove old boxes: centos 6 - - darwin: updated osxfuse to 3.10.4 - - use debian/ubuntu pip/virtualenv packages - - rather use python 3.6.2 than 3.6.0, fixes coverage/sqlite3 issue - - use requirements.d/development.lock.txt to avoid compat issues -- travis: - - - darwin: backport some install code / order from master - - remove deprecated keyword "sudo" from travis config - - allow osx builds to fail, #4955 - this is due to travis-ci frequently being so slow that the OS X builds - just fail because they exceed 50 minutes and get killed by travis. - - -Version 1.1.10 (2019-05-16) ---------------------------- - -Fixes: - -- extract: hang on partial extraction with ssh: repo, when hardlink master - is not matched/extracted and borg hangs on related slave hardlink, #4350 -- lrucache: regularly remove old FDs, #4427 -- avoid stale filehandle issues, #3265 -- freebsd: make xattr platform code api compatible with linux, #3952 -- use whitelist approach for borg serve, #4097 -- borg command shall terminate with rc 2 for ImportErrors, #4424 -- create: only run stat_simple_attrs() once, this increases - backup with lots of unchanged files performance by ~ 5%. -- prune: fix incorrect borg prune --stats output with --dry-run, #4373 -- key export: emit user-friendly error if repo key is exported to a directory, - #4348 - -New features: - -- bundle latest supported msgpack-python release (0.5.6), remove msgpack-python - from setup.py install_requires - by default we use the bundled code now. - optionally, we still support using an external msgpack (see hints in - setup.py), but this requires solid requirements management within - distributions and is not recommended. - borgbackup will break if you upgrade msgpack to an unsupported version. -- display msgpack version as part of sysinfo (e.g. in tracebacks) -- timestamp for borg delete --info added, #4359 -- enable placeholder usage in --comment and --glob-archives, #4559, #4495 - -Other: - -- serve: do not check python/libc for borg serve, #4483 -- shell completions: borg diff second archive -- release scripts: signing binaries with Qubes OS support -- testing: - - - vagrant: upgrade openbsd box to 6.4 - - travis-ci: lock test env to py 3.4 compatible versions, #4343 - - get rid of confusing coverage warning, #2069 - - rename test_mount_hardlinks to test_fuse_mount_hardlinks, - so both can be excluded by "not test_fuse". - - pure-py msgpack warning shall not make a lot of tests fail, #4558 -- docs: - - - add "SSH Configuration" section to "borg serve", #3988, #636, #4485 - - README: new URL for funding options - - add a sample logging.conf in docs/misc, #4380 - - elaborate on append-only mode docs, #3504 - - installation: added Alpine Linux to distribution list, #4415 - - usage.html: only modify window.location when redirecting, #4133 - - add msgpack license to docs/3rd_party/msgpack -- vagrant / binary builds: - - - use python 3.5.7 for builds - - use osxfuse 3.8.3 - - -Version 1.1.9 (2019-02-10) --------------------------- - -Compatibility notes: - -- When upgrading from borg 1.0.x to 1.1.x, please note: - - - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1. - - borg upgrade: you do not need to and you also should not run it. - - borg might ask some security-related questions once after upgrading. - You can answer them either manually or via environment variable. - One known case is if you use unencrypted repositories, then it will ask - about a unknown unencrypted repository one time. - - your first backup with 1.1.x might be significantly slower (it might - completely read, chunk, hash a lot files) - this is due to the - --files-cache mode change (and happens every time you change mode). - You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible - mode (but that is less safe for detecting changed files than the default). - See the --files-cache docs for details. - -Fixes: - -- security fix: configure FUSE with "default_permissions", #3903 - "default_permissions" is now enforced by borg by default to let the - kernel check uid/gid/mode based permissions. - "ignore_permissions" can be given to not enforce "default_permissions". -- make "hostname" short, even on misconfigured systems, #4262 -- fix free space calculation on macOS (and others?), #4289 -- config: quit with error message when no key is provided, #4223 -- recover_segment: handle too small segment files correctly, #4272 -- correctly release memoryview, #4243 -- avoid diaper pattern in configparser by opening files, #4263 -- add "# cython: language_level=3" directive to .pyx files, #4214 -- info: consider part files for "This archive" stats, #3522 -- work around Microsoft WSL issue #645 (sync_file_range), #1961 - -New features: - -- add --rsh command line option to complement BORG_RSH env var, #1701 -- init: --make-parent-dirs parent1/parent2/repo_dir, #4235 - -Other: - -- add archive name to check --repair output, #3447 -- check for unsupported msgpack versions -- shell completions: - - - new shell completions for borg 1.1.9 - - more complete shell completions for borg mount -o - - added shell completions for borg help - - option arguments for zsh tab completion -- docs: - - - add FAQ regarding free disk space check, #3905 - - update BORG_PASSCOMMAND example and clarify variable expansion, #4249 - - FAQ regarding change of compression settings, #4222 - - add note about BSD flags to changelog, #4246 - - improve logging in example automation script - - add note about files changing during backup, #4081 - - work around the backslash issue, #4280 - - update release workflow using twine (docs, scripts), #4213 - - add warnings on repository copies to avoid future problems, #4272 -- tests: - - - fix the homebrew 1.9 issues on travis-ci, #4254 - - fix duplicate test method name, #4311 - - -Version 1.1.8 (2018-12-09) --------------------------- - -Fixes: - -- enforce storage quota if set by serve-command, #4093 -- invalid locations: give err msg containing parsed location, #4179 -- list repo: add placeholders for hostname and username, #4130 -- on linux, symlinks can't have ACLs, so don't try to set any, #4044 - -New features: - -- create: added PATH::archive output on INFO log level -- read a passphrase from a file descriptor specified in the - BORG_PASSPHRASE_FD environment variable. - -Other: - -- docs: - - - option --format is required for some expensive-to-compute values for json - - borg list by default does not compute expensive values except when - they are needed. whether they are needed is determined by the format, - in standard mode as well as in --json mode. - - tell that our binaries are x86/x64 amd/intel, bauerj has ARM - - fixed wrong archive name pattern in CRUD benchmark help - - fixed link to cachedir spec in docs, #4140 -- tests: - - - stop using fakeroot on travis, avoids sporadic EISDIR errors, #2482 - - xattr key names must start with "user." on linux - - fix code so flake8 3.6 does not complain - - explicitly convert environment variable to str, #4136 - - fix DeprecationWarning: Flags not at the start of the expression, #4137 - - support pytest4, #4172 -- vagrant: - - - use python 3.5.6 for builds - - -Version 1.1.7 (2018-08-11) --------------------------- - -Compatibility notes: - -- added support for Python 3.7 - -Fixes: - -- cache lock: use lock_wait everywhere to fix infinite wait, see #3968 -- don't archive tagged dir when recursing an excluded dir, #3991 -- py37 argparse: work around bad default in py 3.7.0a/b/rc, #3996 -- py37 remove loggerDict.clear() from tearDown method, #3805 -- some fixes for bugs which likely did not result in problems in practice: - - - fixed logic bug in platform module API version check - - fixed xattr/acl function prototypes, added missing ones - -New features: - -- init: add warning to store both key and passphrase at safe place(s) -- BORG_HOST_ID env var to work around all-zero MAC address issue, #3985 -- borg debug dump-repo-objs --ghost (dump everything from segment files, - including deleted or superseded objects or commit tags) -- borg debug search-repo-objs (search in repo objects for hex bytes or strings) - -Other changes: - -- add Python 3.7 support -- updated shell completions -- call socket.gethostname only once -- locking: better logging, add some asserts -- borg debug dump-repo-objs: - - - filename layout improvements - - use repository.scan() to get on-disk order -- docs: - - - update installation instructions for macOS - - added instructions to install fuse via homebrew - - improve diff docs - - added note that checkpoints inside files requires 1.1+ - - add link to tempfile module - - remove row/column-spanning from docs source, #4000 #3990 -- tests: - - - fetch less data via os.urandom - - add py37 env for tox - - travis: add 3.7, remove 3.6-dev (we test with -dev in master) -- vagrant / binary builds: - - - use osxfuse 3.8.2 - - use own (uptodate) openindiana box - - -Version 1.1.6 (2018-06-11) --------------------------- - -Compatibility notes: - -- 1.1.6 changes: - - - also allow msgpack-python 0.5.6. - -Fixes: - -- fix borg exception handling on ENOSPC error with xattrs, #3808 -- prune: fix/improve overall progress display -- borg config repo ... does not need cache/manifest/key, #3802 -- debug dump-repo-objs should not depend on a manifest obj -- pypi package: - - - include .coveragerc, needed by tox.ini - - fix package long description, #3854 - -New features: - -- mount: add uid, gid, umask mount options -- delete: - - - only commit once, #3823 - - implement --dry-run, #3822 -- check: - - - show progress while rebuilding missing manifest, #3787 - - more --repair output -- borg config --list , #3612 - -Other changes: - -- update msgpack requirement, #3753 -- update bundled zstd to 1.3.4, #3745 -- update bundled lz4 code to 1.8.2, #3870 -- docs: - - - describe what BORG_LIBZSTD_PREFIX does - - fix and deduplicate encryption quickstart docs, #3776 -- vagrant: - - - FUSE for macOS: upgrade 3.7.1 to 3.8.0 - - exclude macOS High Sierra upgrade on the darwin64 machine - - remove borgbackup.egg-info dir in fs_init (after rsync) - - use pyenv-based build/test on jessie32/62 - - use local 32 and 64bit debian jessie boxes - - use "vagrant" as username for new xenial box -- travis OS X: use xcode 8.3 (not broken) - - -Version 1.1.5 (2018-04-01) --------------------------- - -Compatibility notes: - -- 1.1.5 changes: - - - require msgpack-python >= 0.4.6 and < 0.5.0. - 0.5.0+ dropped python 3.4 testing and also caused some other issues because - the python package was renamed to msgpack and emitted some FutureWarning. - -Fixes: - -- create --list: fix that it was never showing M status, #3492 -- create: fix timing for first checkpoint (read files cache early, init - checkpoint timer after that), see #3394 -- extract: set rc=1 when extracting damaged files with all-zero replacement - chunks or with size inconsistencies, #3448 -- diff: consider an empty file as different to a non-existing file, #3688 -- files cache: improve exception handling, #3553 -- ignore exceptions in scandir_inorder() caused by an implicit stat(), - also remove unneeded sort, #3545 -- fixed tab completion problem where a space is always added after path even - when it shouldn't -- build: do .h file content checks in binary mode, fixes build issue for - non-ascii header files on pure-ascii locale platforms, #3544 #3639 -- borgfs: fix patterns/paths processing, #3551 -- config: add some validation, #3566 -- repository config: add validation for max_segment_size, #3592 -- set cache previous_location on load instead of save -- remove platform.uname() call which caused library mismatch issues, #3732 -- add exception handler around deprecated platform.linux_distribution() call -- use same datetime object for {now} and {utcnow}, #3548 - -New features: - -- create: implement --stdin-name, #3533 -- add chunker_params to borg archive info (--json) -- BORG_SHOW_SYSINFO=no to hide system information from exceptions - -Other changes: - -- updated zsh completions for borg 1.1.4 -- files cache related code cleanups -- be more helpful when parsing invalid --pattern values, #3575 -- be more clear in secure-erase warning message, #3591 -- improve getpass user experience, #3689 -- docs build: unicode problem fixed when using a py27-based sphinx -- docs: - - - security: explicitly note what happens OUTSIDE the attack model - - security: add note about combining compression and encryption - - security: describe chunk size / proximity issue, #3687 - - quickstart: add note about permissions, borg@localhost, #3452 - - quickstart: add introduction to repositories & archives, #3620 - - recreate --recompress: add missing metavar, clarify description, #3617 - - improve logging docs, #3549 - - add an example for --pattern usage, #3661 - - clarify path semantics when matching, #3598 - - link to offline documentation from README, #3502 - - add docs on how to verify a signed release with GPG, #3634 - - chunk seed is generated per repository (not: archive) - - better formatting of CPU usage documentation, #3554 - - extend append-only repo rollback docs, #3579 -- tests: - - - fix erroneously skipped zstd compressor tests, #3606 - - skip a test if argparse is broken, #3705 -- vagrant: - - - xenial64 box now uses username 'vagrant', #3707 - - move cleanup steps to fs_init, #3706 - - the boxcutter wheezy boxes are 404, use local ones - - update to Python 3.5.5 (for binary builds) - - -Version 1.1.4 (2017-12-31) --------------------------- - -Compatibility notes: - -- When upgrading from borg 1.0.x to 1.1.x, please note: - - - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1. - - borg upgrade: you do not need to and you also should not run it. - - borg might ask some security-related questions once after upgrading. - You can answer them either manually or via environment variable. - One known case is if you use unencrypted repositories, then it will ask - about a unknown unencrypted repository one time. - - your first backup with 1.1.x might be significantly slower (it might - completely read, chunk, hash a lot files) - this is due to the - --files-cache mode change (and happens every time you change mode). - You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible - mode (but that is less safe for detecting changed files than the default). - See the --files-cache docs for details. -- borg 1.1.4 changes: - - - zstd compression is new in borg 1.1.4, older borg can't handle it. - - new minimum requirements for the compression libraries - if the required - versions (header and lib) can't be found at build time, bundled code will - be used: - - - added requirement: libzstd >= 1.3.0 (bundled: 1.3.2) - - updated requirement: liblz4 >= 1.7.0 / r129 (bundled: 1.8.0) - -Fixes: - -- check: data corruption fix: fix for borg check --repair malfunction, #3444. - See the more detailed notes close to the top of this document. -- delete: also delete security dir when deleting a repo, #3427 -- prune: fix building the "borg prune" man page, #3398 -- init: use given --storage-quota for local repo, #3470 -- init: properly quote repo path in output -- fix startup delay with dns-only own fqdn resolving, #3471 - -New features: - -- added zstd compression. try it! -- added placeholder {reverse-fqdn} for fqdn in reverse notation -- added BORG_BASE_DIR environment variable, #3338 - -Other changes: - -- list help topics when invalid topic is requested -- fix lz4 deprecation warning, requires lz4 >= 1.7.0 (r129) -- add parens for C preprocessor macro argument usages (did not cause malfunction) -- exclude broken pytest 3.3.0 release -- updated fish/bash completions -- init: more clear exception messages for borg create, #3465 -- docs: - - - add auto-generated docs for borg config - - don't generate HTML docs page for borgfs, #3404 - - docs update for lz4 b2 zstd changes - - add zstd to compression help, readme, docs - - update requirements and install docs about bundled lz4 and zstd -- refactored build of the compress and crypto.low_level extensions, #3415: - - - move some lib/build related code to setup_{zstd,lz4,b2}.py - - bundle lz4 1.8.0 (requirement: >= 1.7.0 / r129) - - bundle zstd 1.3.2 (requirement: >= 1.3.0) - - blake2 was already bundled - - rename BORG_LZ4_PREFIX env var to BORG_LIBLZ4_PREFIX for better consistency: - we also have BORG_LIBB2_PREFIX and BORG_LIBZSTD_PREFIX now. - - add prefer_system_lib* = True settings to setup.py - by default the build - will prefer a shared library over the bundled code, if library and headers - can be found and meet the minimum requirements. - - -Version 1.1.3 (2017-11-27) --------------------------- - -Fixes: - -- Security Fix for CVE-2017-15914: Incorrect implementation of access controls - allows remote users to override repository restrictions in Borg servers. - A user able to access a remote Borg SSH server is able to circumvent access - controls post-authentication. - Affected releases: 1.1.0, 1.1.1, 1.1.2. Releases 1.0.x are NOT affected. -- crc32: deal with unaligned buffer, add tests - this broke borg on older ARM - CPUs that can not deal with unaligned 32bit memory accesses and raise a bus - error in such cases. the fix might also improve performance on some CPUs as - all 32bit memory accesses by the crc32 code are properly aligned now. #3317 -- mount: fixed support of --consider-part-files and do not show .borg_part_N - files by default in the mounted FUSE filesystem. #3347 -- fixed cache/repo timestamp inconsistency message, highlight that information - is obtained from security dir (deleting the cache will not bypass this error - in case the user knows this is a legitimate repo). -- borgfs: don't show sub-command in borgfs help, #3287 -- create: show an error when --dry-run and --stats are used together, #3298 - -New features: - -- mount: added exclusion group options and paths, #2138 - - Reused some code to support similar options/paths as borg extract offers - - making good use of these to only mount a smaller subset of dirs/files can - speed up mounting a lot and also will consume way less memory. - - borg mount [options] repo_or_archive mountpoint path [paths...] - - paths: you can just give some "root paths" (like for borg extract) to - only partially populate the FUSE filesystem. - - new options: --exclude[-from], --pattern[s-from], --strip-components -- create/extract: support st_birthtime on platforms supporting it, #3272 -- add "borg config" command for querying/setting/deleting config values, #3304 - -Other changes: - -- clean up and simplify packaging (only package committed files, do not install - .c/.h/.pyx files) -- docs: - - - point out tuning options for borg create, #3239 - - add instructions for using ntfsclone, zerofree, #81 - - move image backup-related FAQ entries to a new page - - clarify key aliases for borg list --format, #3111 - - mention break-lock in checkpointing FAQ entry, #3328 - - document sshfs rename workaround, #3315 - - add FAQ about removing files from existing archives - - add FAQ about different prune policies - - usage and man page for borgfs, #3216 - - clarify create --stats duration vs. wall time, #3301 - - clarify encrypted key format for borg key export, #3296 - - update release checklist about security fixes - - document good and problematic option placements, fix examples, #3356 - - add note about using --nobsdflags to avoid speed penalty related to - bsdflags, #3239 - - move most of support section to www.borgbackup.org - - -Version 1.1.2 (2017-11-05) --------------------------- - -Fixes: - -- fix KeyError crash when talking to borg server < 1.0.7, #3244 -- extract: set bsdflags last (include immutable flag), #3263 -- create: don't do stat() call on excluded-norecurse directory, fix exception - handling for stat() call, #3209 -- create --stats: do not count data volume twice when checkpointing, #3224 -- recreate: move chunks_healthy when excluding hardlink master, #3228 -- recreate: get rid of chunks_healthy when rechunking (does not match), #3218 -- check: get rid of already existing not matching chunks_healthy metadata, #3218 -- list: fix stdout broken pipe handling, #3245 -- list/diff: remove tag-file options (not used), #3226 - -New features: - -- bash, zsh and fish shell auto-completions, see scripts/shell_completions/ -- added BORG_CONFIG_DIR env var, #3083 - -Other changes: - -- docs: - - - clarify using a blank passphrase in keyfile mode - - mention "!" (exclude-norecurse) type in "patterns" help - - document to first heal before running borg recreate to re-chunk stuff, - because that will have to get rid of chunks_healthy metadata. - - more than 23 is not supported for CHUNK_MAX_EXP, #3115 - - borg does not respect nodump flag by default any more - - clarify same-filesystem requirement for borg upgrade, #2083 - - update / rephrase cygwin / WSL status, #3174 - - improve docs about --stats, #3260 -- vagrant: openindiana new clang package - -Already contained in 1.1.1 (last minute fix): - -- arg parsing: fix fallback function, refactor, #3205. This is a fixup - for #3155, which was broken on at least python <= 3.4.2. - - -Version 1.1.1 (2017-10-22) --------------------------- - -Compatibility notes: - -- The deprecated --no-files-cache is not a global/common option any more, - but only available for borg create (it is not needed for anything else). - Use --files-cache=disabled instead of --no-files-cache. -- The nodump flag ("do not backup this file") is not honoured any more by - default because this functionality (esp. if it happened by error or - unexpected) was rather confusing and unexplainable at first to users. - If you want that "do not backup NODUMP-flagged files" behaviour, use: - borg create --exclude-nodump ... -- If you are on Linux and do not need bsdflags archived, consider using - ``--nobsdflags`` with ``borg create`` to avoid additional syscalls and - speed up backup creation. - -Fixes: - -- borg recreate: correctly compute part file sizes. fixes cosmetic, but - annoying issue as borg check complains about size inconsistencies of part - files in affected archives. you can solve that by running borg recreate on - these archives, see also #3157. -- bsdflags support: do not open BLK/CHR/LNK files, avoid crashes and - slowness, #3130 -- recreate: don't crash on attic archives w/o time_end, #3109 -- don't crash on repository filesystems w/o hardlink support, #3107 -- don't crash in first part of truncate_and_unlink, #3117 -- fix server-side IndexError crash with clients < 1.0.7, #3192 -- don't show traceback if only a global option is given, show help, #3142 -- cache: use SaveFile for more safety, #3158 -- init: fix wrong encryption choices in command line parser, fix missing - "authenticated-blake2", #3103 -- move --no-files-cache from common to borg create options, #3146 -- fix detection of non-local path (failed on ..filename), #3108 -- logging with fileConfig: set json attr on "borg" logger, #3114 -- fix crash with relative BORG_KEY_FILE, #3197 -- show excluded dir with "x" for tagged dirs / caches, #3189 - -New features: - -- create: --nobsdflags and --exclude-nodump options, #3160 -- extract: --nobsdflags option, #3160 - -Other changes: - -- remove annoying hardlinked symlinks warning, #3175 -- vagrant: use self-made FreeBSD 10.3 box, #3022 -- travis: don't brew update, hopefully fixes #2532 -- docs: - - - readme: -e option is required in borg 1.1 - - add example showing --show-version --show-rc - - use --format rather than --list-format (deprecated) in example - - update docs about hardlinked symlinks limitation - - -Version 1.1.0 (2017-10-07) --------------------------- - -Compatibility notes: - -- borg command line: do not put options in between positional arguments - - This sometimes works (e.g. it worked in borg 1.0.x), but can easily stop - working if we make positional arguments optional (like it happened for - borg create's "paths" argument in 1.1). There are also places in borg 1.0 - where we do that, so it doesn't work there in general either. #3356 - - Good: borg create -v --stats repo::archive path - Good: borg create repo::archive path -v --stats - Bad: borg create repo::archive -v --stats path - -Fixes: - -- fix LD_LIBRARY_PATH restoration for subprocesses, #3077 -- "auto" compression: make sure expensive compression is actually better, - otherwise store lz4 compressed data we already computed. - -Other changes: - -- docs: - - - FAQ: we do not implement futile attempts of ETA / progress displays - - manpage: fix typos, update homepage - - implement simple "issue" role for manpage generation, #3075 - - -Version 1.1.0rc4 (2017-10-01) ------------------------------ - -Compatibility notes: - -- A borg server >= 1.1.0rc4 does not support borg clients 1.1.0b3-b5. #3033 -- The files cache is now controlled differently and has a new default mode: - - - the files cache now uses ctime by default for improved file change - detection safety. You can still use mtime for more speed and less safety. - - --ignore-inode is deprecated (use --files-cache=... without "inode") - - --no-files-cache is deprecated (use --files-cache=disabled) - -New features: - -- --files-cache - implement files cache mode control, #911 - You can now control the files cache mode using this option: - --files-cache={ctime,mtime,size,inode,rechunk,disabled} - (only some combinations are supported). See the docs for details. - -Fixes: - -- remote progress/logging: deal with partial lines, #2637 -- remote progress: flush json mode output -- fix subprocess environments, #3050 (and more) - -Other changes: - -- remove client_supports_log_v3 flag, #3033 -- exclude broken Cython 0.27(.0) in requirements, #3066 -- vagrant: - - - upgrade to FUSE for macOS 3.7.1 - - use Python 3.5.4 to build the binaries -- docs: - - - security: change-passphrase only changes the passphrase, #2990 - - fixed/improved borg create --compression examples, #3034 - - add note about metadata dedup and --no[ac]time, #2518 - - twitter account @borgbackup now, better visible, #2948 - - simplified rate limiting wrapper in FAQ - - -Version 1.1.0rc3 (2017-09-10) ------------------------------ - -New features: - -- delete: support naming multiple archives, #2958 - -Fixes: - -- repo cleanup/write: invalidate cached FDs, #2982 -- fix datetime.isoformat() microseconds issues, #2994 -- recover_segment: use mmap(), lower memory needs, #2987 - -Other changes: - -- with-lock: close segment file before invoking subprocess -- keymanager: don't depend on optional readline module, #2976 -- docs: - - - fix macOS keychain integration command - - show/link new screencasts in README, #2936 - - document utf-8 locale requirement for json mode, #2273 -- vagrant: clean up shell profile init, user name, #2977 -- test_detect_attic_repo: don't test mount, #2975 -- add debug logging for repository cleanup - - -Version 1.1.0rc2 (2017-08-28) ------------------------------ - -Compatibility notes: - -- list: corrected mix-up of "isomtime" and "mtime" formats. Previously, - "isomtime" was the default but produced a verbose human format, - while "mtime" produced a ISO-8601-like format. - The behaviours have been swapped (so "mtime" is human, "isomtime" is ISO-like), - and the default is now "mtime". - "isomtime" is now a real ISO-8601 format ("T" between date and time, not a space). - -New features: - -- None. - -Fixes: - -- list: fix weird mixup of mtime/isomtime -- create --timestamp: set start time, #2957 -- ignore corrupt files cache, #2939 -- migrate locks to child PID when daemonize is used -- fix exitcode of borg serve, #2910 -- only compare contents when chunker params match, #2899 -- umount: try fusermount, then try umount, #2863 - -Other changes: - -- JSON: use a more standard ISO 8601 datetime format, #2376 -- cache: write_archive_index: truncate_and_unlink on error, #2628 -- detect non-upgraded Attic repositories, #1933 -- delete various nogil and threading related lines -- coala / pylint related improvements -- docs: - - - renew asciinema/screencasts, #669 - - create: document exclusion through nodump, #2949 - - minor formatting fixes - - tar: tarpipe example - - improve "with-lock" and "info" docs, #2869 - - detail how to use macOS/GNOME/KDE keyrings for repo passwords, #392 -- travis: only short-circuit docs-only changes for pull requests -- vagrant: - - - netbsd: bash is already installed - - fix netbsd version in PKG_PATH - - add exe location to PATH when we build an exe - - -Version 1.1.0rc1 (2017-07-24) ------------------------------ - -Compatibility notes: - -- delete: removed short option for --cache-only - -New features: - -- support borg list repo --format {comment} {bcomment} {end}, #2081 -- key import: allow reading from stdin, #2760 - -Fixes: - -- with-lock: avoid creating segment files that might be overwritten later, #1867 -- prune: fix checkpoints processing with --glob-archives -- FUSE: versions view: keep original file extension at end, #2769 -- fix --last, --first: do not accept values <= 0, - fix reversed archive ordering with --last -- include testsuite data (attic.tar.gz) when installing the package -- use limited unpacker for outer key, for manifest (both security precautions), - #2174 #2175 -- fix bashism in shell scripts, #2820, #2816 -- cleanup endianness detection, create _endian.h, - fixes build on alpine linux, #2809 -- fix crash with --no-cache-sync (give known chunk size to chunk_incref), #2853 - -Other changes: - -- FUSE: versions view: linear numbering by archive time -- split up interval parsing from filtering for --keep-within, #2610 -- add a basic .editorconfig, #2734 -- use archive creation time as mtime for FUSE mount, #2834 -- upgrade FUSE for macOS (osxfuse) from 3.5.8 to 3.6.3, #2706 -- hashindex: speed up by replacing modulo with "if" to check for wraparound -- coala checker / pylint: fixed requirements and .coafile, more ignores -- borg upgrade: name backup directories as 'before-upgrade', #2811 -- add .mailmap -- some minor changes suggested by lgtm.com -- docs: - - - better explanation of the --ignore-inode option relevance, #2800 - - fix openSUSE command and add openSUSE section - - simplify ssh authorized_keys file using "restrict", add legacy note, #2121 - - mount: show usage of archive filters - - mount: add repository example, #2462 - - info: update and add examples, #2765 - - prune: include example - - improved style / formatting - - improved/fixed segments_per_dir docs - - recreate: fix wrong "remove unwanted files" example - - reference list of status chars in borg recreate --filter description - - update source-install docs about doc build dependencies, #2795 - - cleanup installation docs - - file system requirements, update segs per dir - - fix checkpoints/parts reference in FAQ, #2859 -- code: - - - hashindex: don't pass side effect into macro - - crypto low_level: don't mutate local bytes() - - use dash_open function to open file or "-" for stdin/stdout - - archiver: argparse cleanup / refactoring - - shellpattern: add match_end arg -- tests: added some additional unit tests, some fixes, #2700 #2710 -- vagrant: fix setup of cygwin, add Debian 9 "stretch" -- travis: don't perform full travis build on docs-only changes, #2531 - - -Version 1.1.0b6 (2017-06-18) ----------------------------- - -Compatibility notes: - -- 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 "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 - 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. - -- 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: - -- 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, 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 -- 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 -- 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: - -- 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 - - 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 -- 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: - -- 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 -- 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 - - 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 - - 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: - - - 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 - - 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: - - - 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) ----------------------------- - -Compatibility notes: - -- BORG_HOSTNAME_IS_UNIQUE is now on by default. -- removed --compression-from feature -- recreate: add --recompress flag, unify --always-recompress and - --recompress - -Fixes: - -- 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, #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: - -- 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 -- 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 - - 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") - - 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: - -- 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) ----------------------------- - -Compatibility notes: - -- 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 - --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 and .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.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: - -- 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 -- posix platform module: only build / import on non-win32 platforms, #2041 - -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 #1253 -- 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) ----------------------------- - -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. -- 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) -- 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) ----------------------------- - -New features: - -- new commands: - - - 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), - 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 - - 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 -- 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 - - 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 -- 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 -- 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 fewer 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: - - - Group options - - Nicer list of options in Sphinx - - Deduplicate 'Common options' (including --help) - - 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: - - - vagrant: add ubuntu/xenial 64bit - this box has still some issues - - ChunkBuffer: add test for leaving partial chunk in buffer, fixes #945 - - -Version 1.0.13 (2019-02-15) ---------------------------- - -Please note: this is very likely the last 1.0.x release, please upgrade to 1.1.x. - -Bug fixes: - -- security fix: configure FUSE with "default_permissions", #3903. - "default_permissions" is now enforced by borg by default to let the - kernel check uid/gid/mode based permissions. - "ignore_permissions" can be given to not enforce "default_permissions". -- xattrs: fix borg exception handling on ENOSPC error, #3808. - -New features: - -- Read a passphrase from a file descriptor specified in the - BORG_PASSPHRASE_FD environment variable. - -Other changes: - -- acl platform code: fix acl set return type -- xattr: - - - add linux {list,get,set}xattr ctypes prototypes - - fix darwin flistxattr ctypes prototype -- testing / travis-ci: - - - fix the homebrew 1.9 issues on travis-ci, #4254 - - travis OS X: use xcode 8.3 (not broken) - - tox.ini: lock requirements - - unbreak 1.0-maint on travis, fixes #4123 -- vagrant: - - - misc. fixes - - FUSE for macOS: upgrade 3.7.1 to 3.8.3 - - Python: upgrade 3.5.5 to 3.5.6 -- docs: - - - Update installation instructions for macOS - - update release workflow using twine (docs, scripts), #4213 - -Version 1.0.12 (2018-04-08) ---------------------------- - -Bug fixes: - -- repository: cleanup/write: invalidate cached FDs, tests -- serve: fix exitcode, #2910 -- extract: set bsdflags last (include immutable flag), #3263 -- create --timestamp: set start time, #2957 -- create: show excluded dir with "x" for tagged dirs / caches, #3189 -- migrate locks to child PID when daemonize is used -- Buffer: fix wrong thread-local storage use, #2951 -- fix detection of non-local path, #3108 -- fix LDLP restoration for subprocesses, #3077 -- fix subprocess environments (xattr module's fakeroot version check, - borg umount, BORG_PASSCOMMAND), #3050 -- remote: deal with partial lines, #2637 -- get rid of datetime.isoformat, use safe parse_timestamp to parse - timestamps, #2994 -- build: do .h file content checks in binary mode, fixes build issue for - non-ascii header files on pure-ascii locale platforms, #3544 #3639 -- remove platform.uname() call which caused library mismatch issues, #3732 -- add exception handler around deprecated platform.linux_distribution() call - -Other changes: - -- require msgpack-python >= 0.4.6 and < 0.5.0, see #3753 -- add parens for C preprocessor macro argument usages (did not cause - malfunction) -- ignore corrupt files cache, #2939 -- replace "modulo" with "if" to check for wraparound in hashmap -- keymanager: don't depend on optional readline module, #2980 -- exclude broken pytest 3.3.0 release -- exclude broken Cython 0.27(.0) release, #3066 -- flake8: add some ignores -- docs: - - - create: document exclusion through nodump - - document good and problematic option placements, fix examples, #3356 - - update docs about hardlinked symlinks limitation - - faq: we do not implement futile attempts of ETA / progress displays - - simplified rate limiting wrapper in FAQ - - twitter account @borgbackup, #2948 - - add note about metadata dedup and --no[ac]time, #2518 - - change-passphrase only changes the passphrase, #2990 - - clarify encrypted key format for borg key export, #3296 - - document sshfs rename workaround, #3315 - - update release checklist about security fixes - - docs about how to verify a signed release, #3634 - - chunk seed is generated per /repository/ -- vagrant: - - - use FUSE for macOS 3.7.1 to build the macOS binary - - use python 3.5.5 to build the binaries - - add exe location to PATH when we build an exe - - use https pypi url for wheezy - - netbsd: bash is already installed - - netbsd: fix netbsd version in PKG_PATH - - use self-made FreeBSD 10.3 box, #3022 - - backport fs_init (including related updates) from 1.1 - - the boxcutter wheezy boxes are 404, use local ones -- travis: - - - don't perform full Travis build on docs-only changes, #2531 - - only short-circuit docs-only changes for pull requests - - -Version 1.0.11 (2017-07-21) ---------------------------- - -Bug fixes: - -- use limited unpacker for outer key (security precaution), #2174 -- fix paperkey import bug - -Other changes: - -- change --checkpoint-interval default from 600s to 1800s, #2841. - this improves efficiency for big repositories a lot. -- docs: fix OpenSUSE command and add OpenSUSE section -- tests: add tests for split_lstring and paperkey -- vagrant: - - - fix openbsd shell - - backport cpu/ram setup from master - - add stretch64 VM - -Version 1.0.11rc1 (2017-06-27) ------------------------------- - -Bug fixes: - -- performance: rebuild hashtable if we have too few empty buckets, #2246. - this fixes some sporadic, but severe performance breakdowns. -- Archive: allocate zeros when needed, #2308 - fixes huge memory usage of mount (8 MiB × number of archives) -- IPv6 address support - also: Location: more informative exception when parsing fails -- borg single-file binary: use pyinstaller v3.2.1, #2396 - this fixes that the prelink cronjob on some distros kills the - borg binary by stripping away parts of it. -- extract: - - - warning for unextracted big extended attributes, #2258 - - also create parent dir for device files, if needed. - - don't write to disk with --stdout, #2645 -- archive check: detect and fix missing all-zero replacement chunks, #2180 -- fix (de)compression exceptions, #2224 #2221 -- files cache: update inode number, #2226 -- borg rpc: use limited msgpack.Unpacker (security precaution), #2139 -- Manifest: use limited msgpack.Unpacker (security precaution), #2175 -- Location: accept //servername/share/path -- fix ChunkIndex.__contains__ assertion for big-endian archs (harmless) -- create: handle BackupOSError on a per-path level in one spot -- fix error msg, there is no --keep-last in borg 1.0.x, #2282 -- clamp (nano)second values to unproblematic range, #2304 -- fuse / borg mount: - - - fix st_blocks to be an integer (not float) value - - fix negative uid/gid crash (they could come into archives e.g. when - backing up external drives under cygwin), #2674 - - fix crash if empty (None) xattr is read - - do pre-mount checks before opening repository - - check llfuse is installed before asking for passphrase -- borg rename: expand placeholders, #2386 -- borg serve: fix forced command lines containing BORG_* env vars -- fix error msg, it is --keep-within, not --within -- fix borg key/debug/benchmark crashing without subcommand, #2240 -- chunker: fix invalid use of types, don't do uint32_t >> 32 -- document follow_symlinks requirements, check libc, #2507 - -New features: - -- added BORG_PASSCOMMAND environment variable, #2573 -- add minimal version of in repository mandatory feature flags, #2134 - - 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. -- borg delete --force --force to delete severely corrupted archives, #1975 - -Other changes: - -- embrace y2038 issue to support 32bit platforms -- be more clear that this is a "beyond repair" case, #2427 -- key file names: limit to 100 characters and remove colons from host name -- upgrade FUSE for macOS to 3.5.8, #2346 -- split up parsing and filtering for --keep-within, better error message, #2610 -- docs: - - - fix caskroom link, #2299 - - address SSH batch mode, #2202 #2270 - - improve remote-path description - - document snapshot usage, #2178 - - document relative path usage, #1868 - - one link per distro in the installation page - - development: new branching model in git repository - - kill api page - - added FAQ section about backing up root partition - - add bountysource badge, #2558 - - create empty docs.txt reequirements, #2694 - - README: how to help the project - - note -v/--verbose requirement on affected options, #2542 - - document borg init behaviour via append-only borg serve, #2440 - - be clear about what buzhash is used for (chunking) 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, #2390 - - add hint about chunker params to borg upgrade docs, #2421 - - clarify borg upgrade docs, #2436 - - quickstart: delete problematic BORG_PASSPHRASE use, #2623 - - faq: specify "using inline shell scripts" - - document pattern denial of service, #2624 -- tests: - - - remove attic dependency of the tests, #2505 - - travis: - - - enhance travis setuptools_scm situation - - install fakeroot for Linux - - add test for borg delete --force - - enable remote tests on cygwin (the cygwin issue that caused these tests - to break was fixed in cygwin at least since cygwin 2.8, maybe even since - 2.7.0). - - remove skipping the noatime tests on GNU/Hurd, #2710 - - fix borg import issue, add comment, #2718 - - include attic.tar.gz when installing the package - also: add include_package_data=True - -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 -- setup.py: build_api: sort file list for determinism - - -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). -- 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. -- 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. -- 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 - - 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 - - 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 - - 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) --------------------------- - -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: - - - rebuild manifest if it's corrupted - - 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: - -- 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 - - get back pytest's pretty assertion failures, #1938 -- 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) ------------------------------ - -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) --------------------------- - -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 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. - - 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) ------------------------------ - -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, #1622 -- fixed exception borg serve raised when connection was closed before repository - was opened. 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 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 -- pyinstaller: use a spec file to build borg.exe binary, exclude osxfuse dylib - on Mac OS X (avoids mismatch lib <-> driver), #1619 - -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. -- add "borg debug-refcount-obj" to determine a repo objects' referrer counts, - #1352 - -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. -- 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, #1700 -- vagrant (testing/build VMs): - - - 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 -- 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 - - 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) --------------------------- - -Security fixes: - -- 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 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 - 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 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). - -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) ------------------------------ - -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". -- 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 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 - 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) ------------------------------ - -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 unnecessary 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) --------------------------- - -Bug fixes: - -- Linux: handle multiple LD_PRELOAD entries correctly, #1314, #1111 -- Fix crash with unclear message if the libc is not found, #1314, #1111 - -Other changes: - -- tests: - - - Fixed O_NOATIME tests for Solaris and GNU Hurd, #1315 - - Fixed sparse file tests for (file) systems not supporting it, #1310 -- docs: - - - Fixed syntax highlighting, #1313 - - misc docs: added data processing overview picture - - -Version 1.0.6rc1 (2016-07-10) ------------------------------ - -New features: - -- borg check --repair: heal damaged files if missing chunks re-appear (e.g. if - the previously missing chunk was added again in a later backup archive), - #148. (*) Also improved logging. - -Bug fixes: - -- sync_dir: silence fsync() failing with EINVAL, #1287 - Some network filesystems (like smbfs) don't support this and we use this in - repository code. -- borg mount (FUSE): - - - fix directories being shadowed when contained paths were also specified, - #1295 - - raise I/O Error (EIO) on damaged files (unless -o allow_damaged_files is - used), #1302. (*) -- borg extract: warn if a damaged file is extracted, #1299. (*) -- Added some missing return code checks (ChunkIndex._add, hashindex_resize). -- borg check: fix/optimize initial hash table size, avoids resize of the table. - -Other changes: - -- tests: - - - add more FUSE tests, #1284 - - deduplicate FUSE (u)mount code - - fix borg binary test issues, #862 -- docs: - - - changelog: added release dates to older borg releases - - fix some sphinx (docs generator) warnings, #881 - -Notes: - -(*) Some features depend on information (chunks_healthy list) added to item -metadata when a file with missing chunks was "repaired" using all-zero -replacement chunks. The chunks_healthy list is generated since borg 1.0.4, -thus borg can't recognize such "repaired" (but content-damaged) files if the -repair was done with an older borg version. - - -Version 1.0.5 (2016-07-07) --------------------------- - -Bug fixes: - -- borg mount: fix FUSE crash in xattr code on Linux introduced in 1.0.4, #1282 - -Other changes: - -- backport some FAQ entries from master branch -- add release helper scripts -- Vagrantfile: - - - centos6: no FUSE, don't build binary - - add xz for redhat-like dists - - -Version 1.0.4 (2016-07-07) --------------------------- - -New features: - -- borg serve --append-only, #1168 - This was included because it was a simple change (append-only functionality - was already present via repository config file) and makes better security now - practically usable. -- BORG_REMOTE_PATH environment variable, #1258 - This was included because it was a simple change (--remote-path cli option - was already present) and makes borg much easier to use if you need it. -- Repository: cleanup incomplete transaction on "no space left" condition. - In many cases, this can avoid a 100% full repo filesystem (which is very - problematic as borg always needs free space - even to delete archives). - -Bug fixes: - -- Fix wrong handling and reporting of OSErrors in borg create, #1138. - This was a serious issue: in the context of "borg create", errors like - repository I/O errors (e.g. disk I/O errors, ssh repo connection errors) - were handled badly and did not lead to a crash (which would be good for this - case, because the repo transaction would be incomplete and trigger a - transaction rollback to clean up). - Now, error handling for source files is cleanly separated from every other - error handling, so only problematic input files are logged and skipped. -- Implement fail-safe error handling for borg extract. - Note that this isn't nearly as critical as the borg create error handling - bug, since nothing is written to the repo. So this was "merely" misleading - error reporting. -- Add missing error handler in directory attr restore loop. -- repo: make sure write data hits disk before the commit tag (#1236) and also - sync the containing directory. -- FUSE: getxattr fail must use errno.ENOATTR, #1126 - (fixes Mac OS X Finder malfunction: "zero bytes" file length, access denied) -- borg check --repair: do not lose information about the good/original chunks. - If we do not lose the original chunk IDs list when "repairing" a file - (replacing missing chunks with all-zero chunks), we have a chance to "heal" - the file back into its original state later, in case the chunks re-appear - (e.g. in a fresh backup). Healing is not implemented yet, see #148. -- fixes for --read-special mode: - - - ignore known files cache, #1241 - - fake regular file mode, #1214 - - improve symlinks handling, #1215 -- remove passphrase from subprocess environment, #1105 -- Ignore empty index file (will trigger index rebuild), #1195 -- add missing placeholder support for --prefix, #1027 -- improve exception handling for placeholder replacement -- catch and format exceptions in arg parsing -- helpers: fix "undefined name 'e'" in exception handler -- better error handling for missing repo manifest, #1043 -- borg delete: - - - make it possible to delete a repo without manifest - - borg delete --forced allows one to delete corrupted archives, #1139 -- borg check: - - - make borg check work for empty repo - - fix resync and msgpacked item qualifier, #1135 - - rebuild_manifest: fix crash if 'name' or 'time' key were missing. - - better validation of item metadata dicts, #1130 - - better validation of archive metadata dicts -- close the repo on exit - even if rollback did not work, #1197. - This is rather cosmetic, it avoids repo closing in the destructor. - -- tests: - - - fix sparse file test, #1170 - - flake8: ignore new F405, #1185 - - catch "invalid argument" on cygwin, #257 - - fix sparseness assertion in test prep, #1264 - -Other changes: - -- make borg build/work on OpenSSL 1.0 and 1.1, #1187 -- docs / help: - - - fix / clarify prune help, #1143 - - fix "patterns" help formatting - - add missing docs / help about placeholders - - resources: rename atticmatic to borgmatic - - document sshd settings, #545 - - more details about checkpoints, add split trick, #1171 - - support docs: add freenode web chat link, #1175 - - add prune visualization / example, #723 - - add note that Fnmatch is default, #1247 - - make clear that lzma levels > 6 are a waste of cpu cycles - - add a "do not edit" note to auto-generated files, #1250 - - update cygwin installation docs -- repository interoperability with borg master (1.1dev) branch: - - - borg check: read item metadata keys from manifest, #1147 - - read v2 hints files, #1235 - - fix hints file "unknown version" error handling bug -- tests: add tests for format_line -- llfuse: update version requirement for freebsd -- Vagrantfile: - - - use openbsd 5.9, #716 - - do not install llfuse on netbsd (broken) - - update OSXfuse to version 3.3.3 - - use Python 3.5.2 to build the binaries -- glibc compatibility checker: scripts/glibc_check.py -- add .eggs to .gitignore - - -Version 1.0.3 (2016-05-20) --------------------------- - -Bug fixes: - -- prune: avoid that checkpoints are kept and completed archives are deleted in - a prune run), #997 -- prune: fix commandline argument validation - some valid command lines were - considered invalid (annoying, but harmless), #942 -- fix capabilities extraction on Linux (set xattrs last, after chown()), #1069 -- repository: fix commit tags being seen in data -- when probing key files, do binary reads. avoids crash when non-borg binary - files are located in borg's key files directory. -- handle SIGTERM and make a clean exit - avoids orphan lock files. -- repository cache: don't cache large objects (avoid using lots of temp. disk - space), #1063 - -Other changes: - -- Vagrantfile: OS X: update osxfuse / install lzma package, #933 -- setup.py: add check for platform_darwin.c -- setup.py: on freebsd, use a llfuse release that builds ok -- docs / help: - - - update readthedocs URLs, #991 - - add missing docs for "borg break-lock", #992 - - borg create help: add some words to about the archive name - - borg create help: document format tags, #894 - - -Version 1.0.2 (2016-04-16) --------------------------- - -Bug fixes: - -- fix malfunction and potential corruption on (nowadays rather rare) big-endian - architectures or bi-endian archs in (rare) BE mode. #886, #889 - - cache resync / index merge was malfunctioning due to this, potentially - leading to data loss. borg info had cosmetic issues (displayed wrong values). - - note: all (widespread) little-endian archs (like x86/x64) or bi-endian archs - in (widespread) LE mode (like ARMEL, MIPSEL, ...) were NOT affected. -- 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 occurred. -- borg delete: fix --cache-only for broken caches, #874 - - Makes --cache-only idempotent: it won't fail if the cache is already deleted. -- fixed borg create --one-file-system erroneously traversing into other - filesystems (if starting fs device number was 0), #873 -- workaround a bug in Linux fadvise FADV_DONTNEED, #907 - -Other changes: - -- better test coverage for hashindex, incl. overflow testing, checking correct - computations so endianness issues would be discovered. -- reproducible doc for ProgressIndicator*, make the build reproducible. -- use latest llfuse for vagrant machines -- docs: - - - use /path/to/repo in examples, fixes #901 - - fix confusing usage of "repo" as archive name (use "arch") - - -Version 1.0.1 (2016-04-08) --------------------------- - -New features: - -Usually there are no new features in a bugfix release, but these were added -due to their high impact on security/safety/speed or because they are fixes -also: - -- append-only mode for repositories, #809, #36 (see docs) -- borg create: add --ignore-inode option to make borg detect unmodified files - even if your filesystem does not have stable inode numbers (like sshfs and - possibly CIFS). -- add options --warning, --error, --critical for missing log levels, #826. - it's not recommended to suppress warnings or errors, but the user may decide - this on his own. - note: --warning is not given to borg serve so a <= 1.0.0 borg will still - work as server (it is not needed as it is the default). - do not use --error or --critical when using a <= 1.0.0 borg server. - -Bug fixes: - -- fix silently skipping EIO, #748 -- add context manager for Repository (avoid orphan repository locks), #285 -- do not sleep for >60s while waiting for lock, #773 -- unpack file stats before passing to FUSE -- fix build on illumos -- don't try to backup doors or event ports (Solaris and derivatives) -- remove useless/misleading libc version display, #738 -- test suite: reset exit code of persistent archiver, #844 -- RemoteRepository: clean up pipe if remote open() fails -- Remote: don't print tracebacks for Error exceptions handled downstream, #792 -- if BORG_PASSPHRASE is present but wrong, don't prompt for password, but fail - instead, #791 -- ArchiveChecker: move "orphaned objects check skipped" to INFO log level, #826 -- fix capitalization, add ellipses, change log level to debug for 2 messages, - #798 - -Other changes: - -- update llfuse requirement, llfuse 1.0 works -- update OS / dist packages on build machines, #717 -- prefer showing --info over -v in usage help, #859 -- docs: - - - fix cygwin requirements (gcc-g++) - - document how to debug / file filesystem issues, #664 - - fix reproducible build of api docs - - RTD theme: CSS !important overwrite, #727 - - Document logo font. Recreate logo png. Remove GIMP logo file. - - -Version 1.0.0 (2016-03-05) --------------------------- - -The major release number change (0.x -> 1.x) indicates bigger incompatible -changes, please read the compatibility notes, adapt / test your scripts and -check your backup logs. - -Compatibility notes: - -- drop support for python 3.2 and 3.3, require 3.4 or 3.5, #221 #65 #490 - note: we provide binaries that include python 3.5.1 and everything else - needed. they are an option in case you are stuck with < 3.4 otherwise. -- change encryption to be on by default (using "repokey" mode) -- moved keyfile keys from ~/.borg/keys to ~/.config/borg/keys, - you can either move them manually or run "borg upgrade " -- remove support for --encryption=passphrase, - use borg migrate-to-repokey to switch to repokey mode, #97 -- remove deprecated --compression , - use --compression zlib, instead - in case of 0, you could also use --compression none -- remove deprecated --hourly/daily/weekly/monthly/yearly - use --keep-hourly/daily/weekly/monthly/yearly instead -- remove deprecated --do-not-cross-mountpoints, - use --one-file-system instead -- disambiguate -p option, #563: - - - -p now is same as --progress - - -P now is same as --prefix -- remove deprecated "borg verify", - use "borg extract --dry-run" instead -- cleanup environment variable semantics, #355 - the environment variables used to be "yes sayers" when set, this was - conceptually generalized to "automatic answerers" and they just give their - value as answer (as if you typed in that value when being asked). - See the "usage" / "Environment Variables" section of the docs for details. -- change the builtin default for --chunker-params, create 2MiB chunks, #343 - --chunker-params new default: 19,23,21,4095 - old default: 10,23,16,4095 - - one of the biggest issues with borg < 1.0 (and also attic) was that it had a - default target chunk size of 64kiB, thus it created a lot of chunks and thus - also a huge chunk management overhead (high RAM and disk usage). - - please note that the new default won't change the chunks that you already - have in your repository. the new big chunks do not deduplicate with the old - small chunks, so expect your repo to grow at least by the size of every - changed file and in the worst case (e.g. if your files cache was lost / is - not used) by the size of every file (minus any compression you might use). - - in case you want to immediately see a much lower resource usage (RAM / disk) - for chunks management, it might be better to start with a new repo than - continuing in the existing repo (with an existing repo, you'ld have to wait - until all archives with small chunks got pruned to see a lower resource - usage). - - if you used the old --chunker-params default value (or if you did not use - --chunker-params option at all) and you'ld like to continue using small - chunks (and you accept the huge resource usage that comes with that), just - explicitly use borg create --chunker-params=10,23,16,4095. -- archive timestamps: the 'time' timestamp now refers to archive creation - start time (was: end time), the new 'time_end' timestamp refers to archive - creation end time. This might affect prune if your backups take rather long. - if you give a timestamp via cli this is stored into 'time', therefore it now - needs to mean archive creation start time. - -New features: - -- implement password roundtrip, #695 - -Bug fixes: - -- remote end does not need cache nor keys directories, do not create them, #701 -- added retry counter for passwords, #703 - -Other changes: - -- fix compiler warnings, #697 -- docs: - - - update README.rst to new changelog location in docs/changes.rst - - add Teemu to AUTHORS - - changes.rst: fix old chunker params, #698 - - FAQ: how to limit bandwidth - - -Version 1.0.0rc2 (2016-02-28) ------------------------------ - -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 -- add a simple archivename validator, #680 -- borg create --stats: show timestamps in localtime, use same labels/formatting - as borg info, #651 -- llfuse compatibility fixes (now compatible with: 0.40, 0.41, 0.42) - -Other changes: - -- it is now possible to use "pip install borgbackup[fuse]" to automatically - install the llfuse dependency using the correct version requirement - for it. you still need to care about having installed the FUSE / build - related OS package first, though, so that building llfuse can succeed. -- 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 - - 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 - - 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 - - update example for borg create -v --stats (use iso datetime format), #663 - - added example to BORG_RSH docs - - "connection closed by remote": add FAQ entry and point to issue #636 - - -Version 1.0.0rc1 (2016-02-07) ------------------------------ - -New features: - -- borg migrate-to-repokey ("passphrase" -> "repokey" encryption key mode) -- implement --short for borg list REPO, #611 -- implement --list for borg extract (consistency with borg create) -- borg serve: overwrite client's --restrict-to-path with ssh forced command's - option value (but keep everything else from the client commandline), #544 -- use $XDG_CONFIG_HOME/keys for keyfile keys (~/.config/borg/keys), #515 -- "borg upgrade" moves the keyfile keys to the new location -- display both archive creation start and end time in "borg info", #627 - - -Bug fixes: - -- normalize trailing slashes for the repository path, #606 -- Cache: fix exception handling in __init__, release lock, #610 - -Other changes: - -- suppress unneeded exception context (PEP 409), simpler tracebacks -- removed special code needed to deal with imperfections / incompatibilities / - missing stuff in py 3.2/3.3, simplify code that can be done simpler in 3.4 -- removed some version requirements that were kept on old versions because - newer did not support py 3.2 any more -- use some py 3.4+ stdlib code instead of own/openssl/pypi code: - - - use os.urandom instead of own cython openssl RAND_bytes wrapper, #493 - - use hashlib.pbkdf2_hmac from py stdlib instead of own openssl wrapper - - use hmac.compare_digest instead of == operator (constant time comparison) - - use stat.filemode instead of homegrown code - - use "mock" library from stdlib, #145 - - remove borg.support (with non-broken argparse copy), it is ok in 3.4+, #358 -- Vagrant: copy CHANGES.rst as symlink, #592 -- cosmetic code cleanups, add flake8 to tox/travis, #4 -- docs / help: - - - make "borg -h" output prettier, #591 - - slightly rephrase prune help - - add missing example for --list option of borg create - - quote exclude line that includes an asterisk to prevent shell expansion - - fix dead link to license - - delete Ubuntu Vivid, it is not supported anymore (EOL) - - OS X binary does not work for older OS X releases, #629 - - borg serve's special support for forced/original ssh commands, #544 - - misc. updates and fixes - - -Version 0.30.0 (2016-01-23) ---------------------------- - -Compatibility notes: - -- you may need to use -v (or --info) more often to actually see output emitted - at INFO log level (because it is suppressed at the default WARNING log level). - See the "general" section in the usage docs. -- for borg create, you need --list (additionally to -v) to see the long file - list (was needed so you can have e.g. --stats alone without the long list) -- see below about BORG_DELETE_I_KNOW_WHAT_I_AM_DOING (was: - BORG_CHECK_I_KNOW_WHAT_I_AM_DOING) - -Bug fixes: - -- fix crash when using borg create --dry-run --keep-tag-files, #570 -- make sure teardown with cleanup happens for Cache and RepositoryCache, - avoiding leftover locks and TEMP dir contents, #285 (partially), #548 -- fix locking KeyError, partial fix for #502 -- log stats consistently, #526 -- add abbreviated weekday to timestamp format, fixes #496 -- strip whitespace when loading exclusions from file -- unset LD_LIBRARY_PATH before invoking ssh, fixes strange OpenSSL library - version warning when using the borg binary, #514 -- add some error handling/fallback for C library loading, #494 -- added BORG_DELETE_I_KNOW_WHAT_I_AM_DOING for check in "borg delete", #503 -- remove unused "repair" rpc method name - -New features: - -- borg create: implement exclusions using regular expression patterns. -- borg create: implement inclusions using patterns. -- borg extract: support patterns, #361 -- support different styles for patterns: - - - fnmatch (`fm:` prefix, default when omitted), like borg <= 0.29. - - shell (`sh:` prefix) with `*` not matching directory separators and - `**/` matching 0..n directories - - path prefix (`pp:` prefix, for unifying borg create pp1 pp2 into the - patterns system), semantics like in borg <= 0.29 - - regular expression (`re:`), new! -- --progress option for borg upgrade (#291) and borg delete -- update progress indication more often (e.g. for borg create within big - files or for borg check repo), #500 -- finer chunker granularity for items metadata stream, #547, #487 -- borg create --list now used (additionally to -v) to enable the verbose - file list output -- display borg version below tracebacks, #532 - -Other changes: - -- hashtable size (and thus: RAM and disk consumption) follows a growth policy: - grows fast while small, grows slower when getting bigger, #527 -- Vagrantfile: use pyinstaller 3.1 to build binaries, freebsd sqlite3 fix, - fixes #569 -- no separate binaries for centos6 any more because the generic linux binaries - also work on centos6 (or in general: on systems with a slightly older glibc - than debian7 -- dev environment: require virtualenv<14.0 so we get a py32 compatible pip -- docs: - - - add space-saving chunks.archive.d trick to FAQ - - important: clarify -v and log levels in usage -> general, please read! - - sphinx configuration: create a simple man page from usage docs - - add a repo server setup example - - disable unneeded SSH features in authorized_keys examples for security. - - borg prune only knows "--keep-within" and not "--within" - - add gource video to resources docs, #507 - - add netbsd install instructions - - authors: make it more clear what refers to borg and what to attic - - document standalone binary requirements, #499 - - rephrase the mailing list section - - development docs: run build_api and build_usage before tagging release - - internals docs: hash table max. load factor is 0.75 now - - markup, typo, grammar, phrasing, clarifications and other fixes. - - add gcc gcc-c++ to redhat/fedora/corora install docs, fixes #583 - - -Version 0.29.0 (2015-12-13) ---------------------------- - -Compatibility notes: - -- when upgrading to 0.29.0 you need to upgrade client as well as server - installations due to the locking and commandline interface changes otherwise - you'll get an error msg about a RPC protocol mismatch or a wrong commandline - option. - if you run a server that needs to support both old and new clients, it is - suggested that you have a "borg-0.28.2" and a "borg-0.29.0" command. - clients then can choose via e.g. "borg --remote-path=borg-0.29.0 ...". -- the default waiting time for a lock changed from infinity to 1 second for a - better interactive user experience. if the repo you want to access is - currently locked, borg will now terminate after 1s with an error message. - if you have scripts that shall wait for the lock for a longer time, use - --lock-wait N (with N being the maximum wait time in seconds). - -Bug fixes: - -- hash table tuning (better chosen hashtable load factor 0.75 and prime initial - size of 1031 gave ~1000x speedup in some scenarios) -- avoid creation of an orphan lock for one case, #285 -- --keep-tag-files: fix file mode and multiple tag files in one directory, #432 -- fixes for "borg upgrade" (attic repo converter), #466 -- remove --progress isatty magic (and also --no-progress option) again, #476 -- borg init: display proper repo URL -- fix format of umask in help pages, #463 - -New features: - -- implement --lock-wait, support timeout for UpgradableLock, #210 -- implement borg break-lock command, #157 -- include system info below traceback, #324 -- sane remote logging, remote stderr, #461: - - - remote log output: intercept it and log it via local logging system, - with "Remote: " prefixed to message. log remote tracebacks. - - remote stderr: output it to local stderr with "Remote: " prefixed. -- add --debug and --info (same as --verbose) to set the log level of the - builtin logging configuration (which otherwise defaults to warning), #426 - note: there are few messages emitted at DEBUG level currently. -- optionally configure logging via env var BORG_LOGGING_CONF -- add --filter option for status characters: e.g. to show only the added - or modified files (and also errors), use "borg create -v --filter=AME ...". -- more progress indicators, #394 -- use ISO-8601 date and time format, #375 -- "borg check --prefix" to restrict archive checking to that name prefix, #206 - -Other changes: - -- hashindex_add C implementation (speed up cache re-sync for new archives) -- increase FUSE read_size to 1024 (speed up metadata operations) -- check/delete/prune --save-space: free unused segments quickly, #239 -- increase rpc protocol version to 2 (see also Compatibility notes), #458 -- silence borg by default (via default log level WARNING) -- get rid of C compiler warnings, #391 -- upgrade OS X FUSE to 3.0.9 on the OS X binary build system -- use python 3.5.1 to build binaries -- docs: - - - new mailing list borgbackup@python.org, #468 - - readthedocs: color and logo improvements - - load coverage icons over SSL (avoids mixed content) - - more precise binary installation steps - - update release procedure docs about OS X FUSE - - FAQ entry about unexpected 'A' status for unchanged file(s), #403 - - add docs about 'E' file status - - add "borg upgrade" docs, #464 - - add developer docs about output and logging - - clarify encryption, add note about client-side encryption - - add resources section, with videos, talks, presentations, #149 - - Borg moved to Arch Linux [community] - - fix wrong installation instructions for archlinux - - -Version 0.28.2 (2015-11-15) ---------------------------- - -New features: - -- borg create --exclude-if-present TAGFILE - exclude directories that have the - given file from the backup. You can additionally give --keep-tag-files to - preserve just the directory roots and the tag-files (but not backup other - directory contents), #395, attic #128, attic #142 - -Other changes: - -- do not create docs sources at build time (just have them in the repo), - completely remove have_cython() hack, do not use the "mock" library at build - time, #384 -- avoid hidden import, make it easier for PyInstaller, easier fix for #218 -- docs: - - - add description of item flags / status output, fixes #402 - - explain how to regenerate usage and API files (build_api or - build_usage) and when to commit usage files directly into git, #384 - - minor install docs improvements - - -Version 0.28.1 (2015-11-08) ---------------------------- - -Bug fixes: - -- do not try to build api / usage docs for production install, - fixes unexpected "mock" build dependency, #384 - -Other changes: - -- avoid using msgpack.packb at import time -- fix formatting issue in changes.rst -- fix build on readthedocs - - -Version 0.28.0 (2015-11-08) ---------------------------- - -Compatibility notes: - -- changed return codes (exit codes), see docs. in short: - old: 0 = ok, 1 = error. now: 0 = ok, 1 = warning, 2 = error - -New features: - -- refactor return codes (exit codes), fixes #61 -- add --show-rc option enable "terminating with X status, rc N" output, fixes 58, #351 -- borg create backups atime and ctime additionally to mtime, fixes #317 - - extract: support atime additionally to mtime - - FUSE: support ctime and atime additionally to mtime -- support borg --version -- emit a warning if we have a slow msgpack installed -- borg list --prefix=thishostname- REPO, fixes #205 -- Debug commands (do not use except if you know what you do: debug-get-obj, - debug-put-obj, debug-delete-obj, debug-dump-archive-items. - -Bug fixes: - -- setup.py: fix bug related to BORG_LZ4_PREFIX processing -- fix "check" for repos that have incomplete chunks, fixes #364 -- borg mount: fix unlocking of repository at umount time, fixes #331 -- fix reading files without touching their atime, #334 -- non-ascii ACL fixes for Linux, FreeBSD and OS X, #277 -- fix acl_use_local_uid_gid() and add a test for it, attic #359 -- borg upgrade: do not upgrade repositories in place by default, #299 -- fix cascading failure with the index conversion code, #269 -- borg check: implement 'cmdline' archive metadata value decoding, #311 -- fix RobustUnpacker, it missed some metadata keys (new atime and ctime keys - were missing, but also bsdflags). add check for unknown metadata keys. -- create from stdin: also save atime, ctime (cosmetic) -- use default_notty=False for confirmations, fixes #345 -- vagrant: fix msgpack installation on centos, fixes #342 -- deal with unicode errors for symlinks in same way as for regular files and - have a helpful warning message about how to fix wrong locale setup, fixes #382 -- add ACL keys the RobustUnpacker must know about - -Other changes: - -- improve file size displays, more flexible size formatters -- explicitly commit to the units standard, #289 -- archiver: add E status (means that an error occurred when processing this - (single) item -- do binary releases via "github releases", closes #214 -- create: use -x and --one-file-system (was: --do-not-cross-mountpoints), #296 -- a lot of changes related to using "logging" module and screen output, #233 -- show progress display if on a tty, output more progress information, #303 -- factor out status output so it is consistent, fix surrogates removal, - maybe fixes #309 -- move away from RawConfigParser to ConfigParser -- archive checker: better error logging, give chunk_id and sequence numbers - (can be used together with borg debug-dump-archive-items). -- do not mention the deprecated passphrase mode -- emit a deprecation warning for --compression N (giving a just a number) -- misc .coverragerc fixes (and coverage measurement improvements), fixes #319 -- refactor confirmation code, reduce code duplication, add tests -- prettier error messages, fixes #307, #57 -- tests: - - - add a test to find disk-full issues, #327 - - travis: also run tests on Python 3.5 - - travis: use tox -r so it rebuilds the tox environments - - test the generated pyinstaller-based binary by archiver unit tests, #215 - - vagrant: tests: announce whether fakeroot is used or not - - vagrant: add vagrant user to fuse group for debianoid systems also - - vagrant: llfuse install on darwin needs pkgconfig installed - - vagrant: use pyinstaller from develop branch, fixes #336 - - benchmarks: test create, extract, list, delete, info, check, help, fixes #146 - - benchmarks: test with both the binary and the python code - - archiver tests: test with both the binary and the python code, fixes #215 - - make basic test more robust -- docs: - - - moved docs to borgbackup.readthedocs.org, #155 - - a lot of fixes and improvements, use mobile-friendly RTD standard theme - - use zlib,6 compression in some examples, fixes #275 - - add missing rename usage to docs, closes #279 - - include the help offered by borg help in the usage docs, fixes #293 - - include a list of major changes compared to attic into README, fixes #224 - - add OS X install instructions, #197 - - more details about the release process, #260 - - fix linux glibc requirement (binaries built on debian7 now) - - build: move usage and API generation to setup.py - - update docs about return codes, #61 - - remove api docs (too much breakage on rtd) - - borgbackup install + basics presentation (asciinema) - - describe the current style guide in documentation - - add section about debug commands - - warn about not running out of space - - add example for rename - - improve chunker params docs, fixes #362 - - minor development docs update - - -Version 0.27.0 (2015-10-07) ---------------------------- - -New features: - -- "borg upgrade" command - attic -> borg one time converter / migration, #21 -- temporary hack to avoid using lots of disk space for chunks.archive.d, #235: - To use it: rm -rf chunks.archive.d ; touch chunks.archive.d -- respect XDG_CACHE_HOME, attic #181 -- add support for arbitrary SSH commands, attic #99 -- borg delete --cache-only REPO (only delete cache, not REPO), attic #123 - - -Bug fixes: - -- use Debian 7 (wheezy) to build pyinstaller borgbackup binaries, fixes slow - down observed when running the Centos6-built binary on Ubuntu, #222 -- do not crash on empty lock.roster, fixes #232 -- fix multiple issues with the cache config version check, #234 -- fix segment entry header size check, attic #352 - plus other error handling improvements / code deduplication there. -- always give segment and offset in repo IntegrityErrors - - -Other changes: - -- stop producing binary wheels, remove docs about it, #147 -- docs: - - add warning about prune - - generate usage include files only as needed - - development docs: add Vagrant section - - update / improve / reformat FAQ - - hint to single-file pyinstaller binaries from README - - -Version 0.26.1 (2015-09-28) ---------------------------- - -This is a minor update, just docs and new pyinstaller binaries. - -- docs update about python and binary requirements -- better docs for --read-special, fix #220 -- re-built the binaries, fix #218 and #213 (glibc version issue) -- update web site about single-file pyinstaller binaries - -Note: if you did a python-based installation, there is no need to upgrade. - - -Version 0.26.0 (2015-09-19) ---------------------------- - -New features: - -- Faster cache sync (do all in one pass, remove tar/compression stuff), #163 -- BORG_REPO env var to specify the default repo, #168 -- read special files as if they were regular files, #79 -- implement borg create --dry-run, attic issue #267 -- Normalize paths before pattern matching on OS X, #143 -- support OpenBSD and NetBSD (except xattrs/ACLs) -- support / run tests on Python 3.5 - -Bug fixes: - -- borg mount repo: use absolute path, attic #200, attic #137 -- chunker: use off_t to get 64bit on 32bit platform, #178 -- initialize chunker fd to -1, so it's not equal to STDIN_FILENO (0) -- fix reaction to "no" answer at delete repo prompt, #182 -- setup.py: detect lz4.h header file location -- to support python < 3.2.4, add less buggy argparse lib from 3.2.6 (#194) -- fix for obtaining ``char *`` from temporary Python value (old code causes - a compile error on Mint 17.2) -- llfuse 0.41 install troubles on some platforms, require < 0.41 - (UnicodeDecodeError exception due to non-ascii llfuse setup.py) -- cython code: add some int types to get rid of unspecific python add / - subtract operations (avoid ``undefined symbol FPE_``... error on some platforms) -- fix verbose mode display of stdin backup -- extract: warn if a include pattern never matched, fixes #209, - implement counters for Include/ExcludePatterns -- archive names with slashes are invalid, attic issue #180 -- chunker: add a check whether the POSIX_FADV_DONTNEED constant is defined - - fixes building on OpenBSD. - -Other changes: - -- detect inconsistency / corruption / hash collision, #170 -- replace versioneer with setuptools_scm, #106 -- docs: - - - pkg-config is needed for llfuse installation - - be more clear about pruning, attic issue #132 -- unit tests: - - - xattr: ignore security.selinux attribute showing up - - ext3 seems to need a bit more space for a sparse file - - do not test lzma level 9 compression (avoid MemoryError) - - work around strange mtime granularity issue on netbsd, fixes #204 - - ignore st_rdev if file is not a block/char device, fixes #203 - - stay away from the setgid and sticky mode bits -- use Vagrant to do easy cross-platform testing (#196), currently: - - - Debian 7 "wheezy" 32bit, Debian 8 "jessie" 64bit - - Ubuntu 12.04 32bit, Ubuntu 14.04 64bit - - Centos 7 64bit - - FreeBSD 10.2 64bit - - OpenBSD 5.7 64bit - - NetBSD 6.1.5 64bit - - Darwin (OS X Yosemite) - - -Version 0.25.0 (2015-08-29) ---------------------------- - -Compatibility notes: - -- lz4 compression library (liblz4) is a new requirement (#156) -- the new compression code is very compatible: as long as you stay with zlib - compression, older borg releases will still be able to read data from a - repo/archive made with the new code (note: this is not the case for the - default "none" compression, use "zlib,0" if you want a "no compression" mode - that can be read by older borg). Also the new code is able to read repos and - archives made with older borg versions (for all zlib levels 0..9). - -Deprecations: - -- --compression N (with N being a number, as in 0.24) is deprecated. - We keep the --compression 0..9 for now to not break scripts, but it is - deprecated and will be removed later, so better fix your scripts now: - --compression 0 (as in 0.24) is the same as --compression zlib,0 (now). - BUT: if you do not want compression, you rather want --compression none - (which is the default). - --compression 1 (in 0.24) is the same as --compression zlib,1 (now) - --compression 9 (in 0.24) is the same as --compression zlib,9 (now) - -New features: - -- create --compression none (default, means: do not compress, just pass through - data "as is". this is more efficient than zlib level 0 as used in borg 0.24) -- create --compression lz4 (super-fast, but not very high compression) -- create --compression zlib,N (slower, higher compression, default for N is 6) -- create --compression lzma,N (slowest, highest compression, default N is 6) -- honor the nodump flag (UF_NODUMP) and do not backup such items -- list --short just outputs a simple list of the files/directories in an archive - -Bug fixes: - -- fixed --chunker-params parameter order confusion / malfunction, fixes #154 -- close fds of segments we delete (during compaction) -- close files which fell out the lrucache -- fadvise DONTNEED now is only called for the byte range actually read, not for - the whole file, fixes #158. -- fix issue with negative "all archives" size, fixes #165 -- restore_xattrs: ignore if setxattr fails with EACCES, fixes #162 - -Other changes: - -- remove fakeroot requirement for tests, tests run faster without fakeroot - (test setup does not fail any more without fakeroot, so you can run with or - without fakeroot), fixes #151 and #91. -- more tests for archiver -- recover_segment(): don't assume we have an fd for segment -- lrucache refactoring / cleanup, add dispose function, py.test tests -- generalize hashindex code for any key length (less hardcoding) -- lock roster: catch file not found in remove() method and ignore it -- travis CI: use requirements file -- improved docs: - - - replace hack for llfuse with proper solution (install libfuse-dev) - - update docs about compression - - update development docs about fakeroot - - internals: add some words about lock files / locking system - - support: mention BountySource and for what it can be used - - theme: use a lighter green - - add pypi, wheel, dist package based install docs - - split install docs into system-specific preparations and generic instructions - - -Version 0.24.0 (2015-08-09) ---------------------------- - -Incompatible changes (compared to 0.23): - -- borg now always issues --umask NNN option when invoking another borg via ssh - on the repository server. By that, it's making sure it uses the same umask - for remote repos as for local ones. Because of this, you must upgrade both - server and client(s) to 0.24. -- the default umask is 077 now (if you do not specify via --umask) which might - be a different one as you used previously. The default umask avoids that - you accidentally give access permissions for group and/or others to files - created by borg (e.g. the repository). - -Deprecations: - -- "--encryption passphrase" mode is deprecated, see #85 and #97. - See the new "--encryption repokey" mode for a replacement. - -New features: - -- borg create --chunker-params ... to configure the chunker, fixes #16 - (attic #302, attic #300, and somehow also #41). - This can be used to reduce memory usage caused by chunk management overhead, - so borg does not create a huge chunks index/repo index and eats all your RAM - if you back up lots of data in huge files (like VM disk images). - See docs/misc/create_chunker-params.txt for more information. -- borg info now reports chunk counts in the chunk index. -- borg create --compression 0..9 to select zlib compression level, fixes #66 - (attic #295). -- borg init --encryption repokey (to store the encryption key into the repo), - fixes #85 -- improve at-end error logging, always log exceptions and set exit_code=1 -- LoggedIO: better error checks / exceptions / exception handling -- implement --remote-path to allow non-default-path borg locations, #125 -- implement --umask M and use 077 as default umask for better security, #117 -- borg check: give a named single archive to it, fixes #139 -- cache sync: show progress indication -- cache sync: reimplement the chunk index merging in C - -Bug fixes: - -- fix segfault that happened for unreadable files (chunker: n needs to be a - signed size_t), #116 -- fix the repair mode, #144 -- repo delete: add destroy to allowed rpc methods, fixes issue #114 -- more compatible repository locking code (based on mkdir), maybe fixes #92 - (attic #317, attic #201). -- better Exception msg if no Borg is installed on the remote repo server, #56 -- create a RepositoryCache implementation that can cope with >2GiB, - fixes attic #326. -- fix Traceback when running check --repair, attic #232 -- clarify help text, fixes #73. -- add help string for --no-files-cache, fixes #140 - -Other changes: - -- improved docs: - - - added docs/misc directory for misc. writeups that won't be included - "as is" into the html docs. - - document environment variables and return codes (attic #324, attic #52) - - web site: add related projects, fix web site url, IRC #borgbackup - - Fedora/Fedora-based install instructions added to docs - - Cygwin-based install instructions added to docs - - updated AUTHORS - - add FAQ entries about redundancy / integrity - - clarify that borg extract uses the cwd as extraction target - - update internals doc about chunker params, memory usage and compression - - added docs about development - - add some words about resource usage in general - - document how to backup a raw disk - - add note about how to run borg from virtual env - - add solutions for (ll)fuse installation problems - - document what borg check does, fixes #138 - - reorganize borgbackup.github.io sidebar, prev/next at top - - deduplicate and refactor the docs / README.rst - -- use borg-tmp as prefix for temporary files / directories -- short prune options without "keep-" are deprecated, do not suggest them -- improved tox configuration -- remove usage of unittest.mock, always use mock from pypi -- use entrypoints instead of scripts, for better use of the wheel format and - modern installs -- add requirements.d/development.txt and modify tox.ini -- use travis-ci for testing based on Linux and (new) OS X -- use coverage.py, pytest-cov and codecov.io for test coverage support - -I forgot to list some stuff already implemented in 0.23.0, here they are: - -New features: - -- efficient archive list from manifest, meaning a big speedup for slow - repo connections and "list ", "delete ", "prune" (attic #242, - attic #167) -- big speedup for chunks cache sync (esp. for slow repo connections), fixes #18 -- hashindex: improve error messages - -Other changes: - -- explicitly specify binary mode to open binary files -- some easy micro optimizations - - -Version 0.23.0 (2015-06-11) ---------------------------- - -Incompatible changes (compared to attic, fork related): - -- changed sw name and cli command to "borg", updated docs -- package name (and name in urls) uses "borgbackup" to have fewer collisions -- changed repo / cache internal magic strings from ATTIC* to BORG*, - changed cache location to .cache/borg/ - this means that it currently won't - accept attic repos (see issue #21 about improving that) - -Bug fixes: - -- avoid defect python-msgpack releases, fixes attic #171, fixes attic #185 -- fix traceback when trying to do unsupported passphrase change, fixes attic #189 -- datetime does not like the year 10.000, fixes attic #139 -- fix "info" all archives stats, fixes attic #183 -- fix parsing with missing microseconds, fixes attic #282 -- fix misleading hint the fuse ImportError handler gave, fixes attic #237 -- check unpacked data from RPC for tuple type and correct length, fixes attic #127 -- fix Repository._active_txn state when lock upgrade fails -- give specific path to xattr.is_enabled(), disable symlink setattr call that - always fails -- fix test setup for 32bit platforms, partial fix for attic #196 -- upgraded versioneer, PEP440 compliance, fixes attic #257 - -New features: - -- less memory usage: add global option --no-cache-files -- check --last N (only check the last N archives) -- check: sort archives in reverse time order -- rename repo::oldname newname (rename repository) -- create -v output more informative -- create --progress (backup progress indicator) -- create --timestamp (utc string or reference file/dir) -- create: if "-" is given as path, read binary from stdin -- extract: if --stdout is given, write all extracted binary data to stdout -- extract --sparse (simple sparse file support) -- extra debug information for 'fread failed' -- delete (deletes whole repo + local cache) -- FUSE: reflect deduplication in allocated blocks -- only allow whitelisted RPC calls in server mode -- normalize source/exclude paths before matching -- use posix_fadvise to not spoil the OS cache, fixes attic #252 -- toplevel error handler: show tracebacks for better error analysis -- sigusr1 / sigint handler to print current file infos - attic PR #286 -- RPCError: include the exception args we get from remote - -Other changes: - -- source: misc. cleanups, pep8, style -- docs and faq improvements, fixes, updates -- cleanup crypto.pyx, make it easier to adapt to other AES modes -- do os.fsync like recommended in the python docs -- source: Let chunker optionally work with os-level file descriptor. -- source: Linux: remove duplicate os.fsencode calls -- source: refactor _open_rb code a bit, so it is more consistent / regular -- source: refactor indicator (status) and item processing -- source: use py.test for better testing, flake8 for code style checks -- source: fix tox >=2.0 compatibility (test runner) -- pypi package: add python version classifiers, add FreeBSD to platforms - - -Attic Changelog ---------------- - -Here you can see the full list of changes between each Attic release until Borg -forked from Attic: - -Version 0.17 -~~~~~~~~~~~~ - -(bugfix release, released on X) - -- Fix hashindex ARM memory alignment issue (#309) -- Improve hashindex error messages (#298) - -Version 0.16 -~~~~~~~~~~~~ - -(bugfix release, released on May 16, 2015) - -- Fix typo preventing the security confirmation prompt from working (#303) -- Improve handling of systems with improperly configured file system encoding (#289) -- Fix "All archives" output for attic info. (#183) -- More user friendly error message when repository key file is not found (#236) -- Fix parsing of iso 8601 timestamps with zero microseconds (#282) - -Version 0.15 -~~~~~~~~~~~~ - -(bugfix release, released on Apr 15, 2015) - -- xattr: Be less strict about unknown/unsupported platforms (#239) -- Reduce repository listing memory usage (#163). -- Fix BrokenPipeError for remote repositories (#233) -- Fix incorrect behavior with two character directory names (#265, #268) -- Require approval before accessing relocated/moved repository (#271) -- Require approval before accessing previously unknown unencrypted repositories (#271) -- Fix issue with hash index files larger than 2GB. -- Fix Python 3.2 compatibility issue with noatime open() (#164) -- Include missing pyx files in dist files (#168) - -Version 0.14 -~~~~~~~~~~~~ - -(feature release, released on Dec 17, 2014) - -- Added support for stripping leading path segments (#95) - "attic extract --strip-segments X" -- Add workaround for old Linux systems without acl_extended_file_no_follow (#96) -- Add MacPorts' path to the default openssl search path (#101) -- HashIndex improvements, eliminates unnecessary IO on low memory systems. -- Fix "Number of files" output for attic info. (#124) -- limit create file permissions so files aren't read while restoring -- Fix issue with empty xattr values (#106) - -Version 0.13 -~~~~~~~~~~~~ - -(feature release, released on Jun 29, 2014) - -- Fix sporadic "Resource temporarily unavailable" when using remote repositories -- Reduce file cache memory usage (#90) -- Faster AES encryption (utilizing AES-NI when available) -- Experimental Linux, OS X and FreeBSD ACL support (#66) -- Added support for backup and restore of BSDFlags (OSX, FreeBSD) (#56) -- Fix bug where xattrs on symlinks were not correctly restored -- Added cachedir support. CACHEDIR.TAG compatible cache directories - can now be excluded using ``--exclude-caches`` (#74) -- Fix crash on extreme mtime timestamps (year 2400+) (#81) -- Fix Python 3.2 specific lockf issue (EDEADLK) - -Version 0.12 -~~~~~~~~~~~~ - -(feature release, released on April 7, 2014) - -- Python 3.4 support (#62) -- Various documentation improvements a new style -- ``attic mount`` now supports mounting an entire repository not only - individual archives (#59) -- Added option to restrict remote repository access to specific path(s): - ``attic serve --restrict-to-path X`` (#51) -- Include "all archives" size information in "--stats" output. (#54) -- Added ``--stats`` option to ``attic delete`` and ``attic prune`` -- Fixed bug where ``attic prune`` used UTC instead of the local time zone - when determining which archives to keep. -- Switch to SI units (Power of 1000 instead 1024) when printing file sizes - -Version 0.11 -~~~~~~~~~~~~ - -(feature release, released on March 7, 2014) - -- New "check" command for repository consistency checking (#24) -- Documentation improvements -- Fix exception during "attic create" with repeated files (#39) -- New "--exclude-from" option for attic create/extract/verify. -- Improved archive metadata deduplication. -- "attic verify" has been deprecated. Use "attic extract --dry-run" instead. -- "attic prune --hourly|daily|..." has been deprecated. - Use "attic prune --keep-hourly|daily|..." instead. -- Ignore xattr errors during "extract" if not supported by the filesystem. (#46) - -Version 0.10 -~~~~~~~~~~~~ - -(bugfix release, released on Jan 30, 2014) - -- Fix deadlock when extracting 0 sized files from remote repositories -- "--exclude" wildcard patterns are now properly applied to the full path - not just the file name part (#5). -- Make source code endianness agnostic (#1) - -Version 0.9 -~~~~~~~~~~~ - -(feature release, released on Jan 23, 2014) - -- Remote repository speed and reliability improvements. -- Fix sorting of segment names to ignore NFS left over files. (#17) -- Fix incorrect display of time (#13) -- Improved error handling / reporting. (#12) -- Use fcntl() instead of flock() when locking repository/cache. (#15) -- Let ssh figure out port/user if not specified so we don't override .ssh/config (#9) -- Improved libcrypto path detection (#23). - -Version 0.8.1 -~~~~~~~~~~~~~ - -(bugfix release, released on Oct 4, 2013) - -- Fix segmentation fault issue. - -Version 0.8 -~~~~~~~~~~~ - -(feature release, released on Oct 3, 2013) - -- Fix xattr issue when backing up sshfs filesystems (#4) -- Fix issue with excessive index file size (#6) -- Support access of read only repositories. -- New syntax to enable repository encryption: - attic init --encryption="none|passphrase|keyfile". -- Detect and abort if repository is older than the cache. - - -Version 0.7 -~~~~~~~~~~~ - -(feature release, released on Aug 5, 2013) - -- Ported to FreeBSD -- Improved documentation -- Experimental: Archives mountable as FUSE filesystems. -- The "user." prefix is no longer stripped from xattrs on Linux - - -Version 0.6.1 -~~~~~~~~~~~~~ - -(bugfix release, released on July 19, 2013) - -- Fixed an issue where mtime was not always correctly restored. - - -Version 0.6 -~~~~~~~~~~~ - -First public release on July 9, 2013 diff --git a/docs/changes_0.x.rst b/docs/changes_0.x.rst new file mode 100644 index 000000000..52c0b0352 --- /dev/null +++ b/docs/changes_0.x.rst @@ -0,0 +1,807 @@ +.. _changelog_0x: + +Change Log 0.x +============== + +Version 0.30.0 (2016-01-23) +--------------------------- + +Compatibility notes: + +- you may need to use -v (or --info) more often to actually see output emitted + at INFO log level (because it is suppressed at the default WARNING log level). + See the "general" section in the usage docs. +- for borg create, you need --list (additionally to -v) to see the long file + list (was needed so you can have e.g. --stats alone without the long list) +- see below about BORG_DELETE_I_KNOW_WHAT_I_AM_DOING (was: + BORG_CHECK_I_KNOW_WHAT_I_AM_DOING) + +Bug fixes: + +- fix crash when using borg create --dry-run --keep-tag-files, #570 +- make sure teardown with cleanup happens for Cache and RepositoryCache, + avoiding leftover locks and TEMP dir contents, #285 (partially), #548 +- fix locking KeyError, partial fix for #502 +- log stats consistently, #526 +- add abbreviated weekday to timestamp format, fixes #496 +- strip whitespace when loading exclusions from file +- unset LD_LIBRARY_PATH before invoking ssh, fixes strange OpenSSL library + version warning when using the borg binary, #514 +- add some error handling/fallback for C library loading, #494 +- added BORG_DELETE_I_KNOW_WHAT_I_AM_DOING for check in "borg delete", #503 +- remove unused "repair" rpc method name + +New features: + +- borg create: implement exclusions using regular expression patterns. +- borg create: implement inclusions using patterns. +- borg extract: support patterns, #361 +- support different styles for patterns: + + - fnmatch (`fm:` prefix, default when omitted), like borg <= 0.29. + - shell (`sh:` prefix) with `*` not matching directory separators and + `**/` matching 0..n directories + - path prefix (`pp:` prefix, for unifying borg create pp1 pp2 into the + patterns system), semantics like in borg <= 0.29 + - regular expression (`re:`), new! +- --progress option for borg upgrade (#291) and borg delete +- update progress indication more often (e.g. for borg create within big + files or for borg check repo), #500 +- finer chunker granularity for items metadata stream, #547, #487 +- borg create --list now used (additionally to -v) to enable the verbose + file list output +- display borg version below tracebacks, #532 + +Other changes: + +- hashtable size (and thus: RAM and disk consumption) follows a growth policy: + grows fast while small, grows slower when getting bigger, #527 +- Vagrantfile: use pyinstaller 3.1 to build binaries, freebsd sqlite3 fix, + fixes #569 +- no separate binaries for centos6 any more because the generic linux binaries + also work on centos6 (or in general: on systems with a slightly older glibc + than debian7 +- dev environment: require virtualenv<14.0 so we get a py32 compatible pip +- docs: + + - add space-saving chunks.archive.d trick to FAQ + - important: clarify -v and log levels in usage -> general, please read! + - sphinx configuration: create a simple man page from usage docs + - add a repo server setup example + - disable unneeded SSH features in authorized_keys examples for security. + - borg prune only knows "--keep-within" and not "--within" + - add gource video to resources docs, #507 + - add netbsd install instructions + - authors: make it more clear what refers to borg and what to attic + - document standalone binary requirements, #499 + - rephrase the mailing list section + - development docs: run build_api and build_usage before tagging release + - internals docs: hash table max. load factor is 0.75 now + - markup, typo, grammar, phrasing, clarifications and other fixes. + - add gcc gcc-c++ to redhat/fedora/corora install docs, fixes #583 + + +Version 0.29.0 (2015-12-13) +--------------------------- + +Compatibility notes: + +- when upgrading to 0.29.0 you need to upgrade client as well as server + installations due to the locking and commandline interface changes otherwise + you'll get an error msg about a RPC protocol mismatch or a wrong commandline + option. + if you run a server that needs to support both old and new clients, it is + suggested that you have a "borg-0.28.2" and a "borg-0.29.0" command. + clients then can choose via e.g. "borg --remote-path=borg-0.29.0 ...". +- the default waiting time for a lock changed from infinity to 1 second for a + better interactive user experience. if the repo you want to access is + currently locked, borg will now terminate after 1s with an error message. + if you have scripts that shall wait for the lock for a longer time, use + --lock-wait N (with N being the maximum wait time in seconds). + +Bug fixes: + +- hash table tuning (better chosen hashtable load factor 0.75 and prime initial + size of 1031 gave ~1000x speedup in some scenarios) +- avoid creation of an orphan lock for one case, #285 +- --keep-tag-files: fix file mode and multiple tag files in one directory, #432 +- fixes for "borg upgrade" (attic repo converter), #466 +- remove --progress isatty magic (and also --no-progress option) again, #476 +- borg init: display proper repo URL +- fix format of umask in help pages, #463 + +New features: + +- implement --lock-wait, support timeout for UpgradableLock, #210 +- implement borg break-lock command, #157 +- include system info below traceback, #324 +- sane remote logging, remote stderr, #461: + + - remote log output: intercept it and log it via local logging system, + with "Remote: " prefixed to message. log remote tracebacks. + - remote stderr: output it to local stderr with "Remote: " prefixed. +- add --debug and --info (same as --verbose) to set the log level of the + builtin logging configuration (which otherwise defaults to warning), #426 + note: there are few messages emitted at DEBUG level currently. +- optionally configure logging via env var BORG_LOGGING_CONF +- add --filter option for status characters: e.g. to show only the added + or modified files (and also errors), use "borg create -v --filter=AME ...". +- more progress indicators, #394 +- use ISO-8601 date and time format, #375 +- "borg check --prefix" to restrict archive checking to that name prefix, #206 + +Other changes: + +- hashindex_add C implementation (speed up cache re-sync for new archives) +- increase FUSE read_size to 1024 (speed up metadata operations) +- check/delete/prune --save-space: free unused segments quickly, #239 +- increase rpc protocol version to 2 (see also Compatibility notes), #458 +- silence borg by default (via default log level WARNING) +- get rid of C compiler warnings, #391 +- upgrade OS X FUSE to 3.0.9 on the OS X binary build system +- use python 3.5.1 to build binaries +- docs: + + - new mailing list borgbackup@python.org, #468 + - readthedocs: color and logo improvements + - load coverage icons over SSL (avoids mixed content) + - more precise binary installation steps + - update release procedure docs about OS X FUSE + - FAQ entry about unexpected 'A' status for unchanged file(s), #403 + - add docs about 'E' file status + - add "borg upgrade" docs, #464 + - add developer docs about output and logging + - clarify encryption, add note about client-side encryption + - add resources section, with videos, talks, presentations, #149 + - Borg moved to Arch Linux [community] + - fix wrong installation instructions for archlinux + + +Version 0.28.2 (2015-11-15) +--------------------------- + +New features: + +- borg create --exclude-if-present TAGFILE - exclude directories that have the + given file from the backup. You can additionally give --keep-tag-files to + preserve just the directory roots and the tag-files (but not backup other + directory contents), #395, attic #128, attic #142 + +Other changes: + +- do not create docs sources at build time (just have them in the repo), + completely remove have_cython() hack, do not use the "mock" library at build + time, #384 +- avoid hidden import, make it easier for PyInstaller, easier fix for #218 +- docs: + + - add description of item flags / status output, fixes #402 + - explain how to regenerate usage and API files (build_api or + build_usage) and when to commit usage files directly into git, #384 + - minor install docs improvements + + +Version 0.28.1 (2015-11-08) +--------------------------- + +Bug fixes: + +- do not try to build api / usage docs for production install, + fixes unexpected "mock" build dependency, #384 + +Other changes: + +- avoid using msgpack.packb at import time +- fix formatting issue in changes.rst +- fix build on readthedocs + + +Version 0.28.0 (2015-11-08) +--------------------------- + +Compatibility notes: + +- changed return codes (exit codes), see docs. in short: + old: 0 = ok, 1 = error. now: 0 = ok, 1 = warning, 2 = error + +New features: + +- refactor return codes (exit codes), fixes #61 +- add --show-rc option enable "terminating with X status, rc N" output, fixes 58, #351 +- borg create backups atime and ctime additionally to mtime, fixes #317 + - extract: support atime additionally to mtime + - FUSE: support ctime and atime additionally to mtime +- support borg --version +- emit a warning if we have a slow msgpack installed +- borg list --prefix=thishostname- REPO, fixes #205 +- Debug commands (do not use except if you know what you do: debug-get-obj, + debug-put-obj, debug-delete-obj, debug-dump-archive-items. + +Bug fixes: + +- setup.py: fix bug related to BORG_LZ4_PREFIX processing +- fix "check" for repos that have incomplete chunks, fixes #364 +- borg mount: fix unlocking of repository at umount time, fixes #331 +- fix reading files without touching their atime, #334 +- non-ascii ACL fixes for Linux, FreeBSD and OS X, #277 +- fix acl_use_local_uid_gid() and add a test for it, attic #359 +- borg upgrade: do not upgrade repositories in place by default, #299 +- fix cascading failure with the index conversion code, #269 +- borg check: implement 'cmdline' archive metadata value decoding, #311 +- fix RobustUnpacker, it missed some metadata keys (new atime and ctime keys + were missing, but also bsdflags). add check for unknown metadata keys. +- create from stdin: also save atime, ctime (cosmetic) +- use default_notty=False for confirmations, fixes #345 +- vagrant: fix msgpack installation on centos, fixes #342 +- deal with unicode errors for symlinks in same way as for regular files and + have a helpful warning message about how to fix wrong locale setup, fixes #382 +- add ACL keys the RobustUnpacker must know about + +Other changes: + +- improve file size displays, more flexible size formatters +- explicitly commit to the units standard, #289 +- archiver: add E status (means that an error occurred when processing this + (single) item +- do binary releases via "github releases", closes #214 +- create: use -x and --one-file-system (was: --do-not-cross-mountpoints), #296 +- a lot of changes related to using "logging" module and screen output, #233 +- show progress display if on a tty, output more progress information, #303 +- factor out status output so it is consistent, fix surrogates removal, + maybe fixes #309 +- move away from RawConfigParser to ConfigParser +- archive checker: better error logging, give chunk_id and sequence numbers + (can be used together with borg debug-dump-archive-items). +- do not mention the deprecated passphrase mode +- emit a deprecation warning for --compression N (giving a just a number) +- misc .coverragerc fixes (and coverage measurement improvements), fixes #319 +- refactor confirmation code, reduce code duplication, add tests +- prettier error messages, fixes #307, #57 +- tests: + + - add a test to find disk-full issues, #327 + - travis: also run tests on Python 3.5 + - travis: use tox -r so it rebuilds the tox environments + - test the generated pyinstaller-based binary by archiver unit tests, #215 + - vagrant: tests: announce whether fakeroot is used or not + - vagrant: add vagrant user to fuse group for debianoid systems also + - vagrant: llfuse install on darwin needs pkgconfig installed + - vagrant: use pyinstaller from develop branch, fixes #336 + - benchmarks: test create, extract, list, delete, info, check, help, fixes #146 + - benchmarks: test with both the binary and the python code + - archiver tests: test with both the binary and the python code, fixes #215 + - make basic test more robust +- docs: + + - moved docs to borgbackup.readthedocs.org, #155 + - a lot of fixes and improvements, use mobile-friendly RTD standard theme + - use zlib,6 compression in some examples, fixes #275 + - add missing rename usage to docs, closes #279 + - include the help offered by borg help in the usage docs, fixes #293 + - include a list of major changes compared to attic into README, fixes #224 + - add OS X install instructions, #197 + - more details about the release process, #260 + - fix linux glibc requirement (binaries built on debian7 now) + - build: move usage and API generation to setup.py + - update docs about return codes, #61 + - remove api docs (too much breakage on rtd) + - borgbackup install + basics presentation (asciinema) + - describe the current style guide in documentation + - add section about debug commands + - warn about not running out of space + - add example for rename + - improve chunker params docs, fixes #362 + - minor development docs update + + +Version 0.27.0 (2015-10-07) +--------------------------- + +New features: + +- "borg upgrade" command - attic -> borg one time converter / migration, #21 +- temporary hack to avoid using lots of disk space for chunks.archive.d, #235: + To use it: rm -rf chunks.archive.d ; touch chunks.archive.d +- respect XDG_CACHE_HOME, attic #181 +- add support for arbitrary SSH commands, attic #99 +- borg delete --cache-only REPO (only delete cache, not REPO), attic #123 + + +Bug fixes: + +- use Debian 7 (wheezy) to build pyinstaller borgbackup binaries, fixes slow + down observed when running the Centos6-built binary on Ubuntu, #222 +- do not crash on empty lock.roster, fixes #232 +- fix multiple issues with the cache config version check, #234 +- fix segment entry header size check, attic #352 + plus other error handling improvements / code deduplication there. +- always give segment and offset in repo IntegrityErrors + + +Other changes: + +- stop producing binary wheels, remove docs about it, #147 +- docs: + - add warning about prune + - generate usage include files only as needed + - development docs: add Vagrant section + - update / improve / reformat FAQ + - hint to single-file pyinstaller binaries from README + + +Version 0.26.1 (2015-09-28) +--------------------------- + +This is a minor update, just docs and new pyinstaller binaries. + +- docs update about python and binary requirements +- better docs for --read-special, fix #220 +- re-built the binaries, fix #218 and #213 (glibc version issue) +- update web site about single-file pyinstaller binaries + +Note: if you did a python-based installation, there is no need to upgrade. + + +Version 0.26.0 (2015-09-19) +--------------------------- + +New features: + +- Faster cache sync (do all in one pass, remove tar/compression stuff), #163 +- BORG_REPO env var to specify the default repo, #168 +- read special files as if they were regular files, #79 +- implement borg create --dry-run, attic issue #267 +- Normalize paths before pattern matching on OS X, #143 +- support OpenBSD and NetBSD (except xattrs/ACLs) +- support / run tests on Python 3.5 + +Bug fixes: + +- borg mount repo: use absolute path, attic #200, attic #137 +- chunker: use off_t to get 64bit on 32bit platform, #178 +- initialize chunker fd to -1, so it's not equal to STDIN_FILENO (0) +- fix reaction to "no" answer at delete repo prompt, #182 +- setup.py: detect lz4.h header file location +- to support python < 3.2.4, add less buggy argparse lib from 3.2.6 (#194) +- fix for obtaining ``char *`` from temporary Python value (old code causes + a compile error on Mint 17.2) +- llfuse 0.41 install troubles on some platforms, require < 0.41 + (UnicodeDecodeError exception due to non-ascii llfuse setup.py) +- cython code: add some int types to get rid of unspecific python add / + subtract operations (avoid ``undefined symbol FPE_``... error on some platforms) +- fix verbose mode display of stdin backup +- extract: warn if a include pattern never matched, fixes #209, + implement counters for Include/ExcludePatterns +- archive names with slashes are invalid, attic issue #180 +- chunker: add a check whether the POSIX_FADV_DONTNEED constant is defined - + fixes building on OpenBSD. + +Other changes: + +- detect inconsistency / corruption / hash collision, #170 +- replace versioneer with setuptools_scm, #106 +- docs: + + - pkg-config is needed for llfuse installation + - be more clear about pruning, attic issue #132 +- unit tests: + + - xattr: ignore security.selinux attribute showing up + - ext3 seems to need a bit more space for a sparse file + - do not test lzma level 9 compression (avoid MemoryError) + - work around strange mtime granularity issue on netbsd, fixes #204 + - ignore st_rdev if file is not a block/char device, fixes #203 + - stay away from the setgid and sticky mode bits +- use Vagrant to do easy cross-platform testing (#196), currently: + + - Debian 7 "wheezy" 32bit, Debian 8 "jessie" 64bit + - Ubuntu 12.04 32bit, Ubuntu 14.04 64bit + - Centos 7 64bit + - FreeBSD 10.2 64bit + - OpenBSD 5.7 64bit + - NetBSD 6.1.5 64bit + - Darwin (OS X Yosemite) + + +Version 0.25.0 (2015-08-29) +--------------------------- + +Compatibility notes: + +- lz4 compression library (liblz4) is a new requirement (#156) +- the new compression code is very compatible: as long as you stay with zlib + compression, older borg releases will still be able to read data from a + repo/archive made with the new code (note: this is not the case for the + default "none" compression, use "zlib,0" if you want a "no compression" mode + that can be read by older borg). Also the new code is able to read repos and + archives made with older borg versions (for all zlib levels 0..9). + +Deprecations: + +- --compression N (with N being a number, as in 0.24) is deprecated. + We keep the --compression 0..9 for now to not break scripts, but it is + deprecated and will be removed later, so better fix your scripts now: + --compression 0 (as in 0.24) is the same as --compression zlib,0 (now). + BUT: if you do not want compression, you rather want --compression none + (which is the default). + --compression 1 (in 0.24) is the same as --compression zlib,1 (now) + --compression 9 (in 0.24) is the same as --compression zlib,9 (now) + +New features: + +- create --compression none (default, means: do not compress, just pass through + data "as is". this is more efficient than zlib level 0 as used in borg 0.24) +- create --compression lz4 (super-fast, but not very high compression) +- create --compression zlib,N (slower, higher compression, default for N is 6) +- create --compression lzma,N (slowest, highest compression, default N is 6) +- honor the nodump flag (UF_NODUMP) and do not backup such items +- list --short just outputs a simple list of the files/directories in an archive + +Bug fixes: + +- fixed --chunker-params parameter order confusion / malfunction, fixes #154 +- close fds of segments we delete (during compaction) +- close files which fell out the lrucache +- fadvise DONTNEED now is only called for the byte range actually read, not for + the whole file, fixes #158. +- fix issue with negative "all archives" size, fixes #165 +- restore_xattrs: ignore if setxattr fails with EACCES, fixes #162 + +Other changes: + +- remove fakeroot requirement for tests, tests run faster without fakeroot + (test setup does not fail any more without fakeroot, so you can run with or + without fakeroot), fixes #151 and #91. +- more tests for archiver +- recover_segment(): don't assume we have an fd for segment +- lrucache refactoring / cleanup, add dispose function, py.test tests +- generalize hashindex code for any key length (less hardcoding) +- lock roster: catch file not found in remove() method and ignore it +- travis CI: use requirements file +- improved docs: + + - replace hack for llfuse with proper solution (install libfuse-dev) + - update docs about compression + - update development docs about fakeroot + - internals: add some words about lock files / locking system + - support: mention BountySource and for what it can be used + - theme: use a lighter green + - add pypi, wheel, dist package based install docs + - split install docs into system-specific preparations and generic instructions + + +Version 0.24.0 (2015-08-09) +--------------------------- + +Incompatible changes (compared to 0.23): + +- borg now always issues --umask NNN option when invoking another borg via ssh + on the repository server. By that, it's making sure it uses the same umask + for remote repos as for local ones. Because of this, you must upgrade both + server and client(s) to 0.24. +- the default umask is 077 now (if you do not specify via --umask) which might + be a different one as you used previously. The default umask avoids that + you accidentally give access permissions for group and/or others to files + created by borg (e.g. the repository). + +Deprecations: + +- "--encryption passphrase" mode is deprecated, see #85 and #97. + See the new "--encryption repokey" mode for a replacement. + +New features: + +- borg create --chunker-params ... to configure the chunker, fixes #16 + (attic #302, attic #300, and somehow also #41). + This can be used to reduce memory usage caused by chunk management overhead, + so borg does not create a huge chunks index/repo index and eats all your RAM + if you back up lots of data in huge files (like VM disk images). + See docs/misc/create_chunker-params.txt for more information. +- borg info now reports chunk counts in the chunk index. +- borg create --compression 0..9 to select zlib compression level, fixes #66 + (attic #295). +- borg init --encryption repokey (to store the encryption key into the repo), + fixes #85 +- improve at-end error logging, always log exceptions and set exit_code=1 +- LoggedIO: better error checks / exceptions / exception handling +- implement --remote-path to allow non-default-path borg locations, #125 +- implement --umask M and use 077 as default umask for better security, #117 +- borg check: give a named single archive to it, fixes #139 +- cache sync: show progress indication +- cache sync: reimplement the chunk index merging in C + +Bug fixes: + +- fix segfault that happened for unreadable files (chunker: n needs to be a + signed size_t), #116 +- fix the repair mode, #144 +- repo delete: add destroy to allowed rpc methods, fixes issue #114 +- more compatible repository locking code (based on mkdir), maybe fixes #92 + (attic #317, attic #201). +- better Exception msg if no Borg is installed on the remote repo server, #56 +- create a RepositoryCache implementation that can cope with >2GiB, + fixes attic #326. +- fix Traceback when running check --repair, attic #232 +- clarify help text, fixes #73. +- add help string for --no-files-cache, fixes #140 + +Other changes: + +- improved docs: + + - added docs/misc directory for misc. writeups that won't be included + "as is" into the html docs. + - document environment variables and return codes (attic #324, attic #52) + - web site: add related projects, fix web site url, IRC #borgbackup + - Fedora/Fedora-based install instructions added to docs + - Cygwin-based install instructions added to docs + - updated AUTHORS + - add FAQ entries about redundancy / integrity + - clarify that borg extract uses the cwd as extraction target + - update internals doc about chunker params, memory usage and compression + - added docs about development + - add some words about resource usage in general + - document how to backup a raw disk + - add note about how to run borg from virtual env + - add solutions for (ll)fuse installation problems + - document what borg check does, fixes #138 + - reorganize borgbackup.github.io sidebar, prev/next at top + - deduplicate and refactor the docs / README.rst + +- use borg-tmp as prefix for temporary files / directories +- short prune options without "keep-" are deprecated, do not suggest them +- improved tox configuration +- remove usage of unittest.mock, always use mock from pypi +- use entrypoints instead of scripts, for better use of the wheel format and + modern installs +- add requirements.d/development.txt and modify tox.ini +- use travis-ci for testing based on Linux and (new) OS X +- use coverage.py, pytest-cov and codecov.io for test coverage support + +I forgot to list some stuff already implemented in 0.23.0, here they are: + +New features: + +- efficient archive list from manifest, meaning a big speedup for slow + repo connections and "list ", "delete ", "prune" (attic #242, + attic #167) +- big speedup for chunks cache sync (esp. for slow repo connections), fixes #18 +- hashindex: improve error messages + +Other changes: + +- explicitly specify binary mode to open binary files +- some easy micro optimizations + + +Version 0.23.0 (2015-06-11) +--------------------------- + +Incompatible changes (compared to attic, fork related): + +- changed sw name and cli command to "borg", updated docs +- package name (and name in urls) uses "borgbackup" to have fewer collisions +- changed repo / cache internal magic strings from ATTIC* to BORG*, + changed cache location to .cache/borg/ - this means that it currently won't + accept attic repos (see issue #21 about improving that) + +Bug fixes: + +- avoid defect python-msgpack releases, fixes attic #171, fixes attic #185 +- fix traceback when trying to do unsupported passphrase change, fixes attic #189 +- datetime does not like the year 10.000, fixes attic #139 +- fix "info" all archives stats, fixes attic #183 +- fix parsing with missing microseconds, fixes attic #282 +- fix misleading hint the fuse ImportError handler gave, fixes attic #237 +- check unpacked data from RPC for tuple type and correct length, fixes attic #127 +- fix Repository._active_txn state when lock upgrade fails +- give specific path to xattr.is_enabled(), disable symlink setattr call that + always fails +- fix test setup for 32bit platforms, partial fix for attic #196 +- upgraded versioneer, PEP440 compliance, fixes attic #257 + +New features: + +- less memory usage: add global option --no-cache-files +- check --last N (only check the last N archives) +- check: sort archives in reverse time order +- rename repo::oldname newname (rename repository) +- create -v output more informative +- create --progress (backup progress indicator) +- create --timestamp (utc string or reference file/dir) +- create: if "-" is given as path, read binary from stdin +- extract: if --stdout is given, write all extracted binary data to stdout +- extract --sparse (simple sparse file support) +- extra debug information for 'fread failed' +- delete (deletes whole repo + local cache) +- FUSE: reflect deduplication in allocated blocks +- only allow whitelisted RPC calls in server mode +- normalize source/exclude paths before matching +- use posix_fadvise to not spoil the OS cache, fixes attic #252 +- toplevel error handler: show tracebacks for better error analysis +- sigusr1 / sigint handler to print current file infos - attic PR #286 +- RPCError: include the exception args we get from remote + +Other changes: + +- source: misc. cleanups, pep8, style +- docs and faq improvements, fixes, updates +- cleanup crypto.pyx, make it easier to adapt to other AES modes +- do os.fsync like recommended in the python docs +- source: Let chunker optionally work with os-level file descriptor. +- source: Linux: remove duplicate os.fsencode calls +- source: refactor _open_rb code a bit, so it is more consistent / regular +- source: refactor indicator (status) and item processing +- source: use py.test for better testing, flake8 for code style checks +- source: fix tox >=2.0 compatibility (test runner) +- pypi package: add python version classifiers, add FreeBSD to platforms + + +Attic Changelog +--------------- + +Here you can see the full list of changes between each Attic release until Borg +forked from Attic: + +Version 0.17 +~~~~~~~~~~~~ + +(bugfix release, released on X) + +- Fix hashindex ARM memory alignment issue (#309) +- Improve hashindex error messages (#298) + +Version 0.16 +~~~~~~~~~~~~ + +(bugfix release, released on May 16, 2015) + +- Fix typo preventing the security confirmation prompt from working (#303) +- Improve handling of systems with improperly configured file system encoding (#289) +- Fix "All archives" output for attic info. (#183) +- More user friendly error message when repository key file is not found (#236) +- Fix parsing of iso 8601 timestamps with zero microseconds (#282) + +Version 0.15 +~~~~~~~~~~~~ + +(bugfix release, released on Apr 15, 2015) + +- xattr: Be less strict about unknown/unsupported platforms (#239) +- Reduce repository listing memory usage (#163). +- Fix BrokenPipeError for remote repositories (#233) +- Fix incorrect behavior with two character directory names (#265, #268) +- Require approval before accessing relocated/moved repository (#271) +- Require approval before accessing previously unknown unencrypted repositories (#271) +- Fix issue with hash index files larger than 2GB. +- Fix Python 3.2 compatibility issue with noatime open() (#164) +- Include missing pyx files in dist files (#168) + +Version 0.14 +~~~~~~~~~~~~ + +(feature release, released on Dec 17, 2014) + +- Added support for stripping leading path segments (#95) + "attic extract --strip-segments X" +- Add workaround for old Linux systems without acl_extended_file_no_follow (#96) +- Add MacPorts' path to the default openssl search path (#101) +- HashIndex improvements, eliminates unnecessary IO on low memory systems. +- Fix "Number of files" output for attic info. (#124) +- limit create file permissions so files aren't read while restoring +- Fix issue with empty xattr values (#106) + +Version 0.13 +~~~~~~~~~~~~ + +(feature release, released on Jun 29, 2014) + +- Fix sporadic "Resource temporarily unavailable" when using remote repositories +- Reduce file cache memory usage (#90) +- Faster AES encryption (utilizing AES-NI when available) +- Experimental Linux, OS X and FreeBSD ACL support (#66) +- Added support for backup and restore of BSDFlags (OSX, FreeBSD) (#56) +- Fix bug where xattrs on symlinks were not correctly restored +- Added cachedir support. CACHEDIR.TAG compatible cache directories + can now be excluded using ``--exclude-caches`` (#74) +- Fix crash on extreme mtime timestamps (year 2400+) (#81) +- Fix Python 3.2 specific lockf issue (EDEADLK) + +Version 0.12 +~~~~~~~~~~~~ + +(feature release, released on April 7, 2014) + +- Python 3.4 support (#62) +- Various documentation improvements a new style +- ``attic mount`` now supports mounting an entire repository not only + individual archives (#59) +- Added option to restrict remote repository access to specific path(s): + ``attic serve --restrict-to-path X`` (#51) +- Include "all archives" size information in "--stats" output. (#54) +- Added ``--stats`` option to ``attic delete`` and ``attic prune`` +- Fixed bug where ``attic prune`` used UTC instead of the local time zone + when determining which archives to keep. +- Switch to SI units (Power of 1000 instead 1024) when printing file sizes + +Version 0.11 +~~~~~~~~~~~~ + +(feature release, released on March 7, 2014) + +- New "check" command for repository consistency checking (#24) +- Documentation improvements +- Fix exception during "attic create" with repeated files (#39) +- New "--exclude-from" option for attic create/extract/verify. +- Improved archive metadata deduplication. +- "attic verify" has been deprecated. Use "attic extract --dry-run" instead. +- "attic prune --hourly|daily|..." has been deprecated. + Use "attic prune --keep-hourly|daily|..." instead. +- Ignore xattr errors during "extract" if not supported by the filesystem. (#46) + +Version 0.10 +~~~~~~~~~~~~ + +(bugfix release, released on Jan 30, 2014) + +- Fix deadlock when extracting 0 sized files from remote repositories +- "--exclude" wildcard patterns are now properly applied to the full path + not just the file name part (#5). +- Make source code endianness agnostic (#1) + +Version 0.9 +~~~~~~~~~~~ + +(feature release, released on Jan 23, 2014) + +- Remote repository speed and reliability improvements. +- Fix sorting of segment names to ignore NFS left over files. (#17) +- Fix incorrect display of time (#13) +- Improved error handling / reporting. (#12) +- Use fcntl() instead of flock() when locking repository/cache. (#15) +- Let ssh figure out port/user if not specified so we don't override .ssh/config (#9) +- Improved libcrypto path detection (#23). + +Version 0.8.1 +~~~~~~~~~~~~~ + +(bugfix release, released on Oct 4, 2013) + +- Fix segmentation fault issue. + +Version 0.8 +~~~~~~~~~~~ + +(feature release, released on Oct 3, 2013) + +- Fix xattr issue when backing up sshfs filesystems (#4) +- Fix issue with excessive index file size (#6) +- Support access of read only repositories. +- New syntax to enable repository encryption: + attic init --encryption="none|passphrase|keyfile". +- Detect and abort if repository is older than the cache. + + +Version 0.7 +~~~~~~~~~~~ + +(feature release, released on Aug 5, 2013) + +- Ported to FreeBSD +- Improved documentation +- Experimental: Archives mountable as FUSE filesystems. +- The "user." prefix is no longer stripped from xattrs on Linux + + +Version 0.6.1 +~~~~~~~~~~~~~ + +(bugfix release, released on July 19, 2013) + +- Fixed an issue where mtime was not always correctly restored. + + +Version 0.6 +~~~~~~~~~~~ + +First public release on July 9, 2013 diff --git a/docs/changes_1.x.rst b/docs/changes_1.x.rst new file mode 100644 index 000000000..16aad68d7 --- /dev/null +++ b/docs/changes_1.x.rst @@ -0,0 +1,4362 @@ +.. _important_notes_1x: + +Important notes 1.x +=================== + +This section provides information about security and corruption issues. + +.. _hashindex_set_bug: + +Pre-1.1.11 potential index corruption / data loss issue +------------------------------------------------------- + +A bug was discovered in our hashtable code, see issue #4829. +The code is used for the client-side chunks cache and the server-side repo index. + +Although borg uses the hashtables very heavily, the index corruption did not +happen too frequently, because it needed specific conditions to happen. + +Data loss required even more specific conditions, so it should be rare (and +also detectable via borg check). + +You might be affected if borg crashed with / complained about: + +- AssertionError: Corrupted segment reference count - corrupted index or hints +- ObjectNotFound: Object with key ... not found in repository ... +- Index mismatch for key b'...'. (..., ...) != (-1, -1) +- ValueError: stats_against: key contained in self but not in master_index. + +Advised procedure to fix any related issue in your indexes/caches: + +- install fixed borg code (on client AND server) +- for all of your clients and repos remove the cache by: + + borg delete --cache-only YOURREPO + + (later, the cache will be re-built automatically) +- for all your repos, rebuild the repo index by: + + borg check --repair YOURREPO + + This will also check all archives and detect if there is any data-loss issue. + +Affected branches / releases: + +- fd06497 introduced the bug into 1.1-maint branch - it affects all borg 1.1.x since 1.1.0b4. +- fd06497 introduced the bug into master branch - it affects all borg 1.2.0 alpha releases. +- c5cd882 introduced the bug into 1.0-maint branch - it affects all borg 1.0.x since 1.0.11rc1. + +The bug was fixed by: + +- 701159a fixes the bug in 1.1-maint branch - will be released with borg 1.1.11. +- fa63150 fixes the bug in master branch - will be released with borg 1.2.0a8. +- 7bb90b6 fixes the bug in 1.0-maint branch. Branch is EOL, no new release is planned as of now. + +.. _broken_validator: + +Pre-1.1.4 potential data corruption issue +----------------------------------------- + +A data corruption bug was discovered in borg check --repair, see issue #3444. + +This is a 1.1.x regression, releases < 1.1 (e.g. 1.0.x) are not affected. + +To avoid data loss, you must not run borg check --repair using an unfixed version +of borg 1.1.x. The first official release that has the fix is 1.1.4. + +Package maintainers may have applied the fix to updated packages of 1.1.x (x<4) +though, see the package maintainer's package changelog to make sure. + +If you never had missing item metadata chunks, the bug has not affected you +even if you did run borg check --repair with an unfixed version. + +When borg check --repair tried to repair corrupt archives that miss item metadata +chunks, the resync to valid metadata in still present item metadata chunks +malfunctioned. This was due to a broken validator that considered all (even valid) +item metadata as invalid. As they were considered invalid, borg discarded them. +Practically, that means the affected files, directories or other fs objects were +discarded from the archive. + +Due to the malfunction, the process was extremely slow, but if you let it +complete, borg would have created a "repaired" archive that has lost a lot of items. +If you interrupted borg check --repair because it was so strangely slow (killing +borg somehow, e.g. Ctrl-C) the transaction was rolled back and no corruption occurred. + +The log message indicating the precondition for the bug triggering looks like: + + item metadata chunk missing [chunk: 001056_bdee87d...a3e50d] + +If you never had that in your borg check --repair runs, you're not affected. + +But if you're unsure or you actually have seen that, better check your archives. +By just using "borg list repo::archive" you can see if all expected filesystem +items are listed. + +.. _tam_vuln: + +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 + +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 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-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: + +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 +----------------------------------- + +Some external errors (like network or disk I/O errors) could lead to +corruption of the backup repository due to issue #1138. + +A sign that this happened is if "E" status was reported for a file that can +not be explained by problems with the source file. If you still have logs from +"borg create -v --list", you can check for "E" status. + +Here is what could cause corruption and what you can do now: + +1) I/O errors (e.g. repo disk errors) while writing data to repo. + +This could lead to corrupted segment files. + +Fix:: + + # check for corrupt chunks / segments: + borg check -v --repository-only REPO + + # repair the repo: + borg check -v --repository-only --repair REPO + + # make sure everything is fixed: + borg check -v --repository-only REPO + +2) Unreliable network / unreliable connection to the repo. + +This could lead to archive metadata corruption. + +Fix:: + + # check for corrupt archives: + borg check -v --archives-only REPO + + # delete the corrupt archives: + borg delete --force REPO::CORRUPT_ARCHIVE + + # make sure everything is fixed: + borg check -v --archives-only REPO + +3) In case you want to do more intensive checking. + +The best check that everything is ok is to run a dry-run extraction:: + + borg extract -v --dry-run REPO::ARCHIVE + +.. _changelog_1x: + +Change Log 1.x +============== + +Version 1.3.0a1 (2022-04-15) +---------------------------- + +Please note: + +This is an alpha release, only for testing - do not use this with production repos. + +New features: + +- init: new --encryption=(repokey|keyfile)-[blake2-](aes-ocb|chacha20-poly1305) + + - New, better, faster crypto (see encryption-aead diagram in the docs), #6463. + - New AEAD cipher suites: AES-OCB and CHACHA20-POLY1305. + - Session keys are derived via HKDF from random session id and master key. + - Nonces/MessageIVs are counters starting from 0 for each session. + - AAD: chunk id, key type, messageIV, sessionID are now authenticated also. + - Solves the potential AES-CTR mode counter management issues of the legacy crypto. +- init: --key-algorithm=argon2 (new default KDF, older pbkdf2 also still available) + + borg key change-passphrase / change-location keeps the key algorithm unchanged. +- key change-algorithm: to upgrade existing keys to argon2 or downgrade to pbkdf2. + + We recommend you to upgrade unless you have to keep the key compatible with older versions of borg. +- key change-location: usable for repokey <-> keyfile location change +- benchmark cpu: display benchmarks of cpu bound stuff +- export-tar: new --tar-format=PAX (default: GNU) +- import-tar/export-tar: can use PAX format for ctime and atime support +- import-tar/export-tar: --tar-format=BORG: roundtrip ALL item metadata, #5830 +- repository: create and use version 2 repos only for now +- repository: implement PUT2: header crc32, overall xxh64, #1704 + +Other changes: + +- require python >= 3.9, #6315 +- simplify libs setup, #6482 +- unbundle most bundled 3rd party code, use libs, #6316 +- use libdeflate.crc32 (Linux and all others) or zlib.crc32 (macOS) +- repository: code cleanups / simplifications +- internal crypto api: speedups / cleanups / refactorings / modernisation +- remove "borg upgrade" support for "attic backup" repos +- remove PassphraseKey code and borg key migrate-to-repokey command +- OpenBSD: build borg with OpenSSL (not: LibreSSL), #6474 +- remove support for LibreSSL, #6474 +- remove support for OpenSSL < 1.1.1 + + +Version 1.2.0 (2022-02-22 22:02:22 :-) +-------------------------------------- + +Please note: + +This is the first borg 1.2 release, so be careful and read the notes below. + +Upgrade notes: + +Strictly taken, nothing special is required for upgrading to 1.2, but some +things can be recommended: + +- do you already want to upgrade? 1.1.x also will get fixes for a while. +- be careful, first upgrade your less critical / smaller repos. +- first upgrade to a recent 1.1.x release - especially if you run some older + 1.1.* or even 1.0.* borg release. +- using that, run at least one `borg create` (your normal backup), `prune` + and especially a `check` to see everything is in a good state. +- check the output of `borg check` - if there is anything special, consider + a `borg check --repair` followed by another `borg check`. +- if everything is fine so far (borg check reports no issues), you can consider + upgrading to 1.2.0. if not, please first fix any already existing issue. +- if you want to play safer, first **create a backup of your borg repository**. +- upgrade to latest borg 1.2.x release (you could use the fat binary from + github releases page) +- run `borg compact --cleanup-commits` to clean up a ton of 17 bytes long files + in your repo caused by a borg 1.1 bug +- run `borg check` again (now with borg 1.2.x) and check if there is anything + special. +- run `borg info` (with borg 1.2.x) to build the local pre12-meta cache (can + take significant time, but after that it will be fast) - for more details + see below. +- check the compatibility notes (see below) and adapt your scripts, if needed. +- if you run into any issues, please check the github issue tracker before + posting new issues there or elsewhere. + +If you follow this procedure, you can help avoiding that we get a lot of +"borg 1.2" issue reports that are not really 1.2 issues, but existed before +and maybe just were not noticed. + +Compatibility notes: + +- matching of path patterns has been aligned with borg storing relative paths. + Borg archives file paths without leading slashes. Previously, include/exclude + patterns could contain leading slashes. You should check your patterns and + remove leading slashes. +- dropped support / testing for older Pythons, minimum requirement is 3.8. + In case your OS does not provide Python >= 3.8, consider using our binary, + which does not need an external Python interpreter. Or continue using + borg 1.1.x, which is still supported. +- freeing repository space only happens when "borg compact" is invoked. +- mount: the default for --numeric-ids is False now (same as borg extract) +- borg create --noatime is deprecated. Not storing atime is the default behaviour + now (use --atime if you want to store the atime). +- list: corrected mix-up of "isomtime" and "mtime" formats. + Previously, "isomtime" was the default but produced a verbose human format, + while "mtime" produced a ISO-8601-like format. + The behaviours have been swapped (so "mtime" is human, "isomtime" is ISO-like), + and the default is now "mtime". + "isomtime" is now a real ISO-8601 format ("T" between date and time, not a space). +- create/recreate --list: file status for all files used to get announced *AFTER* + the file (with borg < 1.2). Now, file status is announced *BEFORE* the file + contents are processed. If the file status changes later (e.g. due to an error + or a content change), the updated/final file status will be printed again. +- removed deprecated-since-long stuff (deprecated since): + + - command "borg change-passphrase" (2017-02), use "borg key ..." + - option "--keep-tag-files" (2017-01), use "--keep-exclude-tags" + - option "--list-format" (2017-10), use "--format" + - option "--ignore-inode" (2017-09), use "--files-cache" w/o "inode" + - option "--no-files-cache" (2017-09), use "--files-cache=disabled" +- removed BORG_HOSTNAME_IS_UNIQUE env var. + to use borg you must implement one of these 2 scenarios: + + - 1) the combination of FQDN and result of uuid.getnode() must be unique + and stable (this should be the case for almost everybody, except when + having duplicate FQDN *and* MAC address or all-zero MAC address) + - 2) if you are aware that 1) is not the case for you, you must set + BORG_HOST_ID env var to something unique. +- exit with 128 + signal number, #5161. + if you have scripts expecting rc == 2 for a signal exit, you need to update + them to check for >= 128. + +Fixes: + +- diff: reduce memory consumption, fix is_hardlink_master, #6295 +- compact: fix / improve freeable / freed space log output + + - derive really freed space from quota use before/after, #5679 + - do not say "freeable", but "maybe freeable" (based on hint, unsure) +- fix race conditions in internal SaveFile function, #6306 #6028 +- implement internal safe_unlink (was: truncate_and_unlink) function more safely: + usually it does not truncate any more, only under "disk full" circumstances + and only if there is only one hardlink. + see: https://github.com/borgbackup/borg/discussions/6286 + +Other changes: + +- info: use a pre12-meta cache to accelerate stats for borg < 1.2 archives. + the first time borg info is invoked on a borg 1.1 repo, it can take a + rather long time computing and caching some stats values for 1.1 archives, + which borg 1.2 archives have in their archive metadata structure. + be patient, esp. if you have lots of old archives. + following invocations are much faster due to the cache. + related change: add archive name to calc_stats progress display. +- docs: + + - add borg 1.2 upgrade notes, #6217 + - link to borg placeholders and borg patterns help + - init: explain the encryption modes better + - clarify usage of patternfile roots + - put import-tar docs into same file as export-tar docs + - explain the difference between a path that ends with or without a slash, + #6297 + + +Version 1.2.0rc1 (2022-02-05) +----------------------------- + +Fixes: + +- repo::archive location placeholder expansion fixes, #5826, #5998 +- repository: fix intermediate commits, shall be at end of current segment +- delete: don't commit if nothing was deleted, avoid cache sync, #6060 +- argument parsing: accept some options only once, #6026 +- disallow overwriting of existing keyfiles on init, #6036 +- if ensure_dir() fails, give more informative error message, #5952 + +New features: + +- delete --force: do not ask when deleting a repo, #5941 + +Other changes: + +- requirements: exclude broken or incompatible-with-pyinstaller setuptools +- add a requirements.d/development.lock.txt and use it for vagrant +- tests: + + - added nonce-related tests + - refactor: remove assert_true + - vagrant: macos box tuning, netbsd box fixes, #5370, #5922 +- docs: + + - update install docs / requirements docs, #6180 + - borg mount / FUSE "versions" view is not experimental any more + - --pattern* is not experimental any more, #6134 + - impact of deleting path/to/repo/nonce, #5858 + - key export: add examples, #6204 + - ~/.config/borg/keys is not used for repokey keys, #6107 + - excluded parent dir's metadata can't restore + + +Version 1.2.0b4 (2022-01-23) +---------------------------- + +Fixes: + +- create: fix passing device nodes and symlinks to --paths-from-stdin, #6009 +- create --dry-run: fix display of kept tagfile, #5834 +- check --repair: fix missing parameter in "did not consistently fail" msg, #5822 +- fix hardlinkable file type check, #6037 +- list: remove placeholders for shake_* hashes, #6082 +- prune: handle case of calling prune_split when there are no archives, #6015 +- benchmark crud: make sure cleanup of borg-test-data files/dir happens, #5630 +- do not show archive name in repository-related error msgs, #6014 +- prettier error msg (no stacktrace) if exclude file is missing, #5734 +- do not require BORG_CONFIG_DIR if BORG_{SECURITY,KEYS}_DIR are set, #5979 +- fix pyinstaller detection for dir-mode, #5897 +- atomically create the CACHE_TAG file, #6028 +- deal with the SaveFile/SyncFile race, docs, see #6056 708a5853 +- avoid expanding path into LHS of formatting operation + tests, #6064 #6063 +- repository: quota / compactable computation fixes +- info: emit repo info even if repo has 0 archives + test, #6120 + +New features: + +- check --repair: significantly speed up search for next valid object in segment, #6022 +- check: add progress indicator for archive check, #5809 +- create: add retry_erofs workaround for O_NOATIME issue on volume shadow copies in WSL1, #6024 +- create: allow --files-cache=size (this is potentially dangerous, use on your own risk), #5686 +- import-tar: implement import-tar to complement export-tar, #2233 +- implement BORG_SELFTEST env variable (can be carefully used to speedup borg hosting), #5871 +- key export: print key if path is '-' or not given, #6092 +- list --format: Add command_line to format keys + +Other changes: + +- pypi metadata: alpha -> beta +- require python 3.8+, #5975 +- use pyinstaller 4.7 +- allow msgpack 1.0.3 +- upgrade to bundled xxhash to 0.8.1 +- import-tar / export-tar: tar file related changes: + + - check for short tarfile extensions + - add .lz4 and .zstd + - fix docs about extensions and decompression commands +- add github codeql analysis, #6148 +- vagrant: + + - box updates / add new boxes / remove outdated and broken boxes + - use Python 3.9.10 (incl. binary builds) and 3.10.0 + - fix pyenv initialisation, #5798 + - fix vagrant scp on macOS, #5921 + - use macfuse instead of osxfuse +- shell completions: + + - update shell completions to 1.1.17, #5923 + - remove BORG_LIBC completion, since 9914968 borg no longer uses find_library(). +- docs: + + - fixed readme.rst irc webchat link (we use libera chat now, not freenode) + - fix exceptions thrown by `setup.py build_man` + - check --repair: recommend checking hw before check --repair, #5855 + - check --verify-data: clarify and document conflict with --repository-only, #5808 + - serve: improve ssh forced commands docs, #6083 + - list: improve docs for `borg list` --format, #6061 + - list: remove --list-format from borg list + - FAQ: fix manifest-timestamp path (inside security dir) + - fix the broken link to .nix file + - document behavior for filesystems with inconsistent inodes, #5770 + - clarify user_id vs uid for fuse, #5723 + - clarify pattern usage with commands, #5176 + - clarify pp vs. pf pattern type, #5300 + - update referenced freebsd/macOS versions used for binary build, #5942 + - pull mode: add some warnings, #5827 + - clarify "you will need key and passphrase" borg init warning, #4622 + - add missing leading slashes in help patterns, #5857 + - add info on renaming repositories, #5240 + - check: add notice about defective hardware, #5753 + - mention tar --compare (compare archive to fs files), #5880 + - add note about grandfather-father-son backup retention policy / rotation scheme, #6006 + - permissions note rewritten to make it less confusing + - create github security policy + - remove leftovers of BORG_HOSTNAME_IS_UNIQUE + - excluded parent dir's metadata can't restore. (#6062) + - if parent dir is not extracted, we do not have its metadata + - clarify who starts the remote agent + + +Version 1.2.0b3 (2021-05-12) +---------------------------- + +Fixes: + +- create: fix --progress --log-json, #4360#issuecomment-774580052 +- do not load files cache for commands not using it, #5673 +- fix repeated cache tag file writing bug + +New features: + +- create/recreate: print preliminary file status early, #5417 +- create/extract: add --noxattrs and --noacls options, #3955 +- create: verbose files cache logging via --debug-topic=files_cache, #5659 +- mount: implement --numeric-ids (default: False!), #2377 +- diff: add --json-lines option +- info / create --stats: add --iec option to print sizes in powers of 1024. + +Other changes: + +- create: add --upload-(ratelimit|buffer), deprecate --remote-* options, #5611 +- create/extract/mount: add --numeric-ids, deprecate --numeric-owner option, #5724 +- config: accept non-int value for max_segment_size / storage_quota +- use PyInstaller v4.3, #5671 +- vagrant: use Python 3.9.5 to build binaries +- tox.ini: modernize and enable execution without preinstalling deps +- cleanup code style checks +- get rid of distutils, use setuptools+packaging +- github CI: test on Python 3.10-dev +- check: missing / healed chunks: always tell chunk ID, #5704 +- docs: + + - remove bad /var/cache exclusion in example commands, #5625 + - misc. fixes and improvements, esp. for macOS + - add unsafe workaround to use an old repo copy, #5722 + + +Version 1.2.0b2 (2021-02-06) +---------------------------- + +Fixes: + +- create: do not recurse into duplicate roots, #5603 +- create: only print stats if not ctrl-c'ed, fixes traceback, #5668 +- extract: + improve exception handling when setting xattrs, #5092. + emit a warning message giving the path, xattr key and error message. + continue trying to restore other xattrs and bsdflags of the same file + after an exception with xattr-setting happened. +- export-tar: + fix memory leak with ssh: remote repository, #5568. + fix potential memory leak with ssh: remote repository with partial extraction. +- remove empty shadowed_segments lists, #5275 +- fix bad default: manifest.archives.list(consider_checkpoints=False), + fixes tracebacks / KeyErros for missing objects in ChunkIndex, #5668 + +New features: + +- create: improve sparse file support + + - create --sparse (detect sparse file holes) and file map support, + only for the "fixed" chunker, #14 + - detect all-zero chunks in read data in "buzhash" and "fixed" chunkers + - cached_hash: use a small LRU cache to accelerate all-zero chunks hashing + - use cached_hash also to generate all-zero replacement chunks +- create --remote-buffer, add a upload buffer for remote repos, #5574 +- prune: keep oldest archive when retention target not met + +Other changes: + +- use blake2 from python 3.6+ hashlib + (this removes the requirement for libb2 and the bundled blake2 code) +- also accept msgpack up to 1.0.2. + exclude 1.0.1 though, which had some issues (not sure they affect borg). +- create: add repository location to --stats output, #5491 +- check: debug log the segment filename +- delete: add a --list switch to borg delete, #5116 +- borg debug dump-hints - implemented to e.g. to look at shadow_index +- Tab completion support for additional archives for 'borg delete' +- refactor: have one borg.constants.zero all-zero bytes object +- refactor shadow_index updating repo.put/delete, #5661, #5636. +- docs: + + - add another case of attempted hardlink usage + - fix description of borg upgrade hardlink usage, #5518 + - use HTTPS everywhere + - add examples for --paths-from-stdin, --paths-from-command, --paths-separator, #5644 + - fix typos/grammar + - update docs for dev environment installation instructions + - recommend running tests only on installed versions for setup + - add badge with current status of package +- vagrant: + + - use brew install --cask ..., #5557 + - use Python 3.9.1 and PyInstaller 4.1 to build the borg binary + + +Version 1.2.0b1 (2020-12-06) +---------------------------- + +Fixes: + +- BORG_CACHE_DIR crashing borg if empty, atomic handling of + recursive directory creation, #5216 +- fix --dry-run and --stats coexistence, #5415 +- allow EIO with warning when trying to hardlink, #4336 +- export-tar: set tar format to GNU_FORMAT explicitly, #5274 +- use --timestamp for {utcnow} and {now} if given, #5189 +- make timestamp helper timezone-aware + +New features: + +- create: implement --paths-from-stdin and --paths-from-command, see #5492. + These switches read paths to archive from stdin. Delimiter can specified + by --paths-delimiter=DELIM. Paths read will be added honoring every + option but exclusion options and --one-file-system. borg won't recurse + into directories. +- 'obfuscate' pseudo compressor obfuscates compressed chunk size in repo +- add pyfuse3 (successor of llfuse) as an alternative lowlevel fuse + implementation to llfuse (deprecated), #5407. + FUSE implementation can be switched via env var BORG_FUSE_IMPL. +- allow appending to the files cache filename with BORG_FILES_CACHE_SUFFIX +- create: implement --stdin-mode, --stdin-user and --stdin-group, #5333 + +Other changes: + +- split recursive directory walking/processing into directory walking and + item processing. +- fix warning by importing setuptools before distutils. +- debug info: include infos about FUSE implementation, #5546 +- testing: + + - add a test for the hashindex corruption bug, #5531 #4829 + - move away from travis-ci, use github actions, #5528 #5467 + - test both on fuse2 and fuse3 + - upload coverage reports to codecov + - fix spurious failure in test_cache_files, #5438 + - add tests for Location.with_timestamp + - tox: add a non-fuse env to the envlist +- vagrant: + + - use python 3.7.latest and pyinstaller 4.0 for binary creation + - pyinstaller: compute basepath from spec file location + - vagrant: updates/fixes for archlinux box, #5543 +- docs: + + - "filename with spaces" example added to exclude file, #5236 + - add a hint about sleeping computer, #5301 + - how to adjust macOS >= Catalina security settings, #5303 + - process/policy for adding new compression algorithms + - updated docs about hacked backup client, #5480 + - improve ansible deployment docs, make it more generic + - how to approach borg speed issues, give speed example, #5371 + - fix mathematical inaccuracy about chunk size, #5336 + - add example for excluding content using --pattern cli option + - clarify borg create's '--one-file-system' option, #4009 + - improve docs/FAQ about append-only remote repos, #5497 + - fix reST markup issues, labels + - add infos about contributor retirement status + + +Version 1.2.0a9 (2020-10-05) +---------------------------- + +Fixes: + +- fix memory leak related to preloading, #5202 +- check --repair: fix potential data loss, #5325 +- persist shadow_index in between borg runs, #4830 +- fix hardlinked CACHEDIR.TAG processing, #4911 +- --read-special: .part files also should be regular files, #5217 +- allow server side enforcing of umask, --umask is for the local borg + process only (see docs), #4947 +- exit with 128 + signal number, #5161 +- borg config --list does not show last_segment_checked, #5159 +- locking: + + - fix ExclusiveLock race condition bug, #4923 + - fix race condition in lock migration, #4953 + - fix locking on openindiana, #5271 + +New features: + +- --content-from-command: create archive using stdout of given command, #5174 +- allow key-import + BORG_KEY_FILE to create key files +- build directory-based binary for macOS to avoid Gatekeeper delays + +Other changes: + +- upgrade bundled zstd to 1.4.5 +- upgrade bundled xxhash to 0.8.0, #5362 +- if self test fails, also point to OS and hardware, #5334 +- misc. shell completions fixes/updates, rewrite zsh completion +- prettier error message when archive gets too big, #5307 +- stop relying on `false` exiting with status code 1 +- rephrase some warnings, #5164 +- parseformat: unnecessary calls removed, #5169 +- testing: + + - enable Python3.9 env for test suite and VMs, #5373 + - drop python 3.5, #5344 + - misc. vagrant fixes/updates + - misc. testing fixes, #5196 +- docs: + + - add ssh-agent pull backup method to doc, #5288 + - mention double --force in prune docs + - update Homebrew install instructions, #5185 + - better description of how cache and rebuilds of it work + and how the workaround applies to that + - point to borg create --list item flags in recreate usage, #5165 + - add a note to create from stdin regarding files cache, #5180 + - add security faq explaining AES-CTR crypto issues, #5254 + - clarify --exclude-if-present in recreate, #5193 + - add socat pull mode, #5150, #900 + - move content of resources doc page to community project, #2088 + - explain hash collision, #4884 + - clarify --recompress option, #5154 + + +Version 1.2.0a8 (2020-04-22) +---------------------------- + +Fixes: + +- fixed potential index corruption / data loss issue due to bug in hashindex_set, #4829. + Please read and follow the more detailed notes close to the top of this document. +- fix crash when upgrading erroneous hints file, #4922 +- commit-time free space calc: ignore bad compact map entries, #4796 +- info: if the archive doesn't exist, print a pretty message, #4793 +- --prefix / -P: fix processing, avoid argparse issue, #4769 +- ignore EACCES (errno 13) when hardlinking, #4730 +- add a try catch when formatting the info string, #4818 +- check: do not stumble over invalid item key, #4845 +- update prevalence of env vars to set config and cache paths +- mount: fix FUSE low linear read speed on large files, #5032 +- extract: fix confusing output of borg extract --list --strip-components, #4934 +- recreate: support --timestamp option, #4745 +- fix ProgressIndicator msgids (JSON output), #4935 +- fuse: set f_namemax in statfs result, #2684 +- accept absolute paths on windows +- pyinstaller: work around issue with setuptools > 44 + +New features: + +- chunker speedup (plus regression test) +- added --consider-checkpoints and related test, #4788 +- added --noflags option, deprecate --nobsdflags option, #4489 +- compact: add --threshold option, #4674 +- mount: add birthtime to FUSE entries +- support platforms with no os.link, #4901 - if we don't have os.link, + we just extract another copy instead of making a hardlink. +- move sync_file_range to its own extension for better platform compatibility. +- new --bypass-lock option to bypass locking, e.g. for read-only repos +- accept absolute paths by removing leading slashes in patterns of all + sorts but re: style, #4029 +- delete: new --keep-security-info option + +Other changes: + +- support msgpack 0.6.2 and 1.0.0, #5065 +- upgrade bundled zstd to 1.4.4 +- upgrade bundled lz4 to 1.9.2 +- upgrade xxhash to 0.7.3 +- require recent enough llfuse for birthtime support, #5064 +- only store compressed data if the result actually is smaller, #4516 +- check: improve error output for matching index size, see #4829 +- ignore --stats when given with --dry-run, but continue, #4373 +- replaced usage of os.statvfs with shutil.disk_usage (better cross-platform support). +- fuse: remove unneeded version check and compat code, micro opts +- docs: + + - improve description of path variables + - document how to completely delete data, #2929 + - add FAQ about Borg config dir, #4941 + - add docs about errors not printed as JSON, #4073 + - update usage_general.rst.inc + - added "Will move with BORG_CONFIG_DIR variable unless specified." to BORG_SECURITY_DIR info. + - put BORG_SECURITY_DIR immediately below BORG_CONFIG_DIR (and moved BORG_CACHE_DIR up before them). + - add paragraph regarding cache security assumptions, #4900 + - tell about borg cache security precautions + - add FAQ describing difference between a local repo vs. repo on a server. + - document how to test exclusion patterns without performing an actual backup + - create: tell that "Calculating size" time and space needs are caused by --progress + - fix/improve documentation for @api decorator, #4674 + - add a pull backup / push restore how-to, #1552 + - fix man pages creation, #4752 + - more general FAQ for backup and retain original paths, #4532 + - explain difference between --exclude and --pattern, #4118 + - add FAQ for preventing SSH timeout in extract, #3866 + - improve password FAQ (decrease pw length, add -w 0 option to base64 to prevent line wrap), #4591 + - add note about patterns and stored paths, #4160 + - add upgrade of tools to pip installation how-to, #5090 + - document one cause of orphaned chunks in check command, #2295 + - clean up the whole check usage paragraph + - FAQ: linked recommended restrictions to ssh public keys on borg servers, #4946 + - fixed "doc downplays severity of Nonce reuse issue", #4883 + - borg repo restore instructions needed, #3428 + - new FAQ: A repo is corrupt and must be replaced with an older repo. + - clarify borg init's encryption modes +- native windows port: + + - update README_WINDOWS.rst + - updated pyinstaller spec file to support windows builds +- testing / CI: + + - improved travis config / install script, improved macOS builds + - allow osx builds to fail, #4955 + - Windows 10 build on Appveyor CI +- vagrant: + + - upgrade pyinstaller to v3.5 + patch + - use py369 for binary build, add py380 for tests + - fix issue in stretch VM hanging at grub installation + - add a debian buster and a ubuntu focal VM + - update darwin box to 10.12 + - upgrade FreeBSD box to 12.1 + - fix debianoid virtualenv packages + - use pyenv in freebsd64 VM + - remove the flake8 test + - darwin: avoid error if pkg is already installed + - debianoid: don't interactively ask questions + + +Version 1.2.0a7 (2019-09-07) +---------------------------- + +Fixes: + +- slave hardlinks extraction issue, see #4350 +- extract: fix KeyError for "partial" extraction, #4607 +- preload chunks for hardlink slaves w/o preloaded master, #4350 +- fix preloading for old remote servers, #4652 +- fix partial extract for hardlinked contentless file types, #4725 +- Repository.open: use stat() to check for repo dir, #4695 +- Repository.check_can_create_repository: use stat() to check, ~ #4695. +- SecurityManager.known(): check all files, #4614 +- after double-force delete, warn about necessary repair, #4704 +- cope with ANY error when importing pytest into borg.testsuite, #4652 +- fix invalid archive error message +- setup.py: fix detection of missing Cython +- filter out selinux xattrs, #4574 +- location arg - should it be optional? #4541 +- enable placeholder usage in --comment, #4559 +- use whitelist approach for borg serve, #4097 + +New features: + +- minimal native Windows support, see windows readme (work in progress) +- create: first ctrl-c (SIGINT) triggers checkpoint and abort, #4606 +- new BORG_WORKAROUNDS mechanism, basesyncfile, #4710 +- remove WSL autodetection. if WSL still has this problem, you need to + set BORG_WORKAROUNDS=basesyncfile in the borg process environment to + work around it. +- support xxh64 checksum in addition to the hashlib hashes in borg list +- enable placeholder usage in all extra archive arguments +- enable placeholder usage in --comment, #4559 +- enable placeholder usage in --glob-archives, #4495 +- ability to use a system-provided version of "xxhash" +- create: + + - changed the default behaviour to not store the atime of fs items. atime is + often rather not interesting and fragile - it easily changes even if nothing + else has changed and, if stored into the archive, spoils deduplication of + the archive metadata stream. + - if you give the --noatime option, borg will output a deprecation warning + because it is currently ignored / does nothing. + Please remove the --noatime option when using borg 1.2. + - added a --atime option for storing files' atime into an archive + +Other changes: + +- argparser: always use REPOSITORY in metavar +- do not check python/libc for borg serve, #4483 +- small borg compact improvements, #4522 +- compact: log freed space at INFO level +- tests: + + - tox / travis: add testing on py38-dev + - fix broken test that relied on improper zlib assumptions + - pure-py msgpack warning shall not make a lot of tests fail, #4558 + - rename test_mount_hardlinks to test_fuse_mount_hardlinks (master) + - vagrant: add up-to-date openindiana box (py35, openssl10) + - get rid of confusing coverage warning, #2069 +- docs: + + - reiterate that 'file cache names are absolute' in FAQ, + mention bind mount solution, #4738 + - add restore docs, #4670 + - updated docs to cover use of temp directory on remote, #4545 + - add a push-style example to borg-create(1), #4613 + - timestamps in the files cache are now usually ctime, #4583 + - benchmark crud: clarify that space is used until compact + - update documentation of borg create, + corrects a mention of borg 1.1 as a future version. + - fix osxfuse github link in installation docs + - how to supply a passphrase, use crypto devices, #4549 + - extract: document limitation "needs empty destination", #4598 + - update macOS Brew link + - add note about software for automating backup + - compact: improve docs, + - README: new URL for funding options + + +Version 1.2.0a6 (2019-04-22) +---------------------------- + +Fixes: + +- delete / prune: consider part files correctly for stats, #4507 +- fix "all archives" stats considering part files, #4329 +- create: only run stat_simple_attrs() once +- create: --stats does not work with --dry-run, exit with error msg, #4373 +- give "invalid repo" error msg if repo config not found, #4411 + +New features: + +- display msgpack version as part of sysinfo (e.g. in tracebacks) + +Other changes: + +- docs: + + - sdd "SSH Configuration" section, #4493, #3988, #636, #4485 + - better document borg check --max-duration, #4473 + - sorted commands help in multiple steps, #4471 +- testing: + + - travis: use py 3.5.3 and 3.6.7 on macOS to get a pyenv-based python + build with openssl 1.1 + - vagrant: use py 3.5.3 and 3.6.8 on darwin64 VM to build python and + borg with openssl 1.1 + - pytest: -v and default XDISTN to 1, #4481 + + +Version 1.2.0a5 (2019-03-21) +---------------------------- + +Fixes: + +- warn if a file has changed while being backed up, #1750 +- lrucache: regularly remove old FDs, #4427 +- borg command shall terminate with rc 2 for ImportErrors, #4424 +- make freebsd xattr platform code api compatible with linux, #3952 + +Other changes: + +- major setup code refactoring (especially how libraries like openssl, liblz4, + libzstd, libb2 are discovered and how it falls back to code bundled with + borg), new: uses pkg-config now (and needs python "pkgconfig" package + installed), #1925 + + if you are a borg package maintainer, please try packaging this + (see comments in setup.py). +- Vagrantfile: add zstd, reorder, build env vars, #4444 +- travis: install script improvements +- update shell completions +- docs: + + - add a sample logging.conf in docs/misc, #4380 + - fix spelling errors + - update requirements / install docs, #4374 + + +Version 1.2.0a4 (2019-03-11) +---------------------------- + +Fixes: + +- do not use O_NONBLOCK for special files, like FIFOs, block and char devices + when using --read-special. fixes backing up FIFOs. fixes to test. #4394 +- more LibreSSL build fixes: LibreSSL has HMAC_CTX_free and HMAC_CTX_new + +New features: + +- check: incremental repo check (only checks crc32 for segment entries), #1657 + borg check --repository-only --max-duration SECONDS ... +- delete: timestamp for borg delete --info added, #4359 + +Other changes: + +- redo stale lock handling, #3986 + drop BORG_HOSTNAME_IS_UNIQUE (please use BORG_HOST_ID if needed). + borg now always assumes it has a unique host id - either automatically + from fqdn plus uuid.getnode() or overridden via BORG_HOST_ID. +- docs: + + - added Alpine Linux to distribution list + - elaborate on append-only mode docs +- vagrant: + + - darwin: new 10.12 box + - freebsd: new 12.0 box + - openbsd: new 6.4 box + - misc. updates / fixes + + +Version 1.2.0a3 (2019-02-26) +---------------------------- + +Fixes: + +- LibreSSL build fixes, #4403 +- dummy ACL/xattr code fixes (used by OpenBSD and others), #4403 +- create: fix openat/statat issues for root directory, #4405 + + +Version 1.2.0a2 and earlier (2019-02-24) +---------------------------------------- + +New features: + +- compact: "borg compact" needs to be used to free repository space by + compacting the segments (reading sparse segments, rewriting still needed + data to new segments, deleting the sparse segments). + Borg < 1.2 invoked compaction automatically at the end of each repository + writing command. + Borg >= 1.2 does not do that any more to give better speed, more control, + more segment file stability (== less stuff moving to newer segments) and + more robustness. + See the docs about "borg compact" for more details. +- "borg compact --cleanup-commits" is to cleanup the tons of 17byte long + commit-only segment files caused by borg 1.1.x issue #2850. + Invoke this once after upgrading (the server side) borg to 1.2. + Compaction now automatically removes unneeded commit-only segment files. +- prune: Show which rule was applied to keep archive, #2886 +- add fixed blocksize chunker (see --chunker-params docs), #1086 + +Fixes: + +- avoid stale filehandle issues, #3265 +- use more FDs, avoid race conditions on active fs, #906, #908, #1038 +- add O_NOFOLLOW to base flags, #908 +- compact: + + - require >10% freeable space in a segment, #2985 + - repository compaction now automatically removes unneeded 17byte + commit-only segments, #2850 +- make swidth available on all posix platforms, #2667 + +Other changes: + +- repository: better speed and less stuff moving around by using separate + segment files for manifest DELETEs and PUTs, #3947 +- use pyinstaller v3.3.1 to build binaries +- update bundled zstd code to 1.3.8, #4210 +- update bundled lz4 code to 1.8.3, #4209 +- msgpack: + + - switch to recent "msgpack" pypi pkg name, #3890 + - wrap msgpack to avoid future compat complications, #3632, #2738 + - support msgpack 0.6.0 and 0.6.1, #4220, #4308 + +- llfuse: modernize / simplify llfuse version requirements +- code refactorings / internal improvements: + + - include size/csize/nfiles[_parts] stats into archive, #3241 + - calc_stats: use archive stats metadata, if available + - crypto: refactored crypto to use an AEAD style API + - crypto: new AES-OCB, CHACHA20-POLY1305 + - create: use less syscalls by not using a python file obj, #906, #3962 + - diff: refactor the diff functionality to new ItemDiff class, #2475 + - archive: create FilesystemObjectProcessors class + - helpers: make a package, split into smaller modules + - xattrs: move to platform package, use cython instead ctypes, #2495 + - xattrs/acls/bsdflags: misc. code/api optimizations + - FUSE: separate creation of filesystem from implementation of llfuse funcs, #3042 + - FUSE: use unpacker.tell() instead of deprecated write_bytes, #3899 + - setup.py: move build_man / build_usage code to setup_docs.py + - setup.py: update to use a newer Cython/setuptools API for compiling .pyx -> .c, #3788 + - use python 3.5's os.scandir / os.set_blocking + - multithreading preparations (not used yet): + + - item.to_optr(), Item.from_optr() + - fix chunker holding the GIL during blocking I/O + - C code portability / basic MSC compatibility, #4147, #2677 +- testing: + + - vagrant: new VMs for linux/bsd/darwin, most with OpenSSL 1.1 and py36 + + +Version 1.1.18 (2022-06-05) +--------------------------- + +Compatibility notes: + +- When upgrading from borg 1.0.x to 1.1.x, please note: + + - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1. + - borg upgrade: you do not need to and you also should not run it. + - borg might ask some security-related questions once after upgrading. + You can answer them either manually or via environment variable. + One known case is if you use unencrypted repositories, then it will ask + about a unknown unencrypted repository one time. + - your first backup with 1.1.x might be significantly slower (it might + completely read, chunk, hash a lot files) - this is due to the + --files-cache mode change (and happens every time you change mode). + You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible + mode (but that is less safe for detecting changed files than the default). + See the --files-cache docs for details. +- 1.1.11 removes WSL autodetection (Windows 10 Subsystem for Linux). + If WSL still has a problem with sync_file_range, you need to set + BORG_WORKAROUNDS=basesyncfile in the borg process environment to + work around the WSL issue. +- 1.1.14 changes return codes due to a bug fix: + In case you have scripts expecting rc == 2 for a signal exit, you need to + update them to check for >= 128 (as documented since long). +- 1.1.15 drops python 3.4 support, minimum requirement is 3.5 now. +- 1.1.17 install_requires the "packaging" pypi package now. + +New features: + +- check --repair: significantly speed up search for next valid object in segment, #6022 +- create: add retry_erofs workaround for O_NOATIME issue on volume shadow copies in WSL1, #6024 +- key export: display key if path is '-' or not given, #6092 +- list --format: add command_line to format keys, #6108 + +Fixes: + +- check: improve error handling for corrupt archive metadata block, + make robust_iterator more robust, #4777 +- diff: support presence change for blkdev, chrdev and fifo items, #6483 +- diff: reduce memory consumption, fix is_hardlink_master +- init: disallow overwriting of existing keyfiles +- info: fix authenticated mode repo to show "Encrypted: No", #6462 +- info: emit repo info even if repo has 0 archives, #6120 +- list: remove placeholders for shake_* hashes, #6082 +- mount -o versions: give clear error msg instead of crashing +- show_progress: add finished=true/false to archive_progress json, #6570 +- fix hardlinkable file type check, #6037 +- do not show archive name in error msgs referring to the repository, #6023 +- prettier error msg (no stacktrace) if exclude file is missing, #5734 +- do not require BORG_CONFIG_DIR if BORG_{SECURITY,KEYS}_DIR are set, #5979 +- atomically create the CACHE_TAG file, #6028 +- deal with the SaveFile/SyncFile race, docs, see #6176 5c5b59bc9 +- avoid expanding path into LHS of formatting operation + tests, #6064 #6063 +- repository: quota / compactable computation fixes, #6119. + This is mainly to keep the repo code in sync with borg 1.2. As borg 1.1 + compacts immediately, there was not really an issue with this in 1.1. +- fix transaction rollback: use files cache filename as found in txn.active, #6353 +- do not load files cache for commands not using it, fixes #5673 +- fix scp repo url parsing for ip v6 addrs, #6526 +- repo::archive location placeholder expansion fixes, #5826, #5998 + + - use expanded location for log output + - support placeholder expansion for BORG_REPO env var +- respect umask for created directory and file modes, #6400 +- safer truncate_and_unlink implementation + +Other changes: + +- upgrade bundled xxhash code to 0.8.1 +- fix xxh64 related build (setup.py and post-0.8.1 patch for static_assert). + The patch was required to build the bundled xxhash code on FreeBSD, see + https://github.com/Cyan4973/xxHash/pull/670 +- msgpack build: remove endianness macro, #6105 +- update and fix shell completions +- fuse: remove unneeded version check and compat code +- delete --force: do not ask when deleting a repo, #5941 +- delete: don't commit if nothing was deleted, avoid cache sync, #6060 +- delete: add repository id and location to prompt +- compact segments: improve freeable / freed space log output, #5679 +- if ensure_dir() fails, give more informative error message, #5952 +- load_key: no key is same as empty key, #6441 +- better error msg for defect or unsupported repo configs, #6566 +- use hmac.compare_digest instead of ==, #6470 +- implement more standard hashindex.setdefault behaviour +- remove stray punctuation from secure-erase message +- add development.lock.txt, use a real python 3.5 to generate frozen reqs +- setuptools 60.7.0 breaks pyinstaller, #6246 +- setup.py clean2 was added to work around some setuptools customizability limitation. +- allow extra compiler flags for every extension build +- C code: make switch fallthrough explicit +- Cython code: fix "useless trailing comma" cython warnings +- requirements.lock.txt: use the latest cython 0.29.30 +- fix compilation warnings: ‘PyUnicode_AsUnicode’ is deprecated +- docs: + + - ~/.config/borg/keys is not used for repokey keys, #6107 + - excluded parent dir's metadata can't restore, #6062 + - permissions note rewritten to make it less confusing, #5490 + - add note about grandfather-father-son backup retention policy / rotation scheme + - clarify who starts the remote agent (borg serve) + - test/improve pull backup docs, #5903 + - document the socat pull mode described in #900 #515ß + - borg serve: improve ssh forced commands docs, #6083 + - improve docs for borg list --format, #6080 + - fix the broken link to .nix file + - clarify pattern usage with commands, #5176 + - clarify user_id vs uid for fuse, #5723 + - fix binary build freebsd/macOS version, #5942 + - FAQ: fix manifest-timestamp path, #6016 + - remove duplicate faq entries, #5926 + - fix sphinx warnings, #5919 + - virtualisation speed tips + - fix values of TAG bytes, #6515 + - recommend umask for passphrase file perms + - update link to ubuntu packages, #6485 + - clarify on-disk order and size of log entry fields, #6357 + - do not transform --/--- to unicode dashes + - improve linking inside docs, link to borg_placeholders, link to borg_patterns + - use same phrasing in misc. help texts + - borg init: explain the encryption modes better + - explain the difference between a path that ends with or without a slash, #6297 + - clarify usage of patternfile roots, #6242 + - borg key export: add examples + - updates about features not experimental any more: FUSE "versions" view, --pattern*, #6134 + - fix/update cygwin package requirements + - impact of deleting path/to/repo/nonce, #5858 + - warn about tampered server nonce + - mention BORG_FILES_CACHE_SUFFIX as alternative to BORG_FILES_CACHE_TTL, #5602 + - add a troubleshooting note about "is not a valid repository" to the FAQ +- vagrant / CI / testing: + + - misc. fixes and updates, new python versions + - macOS on github: re-enable fuse2 testing by downgrading to older macOS, #6099 + - fix OpenBSD symlink mode test failure, #2055 + - use the generic/openbsd6 box + - strengthen the test: we can read data w/o nonces + - add tests for path/to/repo/nonce deletion + - darwin64: backport some tunings from master + - darwin64: remove fakeroot, #6314 + - darwin64: fix vagrant scp, #5921 + - darwin64: use macfuse instead of osxfuse + - add ubuntu "jammy" 22.04 LTS VM + - adapt memory for openindiana64 and darwin64 + + +Version 1.1.17 (2021-07-12) +--------------------------- + +Compatibility notes: + +- When upgrading from borg 1.0.x to 1.1.x, please note: + + - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1. + - borg upgrade: you do not need to and you also should not run it. + - borg might ask some security-related questions once after upgrading. + You can answer them either manually or via environment variable. + One known case is if you use unencrypted repositories, then it will ask + about a unknown unencrypted repository one time. + - your first backup with 1.1.x might be significantly slower (it might + completely read, chunk, hash a lot files) - this is due to the + --files-cache mode change (and happens every time you change mode). + You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible + mode (but that is less safe for detecting changed files than the default). + See the --files-cache docs for details. +- 1.1.11 removes WSL autodetection (Windows 10 Subsystem for Linux). + If WSL still has a problem with sync_file_range, you need to set + BORG_WORKAROUNDS=basesyncfile in the borg process environment to + work around the WSL issue. +- 1.1.14 changes return codes due to a bug fix: + In case you have scripts expecting rc == 2 for a signal exit, you need to + update them to check for >= 128 (as documented since long). +- 1.1.15 drops python 3.4 support, minimum requirement is 3.5 now. +- 1.1.17 install_requires the "packaging" pypi package now. + +Fixes: + +- pyinstaller dir-mode: fix pyi detection / LIBPATH treatment, #5897 +- handle crash due to kill stale lock race, #5828 +- fix BORG_CACHE_DIR crashing borg if empty, #5216 +- create --dry-run: fix display of kept tagfile, #5834 +- fix missing parameter in "did not consistently fail" msg, #5822 +- missing / healed chunks: always tell chunk ID, #5704 +- benchmark: make sure cleanup happens even on exceptions, #5630 + +New features: + +- implement BORG_SELFTEST env variable, #5871. + this can be used to accelerate borg startup a bit. not recommended for + normal usage, but borg mass hosters with a lot of borg invocations can + save some resources with this. on my laptop, this saved ~100ms cpu time + (sys+user) per borg command invocation. +- implement BORG_LIBC env variable to give the libc filename, #5870. + you can use this if a borg does not find your libc. +- check: add progress indicator for archive check. +- allow --files-cache=size (not recommended, make sure you know what you do) + +Other changes: + +- Python 3.10 now officially supported! + we test on py310-dev on github CI since a while and now also on the vagrant + machines, so it should work ok. +- github CI: test on py310 (again) +- get rid of distutils, use packaging and setuptools. + distutils is deprecated and gives warnings on py 3.10. +- setup.py: rename "clean" to "clean2" to avoid shadowing the "clean" command. +- remove libc filename fallback for the BSDs (there is no "usual" name) +- cleanup flake8 checks, fix some pep8 violations. +- docs building: replace deprecated function ".add_stylesheet()" for Sphinx 4 compatibility +- docs: + + - add a hint on sleeping computer and ssh connections, #5301 + - update the documentation on hacked backup client, #5480 + - improve docs/FAQ about append-only remote repos, #5497 + - complement the documentation for pattern files and exclude files, #5520 + - "filename with spaces" example added to exclude file, #5236 + note: no whitespace escaping needed, processed by borg. + - add info on renaming repositories, #5240 + - clarify borg check --verify-data, #5808 + - add notice about defective hardware to check documentation, #5753 + - add paragraph added in #5855 to utility documentation source + - add missing leading slashes in help patterns, #5857 + - clarify "you will need key and passphrase" borg init warning, #4622 + - pull mode: add some warnings, #5827 + - mention tar --compare (compare archive to fs files), #5880 + - fix typos, backport of #5597 +- vagrant: + + - add py3.7.11 for binary build, also add 3.10-dev. + - use latest Cython 0.29.23 for py310 compat fixes. + - more RAM for openindiana upgrade plan resolver, it just hangs (swaps?) if + there is too little RAM. + - fix install_pyenv to adapt to recent changes in pyenv (same as in master now). + - use generic/netbsd9 box, copied from master branch. + + +Version 1.1.16 (2021-03-23) +--------------------------- + +Fixes: + +- setup.py: add special openssl prefix for Apple M1 compatibility +- do not recurse into duplicate roots, #5603 +- remove empty shadowed_segments lists, #5275, #5614 +- fix libpython load error when borg fat binary / dir-based binary is invoked + via a symlink by upgrading pyinstaller to v4.2, #5688 +- config: accept non-int value (like 500M or 100G) for max_segment_size or + storage_quota, #5639. + please note: when setting a non-int value for this in a repo config, + using the repo will require borg >= 1.1.16. + +New features: + +- bundled msgpack: drop support for old buffer protocol to support Python 3.10 +- verbose files cache logging via --debug-topic=files_cache, #5659. + Use this if you suspect that borg does not detect unmodified files as expected. +- create/extract: add --noxattrs and --noacls option, #3955. + when given with borg create, borg will not get xattrs / ACLs from input files + (and thus, it will not archive xattrs / ACLs). when given with borg extract, + borg will not read xattrs / ACLs from archive and will not set xattrs / ACLs + on extracted files. +- diff: add --json-lines option, #3765 +- check: debug log segment filename +- borg debug dump-hints + +Other changes: + +- Tab completion support for additional archives for 'borg delete' +- repository: deduplicate code of put and delete, no functional change +- tests: fix result order issue (sporadic test failure on openindiana) +- vagrant: + + - upgrade pyinstaller to v4.2, #5671 + - avoid grub-install asking interactively for device + - remove the xenial box + - update freebsd box to 12.1 +- docs: + + - update macOS install instructions, #5677 + - use macFUSE (not osxfuse) for Apple M1 compatibility + - update docs for dev environment installation instructions, #5643 + - fix grammar in faq + - recommend running tests only on installed versions for setup + - add link back to git-installation + - remove /var/cache exclusion in example commands, #5625. + This is generally a poor idea and shouldn't be promoted through examples. + - add repology.org badge with current packaging status + - explain hash collision + - add unsafe workaround to use an old repo copy, #5722 + + +Version 1.1.15 (2020-12-25) +--------------------------- + +Fixes: + +- extract: + + - improve exception handling when setting xattrs, #5092. + - emit a warning message giving the path, xattr key and error message. + - continue trying to restore other xattrs and bsdflags of the same file + after an exception with xattr-setting happened. +- export-tar: + + - set tar format to GNU_FORMAT explicitly, #5274 + - fix memory leak with ssh: remote repository, #5568 + - fix potential memory leak with ssh: remote repository with partial extraction +- create: fix --dry-run and --stats coexistence, #5415 +- use --timestamp for {utcnow} and {now} if given, #5189 + +New features: + +- create: implement --stdin-mode, --stdin-user and --stdin-group, #5333 +- allow appending the files cache filename with BORG_FILES_CACHE_SUFFIX env var + +Other changes: + +- drop python 3.4 support, minimum requirement is 3.5 now. +- enable using libxxhash instead of bundled xxh64 code +- update llfuse requirements (1.3.8) +- set cython language_level in some files to fix warnings +- allow EIO with warning when trying to hardlink +- PropDict: fail early if internal_dict is not a dict +- update shell completions +- tests / CI + + - add a test for the hashindex corruption bug, #5531 #4829 + - fix spurious failure in test_cache_files, #5438 + - added a github ci workflow + - reduce testing on travis, no macOS, no py3x-dev, #5467 + - travis: use newer dists, native py on dist +- vagrant: + + - remove jessie and trusty boxes, #5348 #5383 + - pyinstaller 4.0, build on py379 + - binary build on stretch64, #5348 + - remove easy_install based pip installation +- docs: + + - clarify '--one-file-system' for btrfs, #5391 + - add example for excluding content using the --pattern cmd line arg + - complement the documentation for pattern files and exclude files, #5524 + - made ansible playbook more generic, use package instead of pacman. also + change state from "latest" to "present". + - complete documentation on append-only remote repos, #5497 + - internals: rather talk about target size than statistics, #5336 + - new compression algorithm policy, #1633 #5505 + - faq: add a hint on sleeping computer, #5301 + - note requirements for full disk access on macOS Catalina, #5303 + - fix/improve description of borg upgrade hardlink usage, #5518 +- modernize 1.1 code: + + - drop code/workarounds only needed to support Python 3.4 + - remove workaround for pre-release py37 argparse bug + - removed some outdated comments/docstrings + - requirements: remove some restrictions, lock on current versions + + +Version 1.1.14 (2020-10-07) +--------------------------- + +Fixes: + +- check --repair: fix potential data loss when interrupting it, #5325 +- exit with 128 + signal number (as documented) when borg is killed by a signal, #5161 +- fix hardlinked CACHEDIR.TAG processing, #4911 +- create --read-special: .part files also should be regular files, #5217 +- llfuse dependency: choose least broken 1.3.6/1.3.7. + 1.3.6 is broken on python 3.9, 1.3.7 is broken on FreeBSD. + +Other changes: + +- upgrade bundled xxhash to 0.7.4 +- self test: if it fails, also point to OS and hardware, #5334 +- pyinstaller: compute basepath from spec file location +- prettier error message when archive gets too big, #5307 +- check/recreate are not "experimental" any more (but still potentially dangerous): + + - recreate: remove extra confirmation + - rephrase some warnings, update docs, #5164 +- shell completions: + + - misc. updates / fixes + - support repositories in fish tab completion, #5256 + - complete $BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING + - rewrite zsh completion: + + - completion for almost all optional and positional arguments + - completion for Borg environment variables (parameters) +- use "allow/deny list" instead of "white/black list" wording +- declare "allow_cache_wipe" marker in setup.cfg to avoid pytest warning +- vagrant / tests: + + - misc. fixes / updates + - use python 3.5.10 for binary build + - build directory-based binaries additionally to the single file binaries + - add libffi-dev, required to build python + - use cryptography<3.0, more recent versions break the jessie box + - test on python 3.9 + - do brew update with /dev/null redirect to avoid "too much log output" on travis-ci +- docs: + + - add ssh-agent pull backup method docs, #5288 + - how to approach borg speed issues, #5371 + - mention double --force in prune docs + - update Homebrew install instructions, #5185 + - better description of how cache and rebuilds of it work + - point to borg create --list item flags in recreate usage, #5165 + - add security faq explaining AES-CTR crypto issues, #5254 + - add a note to create from stdin regarding files cache, #5180 + - fix borg.1 manpage generation regression, #5211 + - clarify how exclude options work in recreate, #5193 + - add section for retired contributors + - hint about not misusing private email addresses of contributors for borg support + + +Version 1.1.13 (2020-06-06) +--------------------------- + +Compatibility notes: + +- When upgrading from borg 1.0.x to 1.1.x, please note: + + - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1. + - borg upgrade: you do not need to and you also should not run it. + - borg might ask some security-related questions once after upgrading. + You can answer them either manually or via environment variable. + One known case is if you use unencrypted repositories, then it will ask + about a unknown unencrypted repository one time. + - your first backup with 1.1.x might be significantly slower (it might + completely read, chunk, hash a lot files) - this is due to the + --files-cache mode change (and happens every time you change mode). + You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible + mode (but that is less safe for detecting changed files than the default). + See the --files-cache docs for details. +- 1.1.11 removes WSL autodetection (Windows 10 Subsystem for Linux). + If WSL still has a problem with sync_file_range, you need to set + BORG_WORKAROUNDS=basesyncfile in the borg process environment to + work around the WSL issue. + +Fixes: + +- rebuilt using a current Cython version, compatible with python 3.8, #5214 + + +Version 1.1.12 (2020-06-06) +--------------------------- + +Fixes: + +- fix preload-related memory leak, #5202. +- mount / borgfs (FUSE filesystem): + + - fix FUSE low linear read speed on large files, #5067 + - fix crash on old llfuse without birthtime attrs, #5064 - accidentally + we required llfuse >= 1.3. Now also old llfuse works again. + - set f_namemax in statfs result, #2684 +- update precedence of env vars to set config and cache paths, #4894 +- correctly calculate compression ratio, taking header size into account, too + +New features: + +- --bypass-lock option to bypass locking with read-only repositories + +Other changes: + +- upgrade bundled zstd to 1.4.5 +- travis: adding comments and explanations to Travis config / install script, + improve macOS builds. +- tests: test_delete_force: avoid sporadic test setup issues, #5196 +- misc. vagrant fixes +- the binary for macOS is now built on macOS 10.12 +- the binaries for Linux are now built on Debian 8 "Jessie", #3761 +- docs: + + - PlaceholderError not printed as JSON, #4073 + - "How important is Borg config?", #4941 + - make Sphinx warnings break docs build, #4587 + - some markup / warning fixes + - add "updating borgbackup.org/releases" to release checklist, #4999 + - add "rendering docs" to release checklist, #5000 + - clarify borg init's encryption modes + - add note about patterns and stored paths, #4160 + - add upgrade of tools to pip installation how-to + - document one cause of orphaned chunks in check command, #2295 + - linked recommended restrictions to ssh public keys on borg servers in faq, #4946 + + +Version 1.1.11 (2020-03-08) +--------------------------- + +Compatibility notes: + +- When upgrading from borg 1.0.x to 1.1.x, please note: + + - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1. + - borg upgrade: you do not need to and you also should not run it. + - borg might ask some security-related questions once after upgrading. + You can answer them either manually or via environment variable. + One known case is if you use unencrypted repositories, then it will ask + about a unknown unencrypted repository one time. + - your first backup with 1.1.x might be significantly slower (it might + completely read, chunk, hash a lot files) - this is due to the + --files-cache mode change (and happens every time you change mode). + You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible + mode (but that is less safe for detecting changed files than the default). + See the --files-cache docs for details. +- 1.1.11 removes WSL autodetection (Windows 10 Subsystem for Linux). + If WSL still has a problem with sync_file_range, you need to set + BORG_WORKAROUNDS=basesyncfile in the borg process environment to + work around the WSL issue. + +Fixes: + +- fixed potential index corruption / data loss issue due to bug in hashindex_set, #4829. + Please read and follow the more detailed notes close to the top of this document. +- upgrade bundled xxhash to 0.7.3, #4891. + 0.7.2 is the minimum requirement for correct operations on ARMv6 in non-fixup + mode, where unaligned memory accesses cause bus errors. + 0.7.3 adds some speedups and libxxhash 0.7.3 even has a pkg-config file now. +- upgrade bundled lz4 to 1.9.2 +- upgrade bundled zstd to 1.4.4 +- fix crash when upgrading erroneous hints file, #4922 +- extract: + + - fix KeyError for "partial" extraction, #4607 + - fix "partial" extract for hardlinked contentless file types, #4725 + - fix preloading for old (0.xx) remote servers, #4652 + - fix confusing output of borg extract --list --strip-components, #4934 +- delete: after double-force delete, warn about necessary repair, #4704 +- create: give invalid repo error msg if repo config not found, #4411 +- mount: fix FUSE mount missing st_birthtime, #4763 #4767 +- check: do not stumble over invalid item key, #4845 +- info: if the archive doesn't exist, print a pretty message, #4793 +- SecurityManager.known(): check all files, #4614 +- Repository.open: use stat() to check for repo dir, #4695 +- Repository.check_can_create_repository: use stat() to check, #4695 +- fix invalid archive error message +- fix optional/non-optional location arg, #4541 +- commit-time free space calc: ignore bad compact map entries, #4796 +- ignore EACCES (errno 13) when hardlinking the old config, #4730 +- --prefix / -P: fix processing, avoid argparse issue, #4769 + +New features: + +- enable placeholder usage in all extra archive arguments +- new BORG_WORKAROUNDS mechanism, basesyncfile, #4710 +- recreate: support --timestamp option, #4745 +- support platforms without os.link (e.g. Android with Termux), #4901. + if we don't have os.link, we just extract another copy instead of making a hardlink. +- support linux platforms without sync_file_range (e.g. Android 7 with Termux), #4905 + +Other: + +- ignore --stats when given with --dry-run, but continue, #4373 +- add some ProgressIndicator msgids to code / fix docs, #4935 +- elaborate on "Calculating size" message +- argparser: always use REPOSITORY in metavar, also use more consistent help phrasing. +- check: improve error output for matching index size, see #4829 +- docs: + + - changelog: add advisory about hashindex_set bug #4829 + - better describe BORG_SECURITY_DIR, BORG_CACHE_DIR, #4919 + - infos about cache security assumptions, #4900 + - add FAQ describing difference between a local repo vs. repo on a server. + - document how to test exclusion patterns without performing an actual backup + - timestamps in the files cache are now usually ctime, #4583 + - fix bad reference to borg compact (does not exist in 1.1), #4660 + - create: borg 1.1 is not future any more + - extract: document limitation "needs empty destination", #4598 + - how to supply a passphrase, use crypto devices, #4549 + - fix osxfuse github link in installation docs + - add example of exclude-norecurse rule in help patterns + - update macOS Brew link + - add note about software for automating backups, #4581 + - AUTHORS: mention copyright+license for bundled msgpack + - fix various code blocks in the docs, #4708 + - updated docs to cover use of temp directory on remote, #4545 + - add restore docs, #4670 + - add a pull backup / push restore how-to, #1552 + - add FAQ how to retain original paths, #4532 + - explain difference between --exclude and --pattern, #4118 + - add FAQs for SSH connection issues, #3866 + - improve password FAQ, #4591 + - reiterate that 'file cache names are absolute' in FAQ +- tests: + + - cope with ANY error when importing pytest into borg.testsuite, #4652 + - fix broken test that relied on improper zlib assumptions + - test_fuse: filter out selinux xattrs, #4574 +- travis / vagrant: + + - misc python versions removed / changed (due to openssl 1.1 compatibility) + or added (3.7 and 3.8, for better borg compatibility testing) + - binary building is on python 3.5.9 now +- vagrant: + + - add new boxes: ubuntu 18.04 and 20.04, debian 10 + - update boxes: openindiana, darwin, netbsd + - remove old boxes: centos 6 + - darwin: updated osxfuse to 3.10.4 + - use debian/ubuntu pip/virtualenv packages + - rather use python 3.6.2 than 3.6.0, fixes coverage/sqlite3 issue + - use requirements.d/development.lock.txt to avoid compat issues +- travis: + + - darwin: backport some install code / order from master + - remove deprecated keyword "sudo" from travis config + - allow osx builds to fail, #4955 + this is due to travis-ci frequently being so slow that the OS X builds + just fail because they exceed 50 minutes and get killed by travis. + + +Version 1.1.10 (2019-05-16) +--------------------------- + +Fixes: + +- extract: hang on partial extraction with ssh: repo, when hardlink master + is not matched/extracted and borg hangs on related slave hardlink, #4350 +- lrucache: regularly remove old FDs, #4427 +- avoid stale filehandle issues, #3265 +- freebsd: make xattr platform code api compatible with linux, #3952 +- use whitelist approach for borg serve, #4097 +- borg command shall terminate with rc 2 for ImportErrors, #4424 +- create: only run stat_simple_attrs() once, this increases + backup with lots of unchanged files performance by ~ 5%. +- prune: fix incorrect borg prune --stats output with --dry-run, #4373 +- key export: emit user-friendly error if repo key is exported to a directory, + #4348 + +New features: + +- bundle latest supported msgpack-python release (0.5.6), remove msgpack-python + from setup.py install_requires - by default we use the bundled code now. + optionally, we still support using an external msgpack (see hints in + setup.py), but this requires solid requirements management within + distributions and is not recommended. + borgbackup will break if you upgrade msgpack to an unsupported version. +- display msgpack version as part of sysinfo (e.g. in tracebacks) +- timestamp for borg delete --info added, #4359 +- enable placeholder usage in --comment and --glob-archives, #4559, #4495 + +Other: + +- serve: do not check python/libc for borg serve, #4483 +- shell completions: borg diff second archive +- release scripts: signing binaries with Qubes OS support +- testing: + + - vagrant: upgrade openbsd box to 6.4 + - travis-ci: lock test env to py 3.4 compatible versions, #4343 + - get rid of confusing coverage warning, #2069 + - rename test_mount_hardlinks to test_fuse_mount_hardlinks, + so both can be excluded by "not test_fuse". + - pure-py msgpack warning shall not make a lot of tests fail, #4558 +- docs: + + - add "SSH Configuration" section to "borg serve", #3988, #636, #4485 + - README: new URL for funding options + - add a sample logging.conf in docs/misc, #4380 + - elaborate on append-only mode docs, #3504 + - installation: added Alpine Linux to distribution list, #4415 + - usage.html: only modify window.location when redirecting, #4133 + - add msgpack license to docs/3rd_party/msgpack +- vagrant / binary builds: + + - use python 3.5.7 for builds + - use osxfuse 3.8.3 + + +Version 1.1.9 (2019-02-10) +-------------------------- + +Compatibility notes: + +- When upgrading from borg 1.0.x to 1.1.x, please note: + + - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1. + - borg upgrade: you do not need to and you also should not run it. + - borg might ask some security-related questions once after upgrading. + You can answer them either manually or via environment variable. + One known case is if you use unencrypted repositories, then it will ask + about a unknown unencrypted repository one time. + - your first backup with 1.1.x might be significantly slower (it might + completely read, chunk, hash a lot files) - this is due to the + --files-cache mode change (and happens every time you change mode). + You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible + mode (but that is less safe for detecting changed files than the default). + See the --files-cache docs for details. + +Fixes: + +- security fix: configure FUSE with "default_permissions", #3903 + "default_permissions" is now enforced by borg by default to let the + kernel check uid/gid/mode based permissions. + "ignore_permissions" can be given to not enforce "default_permissions". +- make "hostname" short, even on misconfigured systems, #4262 +- fix free space calculation on macOS (and others?), #4289 +- config: quit with error message when no key is provided, #4223 +- recover_segment: handle too small segment files correctly, #4272 +- correctly release memoryview, #4243 +- avoid diaper pattern in configparser by opening files, #4263 +- add "# cython: language_level=3" directive to .pyx files, #4214 +- info: consider part files for "This archive" stats, #3522 +- work around Microsoft WSL issue #645 (sync_file_range), #1961 + +New features: + +- add --rsh command line option to complement BORG_RSH env var, #1701 +- init: --make-parent-dirs parent1/parent2/repo_dir, #4235 + +Other: + +- add archive name to check --repair output, #3447 +- check for unsupported msgpack versions +- shell completions: + + - new shell completions for borg 1.1.9 + - more complete shell completions for borg mount -o + - added shell completions for borg help + - option arguments for zsh tab completion +- docs: + + - add FAQ regarding free disk space check, #3905 + - update BORG_PASSCOMMAND example and clarify variable expansion, #4249 + - FAQ regarding change of compression settings, #4222 + - add note about BSD flags to changelog, #4246 + - improve logging in example automation script + - add note about files changing during backup, #4081 + - work around the backslash issue, #4280 + - update release workflow using twine (docs, scripts), #4213 + - add warnings on repository copies to avoid future problems, #4272 +- tests: + + - fix the homebrew 1.9 issues on travis-ci, #4254 + - fix duplicate test method name, #4311 + + +Version 1.1.8 (2018-12-09) +-------------------------- + +Fixes: + +- enforce storage quota if set by serve-command, #4093 +- invalid locations: give err msg containing parsed location, #4179 +- list repo: add placeholders for hostname and username, #4130 +- on linux, symlinks can't have ACLs, so don't try to set any, #4044 + +New features: + +- create: added PATH::archive output on INFO log level +- read a passphrase from a file descriptor specified in the + BORG_PASSPHRASE_FD environment variable. + +Other: + +- docs: + + - option --format is required for some expensive-to-compute values for json + + borg list by default does not compute expensive values except when + they are needed. whether they are needed is determined by the format, + in standard mode as well as in --json mode. + - tell that our binaries are x86/x64 amd/intel, bauerj has ARM + - fixed wrong archive name pattern in CRUD benchmark help + - fixed link to cachedir spec in docs, #4140 +- tests: + + - stop using fakeroot on travis, avoids sporadic EISDIR errors, #2482 + - xattr key names must start with "user." on linux + - fix code so flake8 3.6 does not complain + - explicitly convert environment variable to str, #4136 + - fix DeprecationWarning: Flags not at the start of the expression, #4137 + - support pytest4, #4172 +- vagrant: + + - use python 3.5.6 for builds + + +Version 1.1.7 (2018-08-11) +-------------------------- + +Compatibility notes: + +- added support for Python 3.7 + +Fixes: + +- cache lock: use lock_wait everywhere to fix infinite wait, see #3968 +- don't archive tagged dir when recursing an excluded dir, #3991 +- py37 argparse: work around bad default in py 3.7.0a/b/rc, #3996 +- py37 remove loggerDict.clear() from tearDown method, #3805 +- some fixes for bugs which likely did not result in problems in practice: + + - fixed logic bug in platform module API version check + - fixed xattr/acl function prototypes, added missing ones + +New features: + +- init: add warning to store both key and passphrase at safe place(s) +- BORG_HOST_ID env var to work around all-zero MAC address issue, #3985 +- borg debug dump-repo-objs --ghost (dump everything from segment files, + including deleted or superseded objects or commit tags) +- borg debug search-repo-objs (search in repo objects for hex bytes or strings) + +Other changes: + +- add Python 3.7 support +- updated shell completions +- call socket.gethostname only once +- locking: better logging, add some asserts +- borg debug dump-repo-objs: + + - filename layout improvements + - use repository.scan() to get on-disk order +- docs: + + - update installation instructions for macOS + - added instructions to install fuse via homebrew + - improve diff docs + - added note that checkpoints inside files requires 1.1+ + - add link to tempfile module + - remove row/column-spanning from docs source, #4000 #3990 +- tests: + + - fetch less data via os.urandom + - add py37 env for tox + - travis: add 3.7, remove 3.6-dev (we test with -dev in master) +- vagrant / binary builds: + + - use osxfuse 3.8.2 + - use own (uptodate) openindiana box + + +Version 1.1.6 (2018-06-11) +-------------------------- + +Compatibility notes: + +- 1.1.6 changes: + + - also allow msgpack-python 0.5.6. + +Fixes: + +- fix borg exception handling on ENOSPC error with xattrs, #3808 +- prune: fix/improve overall progress display +- borg config repo ... does not need cache/manifest/key, #3802 +- debug dump-repo-objs should not depend on a manifest obj +- pypi package: + + - include .coveragerc, needed by tox.ini + - fix package long description, #3854 + +New features: + +- mount: add uid, gid, umask mount options +- delete: + + - only commit once, #3823 + - implement --dry-run, #3822 +- check: + + - show progress while rebuilding missing manifest, #3787 + - more --repair output +- borg config --list , #3612 + +Other changes: + +- update msgpack requirement, #3753 +- update bundled zstd to 1.3.4, #3745 +- update bundled lz4 code to 1.8.2, #3870 +- docs: + + - describe what BORG_LIBZSTD_PREFIX does + - fix and deduplicate encryption quickstart docs, #3776 +- vagrant: + + - FUSE for macOS: upgrade 3.7.1 to 3.8.0 + - exclude macOS High Sierra upgrade on the darwin64 machine + - remove borgbackup.egg-info dir in fs_init (after rsync) + - use pyenv-based build/test on jessie32/62 + - use local 32 and 64bit debian jessie boxes + - use "vagrant" as username for new xenial box +- travis OS X: use xcode 8.3 (not broken) + + +Version 1.1.5 (2018-04-01) +-------------------------- + +Compatibility notes: + +- 1.1.5 changes: + + - require msgpack-python >= 0.4.6 and < 0.5.0. + 0.5.0+ dropped python 3.4 testing and also caused some other issues because + the python package was renamed to msgpack and emitted some FutureWarning. + +Fixes: + +- create --list: fix that it was never showing M status, #3492 +- create: fix timing for first checkpoint (read files cache early, init + checkpoint timer after that), see #3394 +- extract: set rc=1 when extracting damaged files with all-zero replacement + chunks or with size inconsistencies, #3448 +- diff: consider an empty file as different to a non-existing file, #3688 +- files cache: improve exception handling, #3553 +- ignore exceptions in scandir_inorder() caused by an implicit stat(), + also remove unneeded sort, #3545 +- fixed tab completion problem where a space is always added after path even + when it shouldn't +- build: do .h file content checks in binary mode, fixes build issue for + non-ascii header files on pure-ascii locale platforms, #3544 #3639 +- borgfs: fix patterns/paths processing, #3551 +- config: add some validation, #3566 +- repository config: add validation for max_segment_size, #3592 +- set cache previous_location on load instead of save +- remove platform.uname() call which caused library mismatch issues, #3732 +- add exception handler around deprecated platform.linux_distribution() call +- use same datetime object for {now} and {utcnow}, #3548 + +New features: + +- create: implement --stdin-name, #3533 +- add chunker_params to borg archive info (--json) +- BORG_SHOW_SYSINFO=no to hide system information from exceptions + +Other changes: + +- updated zsh completions for borg 1.1.4 +- files cache related code cleanups +- be more helpful when parsing invalid --pattern values, #3575 +- be more clear in secure-erase warning message, #3591 +- improve getpass user experience, #3689 +- docs build: unicode problem fixed when using a py27-based sphinx +- docs: + + - security: explicitly note what happens OUTSIDE the attack model + - security: add note about combining compression and encryption + - security: describe chunk size / proximity issue, #3687 + - quickstart: add note about permissions, borg@localhost, #3452 + - quickstart: add introduction to repositories & archives, #3620 + - recreate --recompress: add missing metavar, clarify description, #3617 + - improve logging docs, #3549 + - add an example for --pattern usage, #3661 + - clarify path semantics when matching, #3598 + - link to offline documentation from README, #3502 + - add docs on how to verify a signed release with GPG, #3634 + - chunk seed is generated per repository (not: archive) + - better formatting of CPU usage documentation, #3554 + - extend append-only repo rollback docs, #3579 +- tests: + + - fix erroneously skipped zstd compressor tests, #3606 + - skip a test if argparse is broken, #3705 +- vagrant: + + - xenial64 box now uses username 'vagrant', #3707 + - move cleanup steps to fs_init, #3706 + - the boxcutter wheezy boxes are 404, use local ones + - update to Python 3.5.5 (for binary builds) + + +Version 1.1.4 (2017-12-31) +-------------------------- + +Compatibility notes: + +- When upgrading from borg 1.0.x to 1.1.x, please note: + + - read all the compatibility notes for 1.1.0*, starting from 1.1.0b1. + - borg upgrade: you do not need to and you also should not run it. + - borg might ask some security-related questions once after upgrading. + You can answer them either manually or via environment variable. + One known case is if you use unencrypted repositories, then it will ask + about a unknown unencrypted repository one time. + - your first backup with 1.1.x might be significantly slower (it might + completely read, chunk, hash a lot files) - this is due to the + --files-cache mode change (and happens every time you change mode). + You can avoid the one-time slowdown by using the pre-1.1.0rc4-compatible + mode (but that is less safe for detecting changed files than the default). + See the --files-cache docs for details. +- borg 1.1.4 changes: + + - zstd compression is new in borg 1.1.4, older borg can't handle it. + - new minimum requirements for the compression libraries - if the required + versions (header and lib) can't be found at build time, bundled code will + be used: + + - added requirement: libzstd >= 1.3.0 (bundled: 1.3.2) + - updated requirement: liblz4 >= 1.7.0 / r129 (bundled: 1.8.0) + +Fixes: + +- check: data corruption fix: fix for borg check --repair malfunction, #3444. + See the more detailed notes close to the top of this document. +- delete: also delete security dir when deleting a repo, #3427 +- prune: fix building the "borg prune" man page, #3398 +- init: use given --storage-quota for local repo, #3470 +- init: properly quote repo path in output +- fix startup delay with dns-only own fqdn resolving, #3471 + +New features: + +- added zstd compression. try it! +- added placeholder {reverse-fqdn} for fqdn in reverse notation +- added BORG_BASE_DIR environment variable, #3338 + +Other changes: + +- list help topics when invalid topic is requested +- fix lz4 deprecation warning, requires lz4 >= 1.7.0 (r129) +- add parens for C preprocessor macro argument usages (did not cause malfunction) +- exclude broken pytest 3.3.0 release +- updated fish/bash completions +- init: more clear exception messages for borg create, #3465 +- docs: + + - add auto-generated docs for borg config + - don't generate HTML docs page for borgfs, #3404 + - docs update for lz4 b2 zstd changes + - add zstd to compression help, readme, docs + - update requirements and install docs about bundled lz4 and zstd +- refactored build of the compress and crypto.low_level extensions, #3415: + + - move some lib/build related code to setup_{zstd,lz4,b2}.py + - bundle lz4 1.8.0 (requirement: >= 1.7.0 / r129) + - bundle zstd 1.3.2 (requirement: >= 1.3.0) + - blake2 was already bundled + - rename BORG_LZ4_PREFIX env var to BORG_LIBLZ4_PREFIX for better consistency: + we also have BORG_LIBB2_PREFIX and BORG_LIBZSTD_PREFIX now. + - add prefer_system_lib* = True settings to setup.py - by default the build + will prefer a shared library over the bundled code, if library and headers + can be found and meet the minimum requirements. + + +Version 1.1.3 (2017-11-27) +-------------------------- + +Fixes: + +- Security Fix for CVE-2017-15914: Incorrect implementation of access controls + allows remote users to override repository restrictions in Borg servers. + A user able to access a remote Borg SSH server is able to circumvent access + controls post-authentication. + Affected releases: 1.1.0, 1.1.1, 1.1.2. Releases 1.0.x are NOT affected. +- crc32: deal with unaligned buffer, add tests - this broke borg on older ARM + CPUs that can not deal with unaligned 32bit memory accesses and raise a bus + error in such cases. the fix might also improve performance on some CPUs as + all 32bit memory accesses by the crc32 code are properly aligned now. #3317 +- mount: fixed support of --consider-part-files and do not show .borg_part_N + files by default in the mounted FUSE filesystem. #3347 +- fixed cache/repo timestamp inconsistency message, highlight that information + is obtained from security dir (deleting the cache will not bypass this error + in case the user knows this is a legitimate repo). +- borgfs: don't show sub-command in borgfs help, #3287 +- create: show an error when --dry-run and --stats are used together, #3298 + +New features: + +- mount: added exclusion group options and paths, #2138 + + Reused some code to support similar options/paths as borg extract offers - + making good use of these to only mount a smaller subset of dirs/files can + speed up mounting a lot and also will consume way less memory. + + borg mount [options] repo_or_archive mountpoint path [paths...] + + paths: you can just give some "root paths" (like for borg extract) to + only partially populate the FUSE filesystem. + + new options: --exclude[-from], --pattern[s-from], --strip-components +- create/extract: support st_birthtime on platforms supporting it, #3272 +- add "borg config" command for querying/setting/deleting config values, #3304 + +Other changes: + +- clean up and simplify packaging (only package committed files, do not install + .c/.h/.pyx files) +- docs: + + - point out tuning options for borg create, #3239 + - add instructions for using ntfsclone, zerofree, #81 + - move image backup-related FAQ entries to a new page + - clarify key aliases for borg list --format, #3111 + - mention break-lock in checkpointing FAQ entry, #3328 + - document sshfs rename workaround, #3315 + - add FAQ about removing files from existing archives + - add FAQ about different prune policies + - usage and man page for borgfs, #3216 + - clarify create --stats duration vs. wall time, #3301 + - clarify encrypted key format for borg key export, #3296 + - update release checklist about security fixes + - document good and problematic option placements, fix examples, #3356 + - add note about using --nobsdflags to avoid speed penalty related to + bsdflags, #3239 + - move most of support section to www.borgbackup.org + + +Version 1.1.2 (2017-11-05) +-------------------------- + +Fixes: + +- fix KeyError crash when talking to borg server < 1.0.7, #3244 +- extract: set bsdflags last (include immutable flag), #3263 +- create: don't do stat() call on excluded-norecurse directory, fix exception + handling for stat() call, #3209 +- create --stats: do not count data volume twice when checkpointing, #3224 +- recreate: move chunks_healthy when excluding hardlink master, #3228 +- recreate: get rid of chunks_healthy when rechunking (does not match), #3218 +- check: get rid of already existing not matching chunks_healthy metadata, #3218 +- list: fix stdout broken pipe handling, #3245 +- list/diff: remove tag-file options (not used), #3226 + +New features: + +- bash, zsh and fish shell auto-completions, see scripts/shell_completions/ +- added BORG_CONFIG_DIR env var, #3083 + +Other changes: + +- docs: + + - clarify using a blank passphrase in keyfile mode + - mention "!" (exclude-norecurse) type in "patterns" help + - document to first heal before running borg recreate to re-chunk stuff, + because that will have to get rid of chunks_healthy metadata. + - more than 23 is not supported for CHUNK_MAX_EXP, #3115 + - borg does not respect nodump flag by default any more + - clarify same-filesystem requirement for borg upgrade, #2083 + - update / rephrase cygwin / WSL status, #3174 + - improve docs about --stats, #3260 +- vagrant: openindiana new clang package + +Already contained in 1.1.1 (last minute fix): + +- arg parsing: fix fallback function, refactor, #3205. This is a fixup + for #3155, which was broken on at least python <= 3.4.2. + + +Version 1.1.1 (2017-10-22) +-------------------------- + +Compatibility notes: + +- The deprecated --no-files-cache is not a global/common option any more, + but only available for borg create (it is not needed for anything else). + Use --files-cache=disabled instead of --no-files-cache. +- The nodump flag ("do not backup this file") is not honoured any more by + default because this functionality (esp. if it happened by error or + unexpected) was rather confusing and unexplainable at first to users. + If you want that "do not backup NODUMP-flagged files" behaviour, use: + borg create --exclude-nodump ... +- If you are on Linux and do not need bsdflags archived, consider using + ``--nobsdflags`` with ``borg create`` to avoid additional syscalls and + speed up backup creation. + +Fixes: + +- borg recreate: correctly compute part file sizes. fixes cosmetic, but + annoying issue as borg check complains about size inconsistencies of part + files in affected archives. you can solve that by running borg recreate on + these archives, see also #3157. +- bsdflags support: do not open BLK/CHR/LNK files, avoid crashes and + slowness, #3130 +- recreate: don't crash on attic archives w/o time_end, #3109 +- don't crash on repository filesystems w/o hardlink support, #3107 +- don't crash in first part of truncate_and_unlink, #3117 +- fix server-side IndexError crash with clients < 1.0.7, #3192 +- don't show traceback if only a global option is given, show help, #3142 +- cache: use SaveFile for more safety, #3158 +- init: fix wrong encryption choices in command line parser, fix missing + "authenticated-blake2", #3103 +- move --no-files-cache from common to borg create options, #3146 +- fix detection of non-local path (failed on ..filename), #3108 +- logging with fileConfig: set json attr on "borg" logger, #3114 +- fix crash with relative BORG_KEY_FILE, #3197 +- show excluded dir with "x" for tagged dirs / caches, #3189 + +New features: + +- create: --nobsdflags and --exclude-nodump options, #3160 +- extract: --nobsdflags option, #3160 + +Other changes: + +- remove annoying hardlinked symlinks warning, #3175 +- vagrant: use self-made FreeBSD 10.3 box, #3022 +- travis: don't brew update, hopefully fixes #2532 +- docs: + + - readme: -e option is required in borg 1.1 + - add example showing --show-version --show-rc + - use --format rather than --list-format (deprecated) in example + - update docs about hardlinked symlinks limitation + + +Version 1.1.0 (2017-10-07) +-------------------------- + +Compatibility notes: + +- borg command line: do not put options in between positional arguments + + This sometimes works (e.g. it worked in borg 1.0.x), but can easily stop + working if we make positional arguments optional (like it happened for + borg create's "paths" argument in 1.1). There are also places in borg 1.0 + where we do that, so it doesn't work there in general either. #3356 + + Good: borg create -v --stats repo::archive path + Good: borg create repo::archive path -v --stats + Bad: borg create repo::archive -v --stats path + +Fixes: + +- fix LD_LIBRARY_PATH restoration for subprocesses, #3077 +- "auto" compression: make sure expensive compression is actually better, + otherwise store lz4 compressed data we already computed. + +Other changes: + +- docs: + + - FAQ: we do not implement futile attempts of ETA / progress displays + - manpage: fix typos, update homepage + - implement simple "issue" role for manpage generation, #3075 + + +Version 1.1.0rc4 (2017-10-01) +----------------------------- + +Compatibility notes: + +- A borg server >= 1.1.0rc4 does not support borg clients 1.1.0b3-b5. #3033 +- The files cache is now controlled differently and has a new default mode: + + - the files cache now uses ctime by default for improved file change + detection safety. You can still use mtime for more speed and less safety. + - --ignore-inode is deprecated (use --files-cache=... without "inode") + - --no-files-cache is deprecated (use --files-cache=disabled) + +New features: + +- --files-cache - implement files cache mode control, #911 + You can now control the files cache mode using this option: + --files-cache={ctime,mtime,size,inode,rechunk,disabled} + (only some combinations are supported). See the docs for details. + +Fixes: + +- remote progress/logging: deal with partial lines, #2637 +- remote progress: flush json mode output +- fix subprocess environments, #3050 (and more) + +Other changes: + +- remove client_supports_log_v3 flag, #3033 +- exclude broken Cython 0.27(.0) in requirements, #3066 +- vagrant: + + - upgrade to FUSE for macOS 3.7.1 + - use Python 3.5.4 to build the binaries +- docs: + + - security: change-passphrase only changes the passphrase, #2990 + - fixed/improved borg create --compression examples, #3034 + - add note about metadata dedup and --no[ac]time, #2518 + - twitter account @borgbackup now, better visible, #2948 + - simplified rate limiting wrapper in FAQ + + +Version 1.1.0rc3 (2017-09-10) +----------------------------- + +New features: + +- delete: support naming multiple archives, #2958 + +Fixes: + +- repo cleanup/write: invalidate cached FDs, #2982 +- fix datetime.isoformat() microseconds issues, #2994 +- recover_segment: use mmap(), lower memory needs, #2987 + +Other changes: + +- with-lock: close segment file before invoking subprocess +- keymanager: don't depend on optional readline module, #2976 +- docs: + + - fix macOS keychain integration command + - show/link new screencasts in README, #2936 + - document utf-8 locale requirement for json mode, #2273 +- vagrant: clean up shell profile init, user name, #2977 +- test_detect_attic_repo: don't test mount, #2975 +- add debug logging for repository cleanup + + +Version 1.1.0rc2 (2017-08-28) +----------------------------- + +Compatibility notes: + +- list: corrected mix-up of "isomtime" and "mtime" formats. Previously, + "isomtime" was the default but produced a verbose human format, + while "mtime" produced a ISO-8601-like format. + The behaviours have been swapped (so "mtime" is human, "isomtime" is ISO-like), + and the default is now "mtime". + "isomtime" is now a real ISO-8601 format ("T" between date and time, not a space). + +New features: + +- None. + +Fixes: + +- list: fix weird mixup of mtime/isomtime +- create --timestamp: set start time, #2957 +- ignore corrupt files cache, #2939 +- migrate locks to child PID when daemonize is used +- fix exitcode of borg serve, #2910 +- only compare contents when chunker params match, #2899 +- umount: try fusermount, then try umount, #2863 + +Other changes: + +- JSON: use a more standard ISO 8601 datetime format, #2376 +- cache: write_archive_index: truncate_and_unlink on error, #2628 +- detect non-upgraded Attic repositories, #1933 +- delete various nogil and threading related lines +- coala / pylint related improvements +- docs: + + - renew asciinema/screencasts, #669 + - create: document exclusion through nodump, #2949 + - minor formatting fixes + - tar: tarpipe example + - improve "with-lock" and "info" docs, #2869 + - detail how to use macOS/GNOME/KDE keyrings for repo passwords, #392 +- travis: only short-circuit docs-only changes for pull requests +- vagrant: + + - netbsd: bash is already installed + - fix netbsd version in PKG_PATH + - add exe location to PATH when we build an exe + + +Version 1.1.0rc1 (2017-07-24) +----------------------------- + +Compatibility notes: + +- delete: removed short option for --cache-only + +New features: + +- support borg list repo --format {comment} {bcomment} {end}, #2081 +- key import: allow reading from stdin, #2760 + +Fixes: + +- with-lock: avoid creating segment files that might be overwritten later, #1867 +- prune: fix checkpoints processing with --glob-archives +- FUSE: versions view: keep original file extension at end, #2769 +- fix --last, --first: do not accept values <= 0, + fix reversed archive ordering with --last +- include testsuite data (attic.tar.gz) when installing the package +- use limited unpacker for outer key, for manifest (both security precautions), + #2174 #2175 +- fix bashism in shell scripts, #2820, #2816 +- cleanup endianness detection, create _endian.h, + fixes build on alpine linux, #2809 +- fix crash with --no-cache-sync (give known chunk size to chunk_incref), #2853 + +Other changes: + +- FUSE: versions view: linear numbering by archive time +- split up interval parsing from filtering for --keep-within, #2610 +- add a basic .editorconfig, #2734 +- use archive creation time as mtime for FUSE mount, #2834 +- upgrade FUSE for macOS (osxfuse) from 3.5.8 to 3.6.3, #2706 +- hashindex: speed up by replacing modulo with "if" to check for wraparound +- coala checker / pylint: fixed requirements and .coafile, more ignores +- borg upgrade: name backup directories as 'before-upgrade', #2811 +- add .mailmap +- some minor changes suggested by lgtm.com +- docs: + + - better explanation of the --ignore-inode option relevance, #2800 + - fix openSUSE command and add openSUSE section + - simplify ssh authorized_keys file using "restrict", add legacy note, #2121 + - mount: show usage of archive filters + - mount: add repository example, #2462 + - info: update and add examples, #2765 + - prune: include example + - improved style / formatting + - improved/fixed segments_per_dir docs + - recreate: fix wrong "remove unwanted files" example + - reference list of status chars in borg recreate --filter description + - update source-install docs about doc build dependencies, #2795 + - cleanup installation docs + - file system requirements, update segs per dir + - fix checkpoints/parts reference in FAQ, #2859 +- code: + + - hashindex: don't pass side effect into macro + - crypto low_level: don't mutate local bytes() + - use dash_open function to open file or "-" for stdin/stdout + - archiver: argparse cleanup / refactoring + - shellpattern: add match_end arg +- tests: added some additional unit tests, some fixes, #2700 #2710 +- vagrant: fix setup of cygwin, add Debian 9 "stretch" +- travis: don't perform full travis build on docs-only changes, #2531 + + +Version 1.1.0b6 (2017-06-18) +---------------------------- + +Compatibility notes: + +- 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 "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 + 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. + +- 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: + +- 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, 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 +- 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 +- 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: + +- 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 + - 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 +- 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: + +- 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 +- 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 + - 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 + - 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: + + - 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 + - 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: + + - 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) +---------------------------- + +Compatibility notes: + +- BORG_HOSTNAME_IS_UNIQUE is now on by default. +- removed --compression-from feature +- recreate: add --recompress flag, unify --always-recompress and + --recompress + +Fixes: + +- 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, #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: + +- 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 +- 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 + - 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") + - 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: + +- 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) +---------------------------- + +Compatibility notes: + +- 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 + --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 and .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.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: + +- 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 +- posix platform module: only build / import on non-win32 platforms, #2041 + +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 #1253 +- 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) +---------------------------- + +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. +- 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) +- 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) +---------------------------- + +New features: + +- new commands: + + - 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), + 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 + - 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 +- 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 + - 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 +- 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 +- 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 fewer 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: + + - Group options + - Nicer list of options in Sphinx + - Deduplicate 'Common options' (including --help) + - 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: + + - vagrant: add ubuntu/xenial 64bit - this box has still some issues + - ChunkBuffer: add test for leaving partial chunk in buffer, fixes #945 + + +Version 1.0.13 (2019-02-15) +--------------------------- + +Please note: this is very likely the last 1.0.x release, please upgrade to 1.1.x. + +Bug fixes: + +- security fix: configure FUSE with "default_permissions", #3903. + "default_permissions" is now enforced by borg by default to let the + kernel check uid/gid/mode based permissions. + "ignore_permissions" can be given to not enforce "default_permissions". +- xattrs: fix borg exception handling on ENOSPC error, #3808. + +New features: + +- Read a passphrase from a file descriptor specified in the + BORG_PASSPHRASE_FD environment variable. + +Other changes: + +- acl platform code: fix acl set return type +- xattr: + + - add linux {list,get,set}xattr ctypes prototypes + - fix darwin flistxattr ctypes prototype +- testing / travis-ci: + + - fix the homebrew 1.9 issues on travis-ci, #4254 + - travis OS X: use xcode 8.3 (not broken) + - tox.ini: lock requirements + - unbreak 1.0-maint on travis, fixes #4123 +- vagrant: + + - misc. fixes + - FUSE for macOS: upgrade 3.7.1 to 3.8.3 + - Python: upgrade 3.5.5 to 3.5.6 +- docs: + + - Update installation instructions for macOS + - update release workflow using twine (docs, scripts), #4213 + +Version 1.0.12 (2018-04-08) +--------------------------- + +Bug fixes: + +- repository: cleanup/write: invalidate cached FDs, tests +- serve: fix exitcode, #2910 +- extract: set bsdflags last (include immutable flag), #3263 +- create --timestamp: set start time, #2957 +- create: show excluded dir with "x" for tagged dirs / caches, #3189 +- migrate locks to child PID when daemonize is used +- Buffer: fix wrong thread-local storage use, #2951 +- fix detection of non-local path, #3108 +- fix LDLP restoration for subprocesses, #3077 +- fix subprocess environments (xattr module's fakeroot version check, + borg umount, BORG_PASSCOMMAND), #3050 +- remote: deal with partial lines, #2637 +- get rid of datetime.isoformat, use safe parse_timestamp to parse + timestamps, #2994 +- build: do .h file content checks in binary mode, fixes build issue for + non-ascii header files on pure-ascii locale platforms, #3544 #3639 +- remove platform.uname() call which caused library mismatch issues, #3732 +- add exception handler around deprecated platform.linux_distribution() call + +Other changes: + +- require msgpack-python >= 0.4.6 and < 0.5.0, see #3753 +- add parens for C preprocessor macro argument usages (did not cause + malfunction) +- ignore corrupt files cache, #2939 +- replace "modulo" with "if" to check for wraparound in hashmap +- keymanager: don't depend on optional readline module, #2980 +- exclude broken pytest 3.3.0 release +- exclude broken Cython 0.27(.0) release, #3066 +- flake8: add some ignores +- docs: + + - create: document exclusion through nodump + - document good and problematic option placements, fix examples, #3356 + - update docs about hardlinked symlinks limitation + - faq: we do not implement futile attempts of ETA / progress displays + - simplified rate limiting wrapper in FAQ + - twitter account @borgbackup, #2948 + - add note about metadata dedup and --no[ac]time, #2518 + - change-passphrase only changes the passphrase, #2990 + - clarify encrypted key format for borg key export, #3296 + - document sshfs rename workaround, #3315 + - update release checklist about security fixes + - docs about how to verify a signed release, #3634 + - chunk seed is generated per /repository/ +- vagrant: + + - use FUSE for macOS 3.7.1 to build the macOS binary + - use python 3.5.5 to build the binaries + - add exe location to PATH when we build an exe + - use https pypi url for wheezy + - netbsd: bash is already installed + - netbsd: fix netbsd version in PKG_PATH + - use self-made FreeBSD 10.3 box, #3022 + - backport fs_init (including related updates) from 1.1 + - the boxcutter wheezy boxes are 404, use local ones +- travis: + + - don't perform full Travis build on docs-only changes, #2531 + - only short-circuit docs-only changes for pull requests + + +Version 1.0.11 (2017-07-21) +--------------------------- + +Bug fixes: + +- use limited unpacker for outer key (security precaution), #2174 +- fix paperkey import bug + +Other changes: + +- change --checkpoint-interval default from 600s to 1800s, #2841. + this improves efficiency for big repositories a lot. +- docs: fix OpenSUSE command and add OpenSUSE section +- tests: add tests for split_lstring and paperkey +- vagrant: + + - fix openbsd shell + - backport cpu/ram setup from master + - add stretch64 VM + +Version 1.0.11rc1 (2017-06-27) +------------------------------ + +Bug fixes: + +- performance: rebuild hashtable if we have too few empty buckets, #2246. + this fixes some sporadic, but severe performance breakdowns. +- Archive: allocate zeros when needed, #2308 + fixes huge memory usage of mount (8 MiB × number of archives) +- IPv6 address support + also: Location: more informative exception when parsing fails +- borg single-file binary: use pyinstaller v3.2.1, #2396 + this fixes that the prelink cronjob on some distros kills the + borg binary by stripping away parts of it. +- extract: + + - warning for unextracted big extended attributes, #2258 + - also create parent dir for device files, if needed. + - don't write to disk with --stdout, #2645 +- archive check: detect and fix missing all-zero replacement chunks, #2180 +- fix (de)compression exceptions, #2224 #2221 +- files cache: update inode number, #2226 +- borg rpc: use limited msgpack.Unpacker (security precaution), #2139 +- Manifest: use limited msgpack.Unpacker (security precaution), #2175 +- Location: accept //servername/share/path +- fix ChunkIndex.__contains__ assertion for big-endian archs (harmless) +- create: handle BackupOSError on a per-path level in one spot +- fix error msg, there is no --keep-last in borg 1.0.x, #2282 +- clamp (nano)second values to unproblematic range, #2304 +- fuse / borg mount: + + - fix st_blocks to be an integer (not float) value + - fix negative uid/gid crash (they could come into archives e.g. when + backing up external drives under cygwin), #2674 + - fix crash if empty (None) xattr is read + - do pre-mount checks before opening repository + - check llfuse is installed before asking for passphrase +- borg rename: expand placeholders, #2386 +- borg serve: fix forced command lines containing BORG_* env vars +- fix error msg, it is --keep-within, not --within +- fix borg key/debug/benchmark crashing without subcommand, #2240 +- chunker: fix invalid use of types, don't do uint32_t >> 32 +- document follow_symlinks requirements, check libc, #2507 + +New features: + +- added BORG_PASSCOMMAND environment variable, #2573 +- add minimal version of in repository mandatory feature flags, #2134 + + 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. +- borg delete --force --force to delete severely corrupted archives, #1975 + +Other changes: + +- embrace y2038 issue to support 32bit platforms +- be more clear that this is a "beyond repair" case, #2427 +- key file names: limit to 100 characters and remove colons from host name +- upgrade FUSE for macOS to 3.5.8, #2346 +- split up parsing and filtering for --keep-within, better error message, #2610 +- docs: + + - fix caskroom link, #2299 + - address SSH batch mode, #2202 #2270 + - improve remote-path description + - document snapshot usage, #2178 + - document relative path usage, #1868 + - one link per distro in the installation page + - development: new branching model in git repository + - kill api page + - added FAQ section about backing up root partition + - add bountysource badge, #2558 + - create empty docs.txt reequirements, #2694 + - README: how to help the project + - note -v/--verbose requirement on affected options, #2542 + - document borg init behaviour via append-only borg serve, #2440 + - be clear about what buzhash is used for (chunking) 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, #2390 + - add hint about chunker params to borg upgrade docs, #2421 + - clarify borg upgrade docs, #2436 + - quickstart: delete problematic BORG_PASSPHRASE use, #2623 + - faq: specify "using inline shell scripts" + - document pattern denial of service, #2624 +- tests: + + - remove attic dependency of the tests, #2505 + - travis: + + - enhance travis setuptools_scm situation + - install fakeroot for Linux + - add test for borg delete --force + - enable remote tests on cygwin (the cygwin issue that caused these tests + to break was fixed in cygwin at least since cygwin 2.8, maybe even since + 2.7.0). + - remove skipping the noatime tests on GNU/Hurd, #2710 + - fix borg import issue, add comment, #2718 + - include attic.tar.gz when installing the package + also: add include_package_data=True + +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 +- setup.py: build_api: sort file list for determinism + + +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). +- 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. +- 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. +- 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 + - 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 + - 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 + - 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) +-------------------------- + +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: + + - rebuild manifest if it's corrupted + - 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: + +- 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 + - get back pytest's pretty assertion failures, #1938 +- 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) +----------------------------- + +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) +-------------------------- + +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 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. + + 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) +----------------------------- + +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, #1622 +- fixed exception borg serve raised when connection was closed before repository + was opened. 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 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 +- pyinstaller: use a spec file to build borg.exe binary, exclude osxfuse dylib + on Mac OS X (avoids mismatch lib <-> driver), #1619 + +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. +- add "borg debug-refcount-obj" to determine a repo objects' referrer counts, + #1352 + +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. +- 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, #1700 +- vagrant (testing/build VMs): + + - 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 +- 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 + - 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) +-------------------------- + +Security fixes: + +- 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 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 + 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 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). + +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) +----------------------------- + +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". +- 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 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 + 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) +----------------------------- + +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 unnecessary 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) +-------------------------- + +Bug fixes: + +- Linux: handle multiple LD_PRELOAD entries correctly, #1314, #1111 +- Fix crash with unclear message if the libc is not found, #1314, #1111 + +Other changes: + +- tests: + + - Fixed O_NOATIME tests for Solaris and GNU Hurd, #1315 + - Fixed sparse file tests for (file) systems not supporting it, #1310 +- docs: + + - Fixed syntax highlighting, #1313 + - misc docs: added data processing overview picture + + +Version 1.0.6rc1 (2016-07-10) +----------------------------- + +New features: + +- borg check --repair: heal damaged files if missing chunks re-appear (e.g. if + the previously missing chunk was added again in a later backup archive), + #148. (*) Also improved logging. + +Bug fixes: + +- sync_dir: silence fsync() failing with EINVAL, #1287 + Some network filesystems (like smbfs) don't support this and we use this in + repository code. +- borg mount (FUSE): + + - fix directories being shadowed when contained paths were also specified, + #1295 + - raise I/O Error (EIO) on damaged files (unless -o allow_damaged_files is + used), #1302. (*) +- borg extract: warn if a damaged file is extracted, #1299. (*) +- Added some missing return code checks (ChunkIndex._add, hashindex_resize). +- borg check: fix/optimize initial hash table size, avoids resize of the table. + +Other changes: + +- tests: + + - add more FUSE tests, #1284 + - deduplicate FUSE (u)mount code + - fix borg binary test issues, #862 +- docs: + + - changelog: added release dates to older borg releases + - fix some sphinx (docs generator) warnings, #881 + +Notes: + +(*) Some features depend on information (chunks_healthy list) added to item +metadata when a file with missing chunks was "repaired" using all-zero +replacement chunks. The chunks_healthy list is generated since borg 1.0.4, +thus borg can't recognize such "repaired" (but content-damaged) files if the +repair was done with an older borg version. + + +Version 1.0.5 (2016-07-07) +-------------------------- + +Bug fixes: + +- borg mount: fix FUSE crash in xattr code on Linux introduced in 1.0.4, #1282 + +Other changes: + +- backport some FAQ entries from master branch +- add release helper scripts +- Vagrantfile: + + - centos6: no FUSE, don't build binary + - add xz for redhat-like dists + + +Version 1.0.4 (2016-07-07) +-------------------------- + +New features: + +- borg serve --append-only, #1168 + This was included because it was a simple change (append-only functionality + was already present via repository config file) and makes better security now + practically usable. +- BORG_REMOTE_PATH environment variable, #1258 + This was included because it was a simple change (--remote-path cli option + was already present) and makes borg much easier to use if you need it. +- Repository: cleanup incomplete transaction on "no space left" condition. + In many cases, this can avoid a 100% full repo filesystem (which is very + problematic as borg always needs free space - even to delete archives). + +Bug fixes: + +- Fix wrong handling and reporting of OSErrors in borg create, #1138. + This was a serious issue: in the context of "borg create", errors like + repository I/O errors (e.g. disk I/O errors, ssh repo connection errors) + were handled badly and did not lead to a crash (which would be good for this + case, because the repo transaction would be incomplete and trigger a + transaction rollback to clean up). + Now, error handling for source files is cleanly separated from every other + error handling, so only problematic input files are logged and skipped. +- Implement fail-safe error handling for borg extract. + Note that this isn't nearly as critical as the borg create error handling + bug, since nothing is written to the repo. So this was "merely" misleading + error reporting. +- Add missing error handler in directory attr restore loop. +- repo: make sure write data hits disk before the commit tag (#1236) and also + sync the containing directory. +- FUSE: getxattr fail must use errno.ENOATTR, #1126 + (fixes Mac OS X Finder malfunction: "zero bytes" file length, access denied) +- borg check --repair: do not lose information about the good/original chunks. + If we do not lose the original chunk IDs list when "repairing" a file + (replacing missing chunks with all-zero chunks), we have a chance to "heal" + the file back into its original state later, in case the chunks re-appear + (e.g. in a fresh backup). Healing is not implemented yet, see #148. +- fixes for --read-special mode: + + - ignore known files cache, #1241 + - fake regular file mode, #1214 + - improve symlinks handling, #1215 +- remove passphrase from subprocess environment, #1105 +- Ignore empty index file (will trigger index rebuild), #1195 +- add missing placeholder support for --prefix, #1027 +- improve exception handling for placeholder replacement +- catch and format exceptions in arg parsing +- helpers: fix "undefined name 'e'" in exception handler +- better error handling for missing repo manifest, #1043 +- borg delete: + + - make it possible to delete a repo without manifest + - borg delete --forced allows one to delete corrupted archives, #1139 +- borg check: + + - make borg check work for empty repo + - fix resync and msgpacked item qualifier, #1135 + - rebuild_manifest: fix crash if 'name' or 'time' key were missing. + - better validation of item metadata dicts, #1130 + - better validation of archive metadata dicts +- close the repo on exit - even if rollback did not work, #1197. + This is rather cosmetic, it avoids repo closing in the destructor. + +- tests: + + - fix sparse file test, #1170 + - flake8: ignore new F405, #1185 + - catch "invalid argument" on cygwin, #257 + - fix sparseness assertion in test prep, #1264 + +Other changes: + +- make borg build/work on OpenSSL 1.0 and 1.1, #1187 +- docs / help: + + - fix / clarify prune help, #1143 + - fix "patterns" help formatting + - add missing docs / help about placeholders + - resources: rename atticmatic to borgmatic + - document sshd settings, #545 + - more details about checkpoints, add split trick, #1171 + - support docs: add freenode web chat link, #1175 + - add prune visualization / example, #723 + - add note that Fnmatch is default, #1247 + - make clear that lzma levels > 6 are a waste of cpu cycles + - add a "do not edit" note to auto-generated files, #1250 + - update cygwin installation docs +- repository interoperability with borg master (1.1dev) branch: + + - borg check: read item metadata keys from manifest, #1147 + - read v2 hints files, #1235 + - fix hints file "unknown version" error handling bug +- tests: add tests for format_line +- llfuse: update version requirement for freebsd +- Vagrantfile: + + - use openbsd 5.9, #716 + - do not install llfuse on netbsd (broken) + - update OSXfuse to version 3.3.3 + - use Python 3.5.2 to build the binaries +- glibc compatibility checker: scripts/glibc_check.py +- add .eggs to .gitignore + + +Version 1.0.3 (2016-05-20) +-------------------------- + +Bug fixes: + +- prune: avoid that checkpoints are kept and completed archives are deleted in + a prune run), #997 +- prune: fix commandline argument validation - some valid command lines were + considered invalid (annoying, but harmless), #942 +- fix capabilities extraction on Linux (set xattrs last, after chown()), #1069 +- repository: fix commit tags being seen in data +- when probing key files, do binary reads. avoids crash when non-borg binary + files are located in borg's key files directory. +- handle SIGTERM and make a clean exit - avoids orphan lock files. +- repository cache: don't cache large objects (avoid using lots of temp. disk + space), #1063 + +Other changes: + +- Vagrantfile: OS X: update osxfuse / install lzma package, #933 +- setup.py: add check for platform_darwin.c +- setup.py: on freebsd, use a llfuse release that builds ok +- docs / help: + + - update readthedocs URLs, #991 + - add missing docs for "borg break-lock", #992 + - borg create help: add some words to about the archive name + - borg create help: document format tags, #894 + + +Version 1.0.2 (2016-04-16) +-------------------------- + +Bug fixes: + +- fix malfunction and potential corruption on (nowadays rather rare) big-endian + architectures or bi-endian archs in (rare) BE mode. #886, #889 + + cache resync / index merge was malfunctioning due to this, potentially + leading to data loss. borg info had cosmetic issues (displayed wrong values). + + note: all (widespread) little-endian archs (like x86/x64) or bi-endian archs + in (widespread) LE mode (like ARMEL, MIPSEL, ...) were NOT affected. +- 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 occurred. +- borg delete: fix --cache-only for broken caches, #874 + + Makes --cache-only idempotent: it won't fail if the cache is already deleted. +- fixed borg create --one-file-system erroneously traversing into other + filesystems (if starting fs device number was 0), #873 +- workaround a bug in Linux fadvise FADV_DONTNEED, #907 + +Other changes: + +- better test coverage for hashindex, incl. overflow testing, checking correct + computations so endianness issues would be discovered. +- reproducible doc for ProgressIndicator*, make the build reproducible. +- use latest llfuse for vagrant machines +- docs: + + - use /path/to/repo in examples, fixes #901 + - fix confusing usage of "repo" as archive name (use "arch") + + +Version 1.0.1 (2016-04-08) +-------------------------- + +New features: + +Usually there are no new features in a bugfix release, but these were added +due to their high impact on security/safety/speed or because they are fixes +also: + +- append-only mode for repositories, #809, #36 (see docs) +- borg create: add --ignore-inode option to make borg detect unmodified files + even if your filesystem does not have stable inode numbers (like sshfs and + possibly CIFS). +- add options --warning, --error, --critical for missing log levels, #826. + it's not recommended to suppress warnings or errors, but the user may decide + this on his own. + note: --warning is not given to borg serve so a <= 1.0.0 borg will still + work as server (it is not needed as it is the default). + do not use --error or --critical when using a <= 1.0.0 borg server. + +Bug fixes: + +- fix silently skipping EIO, #748 +- add context manager for Repository (avoid orphan repository locks), #285 +- do not sleep for >60s while waiting for lock, #773 +- unpack file stats before passing to FUSE +- fix build on illumos +- don't try to backup doors or event ports (Solaris and derivatives) +- remove useless/misleading libc version display, #738 +- test suite: reset exit code of persistent archiver, #844 +- RemoteRepository: clean up pipe if remote open() fails +- Remote: don't print tracebacks for Error exceptions handled downstream, #792 +- if BORG_PASSPHRASE is present but wrong, don't prompt for password, but fail + instead, #791 +- ArchiveChecker: move "orphaned objects check skipped" to INFO log level, #826 +- fix capitalization, add ellipses, change log level to debug for 2 messages, + #798 + +Other changes: + +- update llfuse requirement, llfuse 1.0 works +- update OS / dist packages on build machines, #717 +- prefer showing --info over -v in usage help, #859 +- docs: + + - fix cygwin requirements (gcc-g++) + - document how to debug / file filesystem issues, #664 + - fix reproducible build of api docs + - RTD theme: CSS !important overwrite, #727 + - Document logo font. Recreate logo png. Remove GIMP logo file. + + +Version 1.0.0 (2016-03-05) +-------------------------- + +The major release number change (0.x -> 1.x) indicates bigger incompatible +changes, please read the compatibility notes, adapt / test your scripts and +check your backup logs. + +Compatibility notes: + +- drop support for python 3.2 and 3.3, require 3.4 or 3.5, #221 #65 #490 + note: we provide binaries that include python 3.5.1 and everything else + needed. they are an option in case you are stuck with < 3.4 otherwise. +- change encryption to be on by default (using "repokey" mode) +- moved keyfile keys from ~/.borg/keys to ~/.config/borg/keys, + you can either move them manually or run "borg upgrade " +- remove support for --encryption=passphrase, + use borg migrate-to-repokey to switch to repokey mode, #97 +- remove deprecated --compression , + use --compression zlib, instead + in case of 0, you could also use --compression none +- remove deprecated --hourly/daily/weekly/monthly/yearly + use --keep-hourly/daily/weekly/monthly/yearly instead +- remove deprecated --do-not-cross-mountpoints, + use --one-file-system instead +- disambiguate -p option, #563: + + - -p now is same as --progress + - -P now is same as --prefix +- remove deprecated "borg verify", + use "borg extract --dry-run" instead +- cleanup environment variable semantics, #355 + the environment variables used to be "yes sayers" when set, this was + conceptually generalized to "automatic answerers" and they just give their + value as answer (as if you typed in that value when being asked). + See the "usage" / "Environment Variables" section of the docs for details. +- change the builtin default for --chunker-params, create 2MiB chunks, #343 + --chunker-params new default: 19,23,21,4095 - old default: 10,23,16,4095 + + one of the biggest issues with borg < 1.0 (and also attic) was that it had a + default target chunk size of 64kiB, thus it created a lot of chunks and thus + also a huge chunk management overhead (high RAM and disk usage). + + please note that the new default won't change the chunks that you already + have in your repository. the new big chunks do not deduplicate with the old + small chunks, so expect your repo to grow at least by the size of every + changed file and in the worst case (e.g. if your files cache was lost / is + not used) by the size of every file (minus any compression you might use). + + in case you want to immediately see a much lower resource usage (RAM / disk) + for chunks management, it might be better to start with a new repo than + continuing in the existing repo (with an existing repo, you'ld have to wait + until all archives with small chunks got pruned to see a lower resource + usage). + + if you used the old --chunker-params default value (or if you did not use + --chunker-params option at all) and you'ld like to continue using small + chunks (and you accept the huge resource usage that comes with that), just + explicitly use borg create --chunker-params=10,23,16,4095. +- archive timestamps: the 'time' timestamp now refers to archive creation + start time (was: end time), the new 'time_end' timestamp refers to archive + creation end time. This might affect prune if your backups take rather long. + if you give a timestamp via cli this is stored into 'time', therefore it now + needs to mean archive creation start time. + +New features: + +- implement password roundtrip, #695 + +Bug fixes: + +- remote end does not need cache nor keys directories, do not create them, #701 +- added retry counter for passwords, #703 + +Other changes: + +- fix compiler warnings, #697 +- docs: + + - update README.rst to new changelog location in docs/changes.rst + - add Teemu to AUTHORS + - changes.rst: fix old chunker params, #698 + - FAQ: how to limit bandwidth + + +Version 1.0.0rc2 (2016-02-28) +----------------------------- + +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 +- add a simple archivename validator, #680 +- borg create --stats: show timestamps in localtime, use same labels/formatting + as borg info, #651 +- llfuse compatibility fixes (now compatible with: 0.40, 0.41, 0.42) + +Other changes: + +- it is now possible to use "pip install borgbackup[fuse]" to automatically + install the llfuse dependency using the correct version requirement + for it. you still need to care about having installed the FUSE / build + related OS package first, though, so that building llfuse can succeed. +- 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 + - 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 + - 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 + - update example for borg create -v --stats (use iso datetime format), #663 + - added example to BORG_RSH docs + - "connection closed by remote": add FAQ entry and point to issue #636 + + +Version 1.0.0rc1 (2016-02-07) +----------------------------- + +New features: + +- borg migrate-to-repokey ("passphrase" -> "repokey" encryption key mode) +- implement --short for borg list REPO, #611 +- implement --list for borg extract (consistency with borg create) +- borg serve: overwrite client's --restrict-to-path with ssh forced command's + option value (but keep everything else from the client commandline), #544 +- use $XDG_CONFIG_HOME/keys for keyfile keys (~/.config/borg/keys), #515 +- "borg upgrade" moves the keyfile keys to the new location +- display both archive creation start and end time in "borg info", #627 + + +Bug fixes: + +- normalize trailing slashes for the repository path, #606 +- Cache: fix exception handling in __init__, release lock, #610 + +Other changes: + +- suppress unneeded exception context (PEP 409), simpler tracebacks +- removed special code needed to deal with imperfections / incompatibilities / + missing stuff in py 3.2/3.3, simplify code that can be done simpler in 3.4 +- removed some version requirements that were kept on old versions because + newer did not support py 3.2 any more +- use some py 3.4+ stdlib code instead of own/openssl/pypi code: + + - use os.urandom instead of own cython openssl RAND_bytes wrapper, #493 + - use hashlib.pbkdf2_hmac from py stdlib instead of own openssl wrapper + - use hmac.compare_digest instead of == operator (constant time comparison) + - use stat.filemode instead of homegrown code + - use "mock" library from stdlib, #145 + - remove borg.support (with non-broken argparse copy), it is ok in 3.4+, #358 +- Vagrant: copy CHANGES.rst as symlink, #592 +- cosmetic code cleanups, add flake8 to tox/travis, #4 +- docs / help: + + - make "borg -h" output prettier, #591 + - slightly rephrase prune help + - add missing example for --list option of borg create + - quote exclude line that includes an asterisk to prevent shell expansion + - fix dead link to license + - delete Ubuntu Vivid, it is not supported anymore (EOL) + - OS X binary does not work for older OS X releases, #629 + - borg serve's special support for forced/original ssh commands, #544 + - misc. updates and fixes diff --git a/docs/index.rst b/docs/index.rst index 9675ed989..6fc3bb6a5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,6 +18,8 @@ Borg Documentation faq support changes + changes_1.x + changes_0.x internals development authors From 51cf85e627233feaae689b53950372dd8298b743 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 24 Jun 2022 01:19:19 +0200 Subject: [PATCH 071/160] build_usage / build_man / doc updates --- docs/faq.rst | 247 ++--------------- docs/installation.rst | 4 +- docs/internals/data-structures.rst | 7 +- docs/internals/frontends.rst | 4 +- docs/internals/security.rst | 117 +------- docs/man/borg-benchmark-cpu.1 | 14 +- docs/man/borg-benchmark-crud.1 | 11 +- docs/man/borg-benchmark.1 | 6 +- docs/man/borg-break-lock.1 | 14 +- docs/man/borg-check.1 | 27 +- docs/man/borg-common.1 | 35 +-- docs/man/borg-compact.1 | 21 +- docs/man/borg-compression.1 | 6 +- docs/man/borg-config.1 | 23 +- docs/man/borg-create.1 | 110 ++++---- docs/man/borg-delete.1 | 63 ++--- docs/man/borg-diff.1 | 57 +--- docs/man/borg-export-tar.1 | 16 +- docs/man/borg-extract.1 | 44 +-- docs/man/borg-import-tar.1 | 38 +-- docs/man/borg-info.1 | 82 ++---- docs/man/borg-key-change-algorithm.1 | 10 +- docs/man/borg-key-change-location.1 | 27 +- docs/man/borg-key-change-passphrase.1 | 21 +- docs/man/borg-key-export.1 | 14 +- docs/man/borg-key-import.1 | 12 +- docs/man/borg-key.1 | 6 +- docs/man/borg-list.1 | 136 ++------- docs/man/borg-mount.1 | 21 +- docs/man/borg-patterns.1 | 258 ++++++++++-------- docs/man/borg-placeholders.1 | 8 +- docs/man/borg-prune.1 | 48 ++-- docs/man/{borg-init.1 => borg-rcreate.1} | 86 +++--- docs/man/borg-rdelete.1 | 90 ++++++ docs/man/borg-recreate.1 | 57 ++-- docs/man/borg-rename.1 | 22 +- docs/man/borg-rinfo.1 | 83 ++++++ docs/man/borg-rlist.1 | 160 +++++++++++ docs/man/borg-serve.1 | 8 +- docs/man/borg-transfer.1 | 100 +++++++ docs/man/borg-umount.1 | 25 +- docs/man/borg-upgrade.1 | 24 +- docs/man/borg-with-lock.1 | 11 +- docs/man/borg.1 | 117 ++++---- docs/man/borgfs.1 | 23 +- docs/man_intro.rst | 4 +- docs/quickstart.rst | 51 ++-- docs/quickstart_example.rst.inc | 53 ++-- docs/usage.rst | 20 +- docs/usage/benchmark_cpu.rst.inc | 1 + docs/usage/benchmark_crud.rst.inc | 24 +- docs/usage/break-lock.rst.inc | 18 +- docs/usage/check.rst.inc | 11 +- docs/usage/common-options.rst.inc | 35 +-- docs/usage/compact.rst | 7 +- docs/usage/compact.rst.inc | 8 +- docs/usage/config.rst | 6 +- docs/usage/config.rst.inc | 6 +- docs/usage/create.rst | 48 ++-- docs/usage/create.rst.inc | 12 +- docs/usage/delete.rst | 18 +- docs/usage/delete.rst.inc | 26 +- docs/usage/diff.rst | 33 +-- docs/usage/diff.rst.inc | 12 +- docs/usage/export-tar.rst.inc | 8 +- docs/usage/extract.rst | 12 +- docs/usage/extract.rst.inc | 8 +- docs/usage/general.rst | 4 +- docs/usage/general/environment.rst.inc | 7 +- .../general/positional-arguments.rst.inc | 8 +- .../general/repository-locations.rst.inc | 15 +- docs/usage/general/repository-urls.rst.inc | 24 +- docs/usage/help.rst.inc | 233 ++++++++-------- docs/usage/import-tar.rst.inc | 8 +- docs/usage/info.rst | 64 +---- docs/usage/info.rst.inc | 10 +- docs/usage/init.rst | 22 -- docs/usage/key.rst | 10 +- docs/usage/key_change-algorithm.rst.inc | 24 +- docs/usage/key_change-location.rst.inc | 16 +- docs/usage/key_change-passphrase.rst.inc | 18 +- docs/usage/key_export.rst.inc | 36 ++- docs/usage/key_import.rst.inc | 32 +-- docs/usage/list.rst | 17 +- docs/usage/list.rst.inc | 134 +++------ docs/usage/mount.rst | 19 +- docs/usage/mount.rst.inc | 6 +- docs/usage/notes.rst | 16 +- docs/usage/prune.rst | 10 +- docs/usage/prune.rst.inc | 8 +- docs/usage/rcreate.rst | 25 ++ docs/usage/{init.rst.inc => rcreate.rst.inc} | 31 +-- docs/usage/rdelete.rst | 13 + docs/usage/rdelete.rst.inc | 66 +++++ docs/usage/recreate.rst | 19 +- docs/usage/recreate.rst.inc | 125 +++++---- docs/usage/rename.rst | 8 +- docs/usage/rename.rst.inc | 30 +- docs/usage/rinfo.rst | 16 ++ docs/usage/rinfo.rst.inc | 56 ++++ docs/usage/rlist.rst | 13 + docs/usage/rlist.rst.inc | 125 +++++++++ docs/usage/tar.rst | 18 +- docs/usage/transfer.rst | 1 + docs/usage/transfer.rst.inc | 87 ++++++ docs/usage/upgrade.rst.inc | 8 +- docs/usage/with-lock.rst.inc | 28 +- src/borg/archiver.py | 73 ++--- 108 files changed, 2134 insertions(+), 2083 deletions(-) rename docs/man/{borg-init.1 => borg-rcreate.1} (81%) create mode 100644 docs/man/borg-rdelete.1 create mode 100644 docs/man/borg-rinfo.1 create mode 100644 docs/man/borg-rlist.1 create mode 100644 docs/man/borg-transfer.1 delete mode 100644 docs/usage/init.rst create mode 100644 docs/usage/rcreate.rst rename docs/usage/{init.rst.inc => rcreate.rst.inc} (91%) create mode 100644 docs/usage/rdelete.rst create mode 100644 docs/usage/rdelete.rst.inc create mode 100644 docs/usage/rinfo.rst create mode 100644 docs/usage/rinfo.rst.inc create mode 100644 docs/usage/rlist.rst create mode 100644 docs/usage/rlist.rst.inc create mode 100644 docs/usage/transfer.rst create mode 100644 docs/usage/transfer.rst.inc diff --git a/docs/faq.rst b/docs/faq.rst index b91c0ea59..9566fdec7 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -27,13 +27,7 @@ which is slower. Can I backup from multiple servers into a single repository? ------------------------------------------------------------ -Yes, this is *possible* from the technical standpoint, but it is -*not recommended* from the security perspective. BorgBackup is -built upon a defined :ref:`attack_model` that cannot provide its -guarantees for multiple clients using the same repository. See -:ref:`borg_security_critique` for a detailed explanation. - -Also, in order for the deduplication used by Borg to work, it +In order for the deduplication used by Borg to work, it needs to keep a local cache containing checksums of all file chunks already stored in the repository. This cache is stored in ``~/.cache/borg/``. If Borg detects that a repository has been @@ -49,7 +43,7 @@ Can I back up to multiple, swapped backup targets? -------------------------------------------------- It is possible to swap your backup disks if each backup medium is assigned its -own repository by creating a new one with :ref:`borg_init`. +own repository by creating a new one with :ref:`borg_rcreate`. Can I copy or synchronize my repo to another location? ------------------------------------------------------ @@ -57,8 +51,8 @@ Can I copy or synchronize my repo to another location? If you want to have redundant backup repositories (preferably at separate locations), the recommended way to do that is like this: -- ``borg init repo1`` -- ``borg init repo2`` +- ``borg rcreate repo1`` +- ``borg rcreate repo2`` - client machine ---borg create---> repo1 - client machine ---borg create---> repo2 @@ -94,10 +88,6 @@ Also, you must not run borg against multiple instances of the same repo think they are identical and e.g. use the same local cache for them (which is an issue if they happen to be not the same). See :issue:`4272` for an example. -- Encryption security issues if you would update repo and copy-of-repo - independently, due to AES counter reuse (when using legacy encryption modes). - -See also: :ref:`faq_corrupt_repo` "this is either an attack or unsafe" warning -------------------------------------------- @@ -118,9 +108,9 @@ you could delete the manifest-timestamp and the local cache: :: - borg config repo id # shows the REPO_ID + borg config id # shows the REPO_ID rm ~/.config/borg/security/REPO_ID/manifest-timestamp - borg delete --cache-only REPO + borg rdelete --cache-only This is an unsafe and unsupported way to use borg, you have been warned. @@ -199,11 +189,6 @@ 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. -Note that checkpoints inside files are created only since version 1.1, make -sure you have an up-to-date version of borgbackup if you want to continue -instead of retransferring a huge file. In some cases, there is only an outdated -version shipped with your distribution (e.g. Debian). See :ref:`installation`. - How can I backup huge file(s) over a unstable connection? --------------------------------------------------------- @@ -241,33 +226,9 @@ then use ``tar`` to perform the comparison: :: - borg export-tar /path/to/repo::archive-name - | tar --compare -f - -C /path/to/compare/to + borg export-tar archive-name - | tar --compare -f - -C /path/to/compare/to -.. _faq_corrupt_repo: - -My repository is corrupt, how can I restore from an older copy of it? ---------------------------------------------------------------------- - -Note: this is only required for repos using legacy encryption modes. - -If your repositories are encrypted and have the same ID, the recommended method -is to delete the corrupted repository, but keep its security info, and then copy -the working repository to the same location: - -:: - - borg delete --keep-security-info /path/to/repo - rsync -aH /path/to/repo-working/ /path/to/repo # Note the trailing slash. - -A plain delete command would remove the security info in -``~/.config/borg/security``, including the nonce value. In BorgBackup -:ref:`security_encryption` is AES-CTR, where the nonce is a counter. When the -working repo was used later for creating new archives, Borg would re-use nonce -values due to starting from a lower counter value given by the older copy of the -repository. To prevent this, the ``keep-security-info`` option is applied so -that the client-side nonce counter is kept. - Can Borg add redundancy to the backup data to deal with hardware malfunction? ----------------------------------------------------------------------------- @@ -296,7 +257,7 @@ 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, +Borg ships with default settings suitable for SMR drives, and has been successfully tested on *Seagate Archive v2* drives using the ext4 file system. @@ -436,16 +397,16 @@ Say you want to prune ``/var/log`` faster than the rest of archive *names* and then implement different prune policies for different prefixes. For example, you could have a script that does:: - borg create --exclude var/log $REPOSITORY:main-$(date +%Y-%m-%d) / - borg create $REPOSITORY:logs-$(date +%Y-%m-%d) /var/log + borg create --exclude var/log main-$(date +%Y-%m-%d) / + borg create logs-$(date +%Y-%m-%d) /var/log Then you would have two different prune calls with different policies:: - borg prune --verbose --list -d 30 --prefix main- "$REPOSITORY" - borg prune --verbose --list -d 7 --prefix logs- "$REPOSITORY" + borg prune --verbose --list -d 30 --prefix main- + borg prune --verbose --list -d 7 --prefix logs- -This will keep 7 days of logs and 30 days of everything else. Borg 1.1 -also supports the ``--glob-archives`` parameter. +This will keep 7 days of logs and 30 days of everything else. +Borg also supports the ``--glob-archives`` parameter. How do I remove files from an existing backup? ---------------------------------------------- @@ -476,37 +437,6 @@ to change them. Security ######## -.. _borg_security_critique: - -Isn't BorgBackup's legacy AES-CTR-based crypto broken? ------------------------------------------------------- - -Note: in borg 1.3 new AEAD cipher based modes with session keys were added, -solving the issues of the legacy modes. - -If a nonce (counter) value is reused, AES-CTR mode crypto is broken. - -To exploit the AES counter management issue, an attacker would need to have -access to the borg repository. - -By tampering with the repo, the attacker could bring the repo into a state so -that it reports a lower "highest used counter value" than the one that actually -was used. The client would usually notice that, because it rather trusts the -clientside stored "highest used counter value" than trusting the server. - -But there are situations, where this is simply not possible: - -- If clients A and B used the repo, the client A can only know its own highest - CTR value, but not the one produced by B. That is only known to (B and) the - server (the repo) and thus the client A needs to trust the server about the - value produced by B in that situation. You can't do much about this except - not having multiple clients per repo. - -- Even if there is only one client, if client-side information is completely - lost (e.g. due to disk defect), the client also needs to trust the value from - server side. You can avoid this by not continuing to write to the repository - after you have lost clientside borg information. - .. _home_config_borg: How important is the $HOME/.config/borg directory? @@ -583,7 +513,7 @@ Using ``BORG_PASSCOMMAND`` with a properly permissioned file Using keyfile-based encryption with a blank passphrase It is possible to encrypt your repository in ``keyfile`` mode instead of the default ``repokey`` mode and use a blank passphrase for the key file (simply press Enter twice - when ``borg init`` asks for the password). See :ref:`encrypted_repos` + when ``borg rcreate`` asks for the password). See :ref:`encrypted_repos` for more details. Using ``BORG_PASSCOMMAND`` with macOS Keychain @@ -717,34 +647,6 @@ Send a private email to the :ref:`security contact ` if you think you have discovered a security issue. Please disclose security issues responsibly. -How important are the nonce files? ------------------------------------- - -This only applies to repositories using legacy encryption modes. - -Borg uses :ref:`AES-CTR encryption `. An -essential part of AES-CTR is a sequential counter that must **never** -repeat. If the same value of the counter is used twice in the same repository, -an attacker can decrypt the data. The counter is stored in the home directory -of each user ($HOME/.config/borg/security/$REPO_ID/nonce) as well as -in the repository (/path/to/repo/nonce). When creating a new archive borg uses -the highest of the two values. The value of the counter in the repository may be -higher than your local value if another user has created an archive more recently -than you did. - -Since the nonce is not necessary to read the data that is already encrypted, -``borg info``, ``borg list``, ``borg extract`` and ``borg mount`` should work -just fine without it. - -If the nonce file stored in the repo is lost, but you still have your local copy, -borg will recreate the repository nonce file the next time you run ``borg create``. -This should be safe for repositories that are only used from one user account -on one machine. - -For repositories that are used by multiple users and/or from multiple machines -it is safest to avoid running *any* commands that modify the repository after -the nonce is deleted or if you suspect it may have been tampered with. See :ref:`attack_model`. - Common issues ############# @@ -789,9 +691,9 @@ How can I deal with my very unstable SSH connection? If you have issues with lost connections during long-running borg commands, you could try to work around: -- Make partial extracts like ``borg extract REPO PATTERN`` to do multiple +- Make partial extracts like ``borg extract PATTERN`` to do multiple smaller extraction runs that complete before your connection has issues. -- Try using ``borg mount REPO MOUNTPOINT`` and ``rsync -avH`` from +- Try using ``borg mount MOUNTPOINT`` and ``rsync -avH`` from ``MOUNTPOINT`` to your desired extraction directory. If the connection breaks down, just repeat that over and over again until rsync does not find anything to do any more. Due to the way borg mount works, this might be less efficient @@ -846,7 +748,7 @@ space for chunks.archive.d (see :issue:`235` for details): :: # this assumes you are working with the same user as the backup. - cd ~/.cache/borg/$(borg config /path/to/repo id) + cd ~/.cache/borg/$(borg config id) rm -rf chunks.archive.d ; touch chunks.archive.d This deletes all the cached archive chunk indexes and replaces the directory @@ -1151,7 +1053,7 @@ To achieve this, run ``borg create`` within the mountpoint/snapshot directory: # Example: Some file system mounted in /mnt/rootfs. cd /mnt/rootfs - borg create /path/to/repo::rootfs_backup . + borg create rootfs_backup . I am having troubles with some network/FUSE/special filesystem, why? @@ -1240,7 +1142,7 @@ This can happen for CIFS- or FUSE shares. If you are sure that your target volum will always have enough disk space, you can use the following workaround to disable checking for free disk space:: - borg config -- $REPO_LOCATION additional_free_space -2T + borg config -- additional_free_space -2T How do I rename a repository? ----------------------------- @@ -1279,112 +1181,3 @@ 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? -------------------------------- - -Borg was created in May 2015 in response to the difficulty of getting new -code or larger changes incorporated into Attic and establishing a bigger -developer community / more open development. - -More details can be found in `ticket 217 -`_ that led to the fork. - -Borg intends to be: - -* simple: - - * as simple as possible, but no simpler - * do the right thing by default, but offer options -* open: - - * welcome feature requests - * accept pull requests of good quality and coding style - * give feedback on PRs that can't be accepted "as is" - * discuss openly, don't work in the dark -* changing: - - * Borg is not compatible with Attic - * 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 - -Migrating from Attic -#################### - -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: - -* lots of attic issues fixed - (see `issue #5 `_), - including critical data corruption bugs and security issues. -* more open, faster paced development - (see `issue #1 `_) -* 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, zstd, 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 -* rename-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, macOS - -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). - -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. Get the repository ID with ``borg config /path/to/repo id``. - 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 - "borg key migrate-to-repokey" (only available in borg <= 1.2.x). Borg does not - support the "passphrase" mode in 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. - -Borg also offers zstd, zlib and lzma compression, choose wisely. - -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/installation.rst b/docs/installation.rst index 1ff657879..60b05e45d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -177,8 +177,8 @@ following dependencies first: * optionally, if you wish to mount an archive as a FUSE filesystem, you need a FUSE implementation for Python: - - Either pyfuse3_ (preferably, newer and maintained) or llfuse_ (older, - unmaintained now). See also the BORG_FUSE_IMPL env variable. + - Either pyfuse3_ (preferably, newer) or llfuse_ (older). + See also the BORG_FUSE_IMPL env variable. - See setup.py about the version requirements. If you have troubles finding the right package names, have a look at the diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 8de20761b..b494fde50 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -79,7 +79,7 @@ Normally the keys are computed like this:: key = id = id_hash(unencrypted_data) -The id_hash function depends on the :ref:`encryption mode `. +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. @@ -736,7 +736,6 @@ The chunks cache is a key -> value mapping and contains: - reference count - size - - encrypted/compressed size 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 @@ -754,9 +753,9 @@ Here is the estimated memory usage of Borg - it's complicated:: chunk_size ~= 2 ^ HASH_MASK_BITS (for buzhash chunker, BLOCK_SIZE for fixed chunker) chunk_count ~= total_file_size / chunk_size - repo_index_usage = chunk_count * 40 + repo_index_usage = chunk_count * 48 - chunks_cache_usage = chunk_count * 44 + chunks_cache_usage = chunk_count * 40 files_cache_usage = total_file_count * 240 + chunk_count * 80 diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index d0ebed48c..7ef96c788 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -208,7 +208,7 @@ Passphrase prompts should be handled differently. Use the environment variables 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 +When setting a new passphrase (:ref:`borg_rcreate`, :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*. @@ -252,7 +252,7 @@ last_modified The *encryption* key, if present, contains: mode - Textual encryption mode name (same as :ref:`borg_init` ``--encryption`` names) + Textual encryption mode name (same as :ref:`borg_rcreate` ``--encryption`` names) keyfile Path to the local key file used for access. Depending on *mode* this key may be absent. diff --git a/docs/internals/security.rst b/docs/internals/security.rst index 69f22a804..b7b247d0e 100644 --- a/docs/internals/security.rst +++ b/docs/internals/security.rst @@ -38,9 +38,6 @@ 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). -When the above attack model is extended to include multiple clients -independently updating the same repository, then Borg fails to provide -confidentiality (i.e. guarantees 3) and 4) do not apply any more). .. _security_structural_auth: @@ -78,7 +75,7 @@ the root node of a DAG through its edges, since the root node has no incoming ed 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`). +to the manifest (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 [#]_:: @@ -129,12 +126,12 @@ AEAD modes Modes: --encryption (repokey|keyfile)-[blake2-](aes-ocb|chacha20-poly1305) -Supported: borg 1.3+ +Supported: borg 2.0+ Encryption with these modes is based on AEAD ciphers (authenticated encryption with associated data) and session keys. -Depending on the chosen mode (see :ref:`borg_init`) different AEAD ciphers are used: +Depending on the chosen mode (see :ref:`borg_rcreate`) different AEAD ciphers are used: - AES-256-OCB - super fast, single-pass algorithm IF you have hw accelerated AES. - chacha20-poly1305 - very fast, purely software based AEAD cipher. @@ -201,106 +198,13 @@ Legacy modes Modes: --encryption (repokey|keyfile)-[blake2] -Supported: all borg versions, blake2 since 1.1 +Supported: borg < 2.0 -DEPRECATED. We strongly suggest you use the safer AEAD modes, see above. +These were the AES-CTR based modes in previous borg versions. -Encryption with these modes is 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. +borg 2.0 does not support creating new repos using these modes, +but ``borg transfer`` can still read such existing repos. -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: - -- Legacy encryption modes use 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. - - Both HMAC-SHA-256 and BLAKE2b have undergone extensive cryptanalysis - and have proven secure against known attacks. The known vulnerability - of SHA-256 against length extension attacks does not apply to HMAC-SHA-256. - - The authentication primitive should be chosen based upon SHA hardware support. - With SHA hardware support, hmac-sha256 is likely to be much faster. - If no hardware support is provided, Blake2b-256 will outperform hmac-sha256. - To find out if you have SHA hardware support, use:: - - $ borg benchmark cpu - - The output will include an evaluation of cryptographic hashes/MACs like:: - - Cryptographic hashes / MACs ==================================== - hmac-sha256 1GB 0.436s - blake2b-256 1GB 1.579s - - Based upon your output, choose the primitive that is faster (in the above - example, hmac-sha256 is much faster, which indicates SHA hardware support). - -- 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) ) ) - -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 Borg's functionality. .. _key_encryption: @@ -468,13 +372,12 @@ Compression and Encryption Combining encryption with compression can be insecure in some contexts (e.g. online protocols). -There was some discussion about this in `github issue #1040`_ and for Borg some developers +There was some discussion about this in :issue:`1040` and for Borg some developers concluded this is no problem at all, some concluded this is hard and extremely slow to exploit and thus no problem in practice. No matter what, there is always the option not to use compression if you are worried about this. -.. _github issue #1040: https://github.com/borgbackup/borg/issues/1040 Fingerprinting ============== @@ -490,7 +393,7 @@ output of the chunker. The sizes of these stored chunks are influenced by the compression, encryption and authentication. buzhash chunker -+++++++++++++++ +~~~~~~~~~~~~~~~ The buzhash chunker chunks according to the input data, the chunker's parameters and the secret chunker seed (which all influence the chunk boundary @@ -501,7 +404,7 @@ chunk (identical content / size as the original file), bigger files result in multiple chunks. fixed chunker -+++++++++++++ +~~~~~~~~~~~~~ This chunker yields fixed sized chunks, with optional support of a differently sized header chunk. The last chunk is not required to have the full block size diff --git a/docs/man/borg-benchmark-cpu.1 b/docs/man/borg-benchmark-cpu.1 index e6c51936b..9c3f4e0d4 100644 --- a/docs/man/borg-benchmark-cpu.1 +++ b/docs/man/borg-benchmark-cpu.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-BENCHMARK-CPU 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-benchmark-cpu \- Benchmark CPU bound operations. . .nr rst2man-indent-level 0 . @@ -30,6 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-BENCHMARK-CPU" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-benchmark-cpu \- Benchmark CPU bound operations. .SH SYNOPSIS .sp borg [common options] benchmark cpu [options] @@ -39,8 +39,12 @@ This command benchmarks misc. CPU bound borg operations. .sp It creates input data in memory, runs the operation and then displays throughput. To reduce outside influence on the timings, please make sure to run this with: -\- an otherwise as idle as possible machine -\- enough free memory so there will be no slow down due to paging activity +.INDENT 0.0 +.IP \(bu 2 +an otherwise as idle as possible machine +.IP \(bu 2 +enough free memory so there will be no slow down due to paging activity +.UNINDENT .SH OPTIONS .sp See \fIborg\-common(1)\fP for common options of Borg commands. diff --git a/docs/man/borg-benchmark-crud.1 b/docs/man/borg-benchmark-crud.1 index efa4df554..89cd546ec 100644 --- a/docs/man/borg-benchmark-crud.1 +++ b/docs/man/borg-benchmark-crud.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-BENCHMARK-CRUD 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-benchmark-crud \- Benchmark Create, Read, Update, Delete for archives. . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-BENCHMARK-CRUD" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-benchmark-crud \- Benchmark Create, Read, Update, Delete for archives. .SH SYNOPSIS .sp -borg [common options] benchmark crud [options] REPOSITORY PATH +borg [common options] benchmark crud [options] PATH .SH DESCRIPTION .sp This command benchmarks borg CRUD (create, read, update, delete) operations. @@ -88,9 +88,6 @@ See \fIborg\-common(1)\fP for common options of Borg commands. .SS arguments .INDENT 0.0 .TP -.B REPOSITORY -repository to use for benchmark (must exist) -.TP .B PATH path were to create benchmark input data .UNINDENT diff --git a/docs/man/borg-benchmark.1 b/docs/man/borg-benchmark.1 index 0cd5e90cb..c832a7925 100644 --- a/docs/man/borg-benchmark.1 +++ b/docs/man/borg-benchmark.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-BENCHMARK 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-benchmark \- benchmark command . .nr rst2man-indent-level 0 . @@ -30,6 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-BENCHMARK" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-benchmark \- benchmark command .SH SYNOPSIS .nf borg [common options] benchmark crud ... diff --git a/docs/man/borg-break-lock.1 b/docs/man/borg-break-lock.1 index bd01367c9..81c290601 100644 --- a/docs/man/borg-break-lock.1 +++ b/docs/man/borg-break-lock.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-BREAK-LOCK 1 "2022-04-14" "" "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 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-BREAK-LOCK" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-break-lock \- Break the repository lock (e.g. in case it was left by a dead borg. .SH SYNOPSIS .sp -borg [common options] break\-lock [options] [REPOSITORY] +borg [common options] break\-lock [options] .SH DESCRIPTION .sp This command breaks the repository and cache locks. @@ -41,12 +41,6 @@ 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 diff --git a/docs/man/borg-check.1 b/docs/man/borg-check.1 index 0dfbc27b1..9ec8e69f9 100644 --- a/docs/man/borg-check.1 +++ b/docs/man/borg-check.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CHECK 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-check \- Check repository consistency . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-CHECK" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-check \- Check repository consistency .SH SYNOPSIS .sp -borg [common options] check [options] [REPOSITORY_OR_ARCHIVE] +borg [common options] check [options] .SH DESCRIPTION .sp The check command verifies the consistency of a repository and the corresponding archives. @@ -126,28 +126,25 @@ 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 +.BI \-\-name \ NAME +specify the archive name +.TP +.B \-\-repository\-only only perform repository checks .TP -.B \-\-archives\-only +.B \-\-archives\-only only perform archives checks .TP -.B \-\-verify\-data +.B \-\-verify\-data perform cryptographic archive data integrity verification (conflicts with \fB\-\-repository\-only\fP) .TP -.B \-\-repair +.B \-\-repair attempt to repair any inconsistencies found .TP -.B \-\-save\-space +.B \-\-save\-space work slower, but using less space .TP .BI \-\-max\-duration \ SECONDS diff --git a/docs/man/borg-common.1 b/docs/man/borg-common.1 index 85b46e70f..5e960b713 100644 --- a/docs/man/borg-common.1 +++ b/docs/man/borg-common.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-COMMON 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-common \- Common options of Borg commands . .nr rst2man-indent-level 0 . @@ -30,49 +27,52 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-COMMON" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-common \- Common options of Borg commands .SH SYNOPSIS .INDENT 0.0 .TP -.B \-h\fP,\fB \-\-help +.B \-h\fP,\fB \-\-help show this help message and exit .TP -.B \-\-critical +.B \-\-critical work on log level CRITICAL .TP -.B \-\-error +.B \-\-error work on log level ERROR .TP -.B \-\-warning +.B \-\-warning work on log level WARNING (default) .TP -.B \-\-info\fP,\fB \-v\fP,\fB \-\-verbose +.B \-\-info\fP,\fB \-v\fP,\fB \-\-verbose work on log level INFO .TP -.B \-\-debug +.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 -.B \-p\fP,\fB \-\-progress +.B \-p\fP,\fB \-\-progress show progress information .TP -.B \-\-iec +.B \-\-iec format using IEC units (1KiB = 1024B) .TP -.B \-\-log\-json +.B \-\-log\-json Output one JSON object per log line instead of formatted text. .TP .BI \-\-lock\-wait \ SECONDS wait at most SECONDS for acquiring a repository/cache lock (default: 1). .TP -.B \-\-bypass\-lock +.B \-\-bypass\-lock Bypass locking mechanism .TP -.B \-\-show\-version +.B \-\-show\-version show/log the borg version .TP -.B \-\-show\-rc +.B \-\-show\-rc show/log the return code (rc) .TP .BI \-\-umask \ M @@ -93,7 +93,7 @@ deprecated, use \fB\-\-upload\-buffer\fP instead .BI \-\-upload\-buffer \ UPLOAD_BUFFER set network upload buffer size in MiB. (default: 0=no buffer) .TP -.B \-\-consider\-part\-files +.B \-\-consider\-part\-files treat part files like normal files (e.g. to list/extract them) .TP .BI \-\-debug\-profile \ FILE @@ -101,6 +101,9 @@ Write execution profile in Borg format into FILE. For local use a Python\-compat .TP .BI \-\-rsh \ RSH Use this command to connect to the \(aqborg serve\(aq process (default: \(aqssh\(aq) +.TP +.BI \-r \ REPO\fR,\fB \ \-\-repo \ REPO +repository to use .UNINDENT .SH SEE ALSO .sp diff --git a/docs/man/borg-compact.1 b/docs/man/borg-compact.1 index 79f8fc4e8..18d79ee76 100644 --- a/docs/man/borg-compact.1 +++ b/docs/man/borg-compact.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-COMPACT 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-compact \- compact segment files in the repository . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-COMPACT" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-compact \- compact segment files in the repository .SH SYNOPSIS .sp -borg [common options] compact [options] [REPOSITORY] +borg [common options] compact [options] .SH DESCRIPTION .sp This command frees repository space by compacting segments. @@ -60,16 +60,10 @@ See \fIseparate_compaction\fP in Additional Notes for more details. .SH OPTIONS .sp See \fIborg\-common(1)\fP for common options of Borg commands. -.SS arguments -.INDENT 0.0 -.TP -.B REPOSITORY -repository to compact -.UNINDENT .SS optional arguments .INDENT 0.0 .TP -.B \-\-cleanup\-commits +.B \-\-cleanup\-commits cleanup commit\-only 17\-byte segment files .TP .BI \-\-threshold \ PERCENT @@ -82,10 +76,7 @@ set minimum threshold for saved space in PERCENT (Default: 10) .nf .ft C # compact segments and free repo disk space -$ borg compact /path/to/repo - -# same as above plus clean up 17byte commit\-only segments -$ borg compact \-\-cleanup\-commits /path/to/repo +$ borg compact .ft P .fi .UNINDENT diff --git a/docs/man/borg-compression.1 b/docs/man/borg-compression.1 index a55a7dba7..ff348a7e3 100644 --- a/docs/man/borg-compression.1 +++ b/docs/man/borg-compression.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-COMPRESSION 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-compression \- Details regarding compression . .nr rst2man-indent-level 0 . @@ -30,6 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-COMPRESSION" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-compression \- Details regarding compression .SH DESCRIPTION .sp It is no problem to mix different compression methods in one repo, diff --git a/docs/man/borg-config.1 b/docs/man/borg-config.1 index 26ccffdd6..35cc426d9 100644 --- a/docs/man/borg-config.1 +++ b/docs/man/borg-config.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CONFIG 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-config \- get, set, and delete values in a repository or cache config file . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-CONFIG" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-config \- get, set, and delete values in a repository or cache config file .SH SYNOPSIS .sp -borg [common options] config [options] [REPOSITORY] [NAME] [VALUE] +borg [common options] config [options] [NAME] [VALUE] .SH DESCRIPTION .sp This command gets and sets options in a local repository or cache config file. @@ -53,9 +53,6 @@ See \fIborg\-common(1)\fP for common options of Borg commands. .SS arguments .INDENT 0.0 .TP -.B REPOSITORY -repository to configure -.TP .B NAME name of config key .TP @@ -65,13 +62,13 @@ new value for key .SS optional arguments .INDENT 0.0 .TP -.B \-c\fP,\fB \-\-cache +.B \-c\fP,\fB \-\-cache get and set values from the repo cache .TP -.B \-d\fP,\fB \-\-delete +.B \-d\fP,\fB \-\-delete delete the key from the config file .TP -.B \-l\fP,\fB \-\-list +.B \-l\fP,\fB \-\-list list the configuration of the repo .UNINDENT .SH EXAMPLES @@ -90,13 +87,13 @@ making changes! .nf .ft C # find cache directory -$ cd ~/.cache/borg/$(borg config /path/to/repo id) +$ cd ~/.cache/borg/$(borg config id) # reserve some space -$ borg config /path/to/repo additional_free_space 2G +$ borg config additional_free_space 2G # make a repo append\-only -$ borg config /path/to/repo append_only 1 +$ borg config append_only 1 .ft P .fi .UNINDENT diff --git a/docs/man/borg-create.1 b/docs/man/borg-create.1 index 887814626..7d36f0940 100644 --- a/docs/man/borg-create.1 +++ b/docs/man/borg-create.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-CREATE 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-create \- Create new archive . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-CREATE" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-create \- Create new archive .SH SYNOPSIS .sp -borg [common options] create [options] ARCHIVE [PATH...] +borg [common options] create [options] NAME [PATH...] .SH DESCRIPTION .sp This command creates a backup archive containing all files found while recursively @@ -126,8 +126,8 @@ 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) +.B NAME +specify the archive name .TP .B PATH paths to archive @@ -135,22 +135,22 @@ paths to archive .SS optional arguments .INDENT 0.0 .TP -.B \-n\fP,\fB \-\-dry\-run +.B \-n\fP,\fB \-\-dry\-run do not create a backup archive .TP -.B \-s\fP,\fB \-\-stats +.B \-s\fP,\fB \-\-stats print statistics for the created archive .TP -.B \-\-list +.B \-\-list output verbose list of items (files, dirs, ...) .TP .BI \-\-filter \ STATUSCHARS only display items with the given status characters (see description) .TP -.B \-\-json +.B \-\-json output stats as JSON. Implies \fB\-\-stats\fP\&. .TP -.B \-\-no\-cache\-sync +.B \-\-no\-cache\-sync experimental: do not synchronize the cache. Implies not using the files cache. .TP .BI \-\-stdin\-name \ NAME @@ -160,18 +160,18 @@ use NAME in archive for stdin data (default: \(aqstdin\(aq) set user USER in archive for stdin data (default: \(aqroot\(aq) .TP .BI \-\-stdin\-group \ GROUP -set group GROUP in archive for stdin data (default: \(aqroot\(aq) +set group GROUP in archive for stdin data (default: \(aqwheel\(aq) .TP .BI \-\-stdin\-mode \ M set mode to M in archive for stdin data (default: 0660) .TP -.B \-\-content\-from\-command +.B \-\-content\-from\-command interpret PATH as command and store its stdout. See also section Reading from stdin below. .TP -.B \-\-paths\-from\-stdin +.B \-\-paths\-from\-stdin read DELIM\-separated list of paths to backup from stdin. Will not recurse into directories. .TP -.B \-\-paths\-from\-command +.B \-\-paths\-from\-command interpret PATH as command and treat its output as \fB\-\-paths\-from\-stdin\fP .TP .BI \-\-paths\-delimiter \ DELIM @@ -192,61 +192,61 @@ include/exclude paths matching PATTERN .BI \-\-patterns\-from \ PATTERNFILE read include/exclude patterns from PATTERNFILE, one per line .TP -.B \-\-exclude\-caches +.B \-\-exclude\-caches exclude directories that contain a CACHEDIR.TAG file (\fI\%http://www.bford.info/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 +.B \-\-keep\-exclude\-tags if tag objects are specified with \fB\-\-exclude\-if\-present\fP, don\(aqt omit the tag objects themselves from the backup archive .TP -.B \-\-exclude\-nodump +.B \-\-exclude\-nodump exclude files flagged NODUMP .UNINDENT .SS Filesystem options .INDENT 0.0 .TP -.B \-x\fP,\fB \-\-one\-file\-system +.B \-x\fP,\fB \-\-one\-file\-system stay in the same file system and do not store mount points of other file systems. This might behave different from your expectations, see the docs. .TP -.B \-\-numeric\-owner +.B \-\-numeric\-owner deprecated, use \fB\-\-numeric\-ids\fP instead .TP -.B \-\-numeric\-ids +.B \-\-numeric\-ids only store numeric user and group identifiers .TP -.B \-\-noatime +.B \-\-noatime do not store atime into archive .TP -.B \-\-atime +.B \-\-atime do store atime into archive .TP -.B \-\-noctime +.B \-\-noctime do not store ctime into archive .TP -.B \-\-nobirthtime +.B \-\-nobirthtime do not store birthtime (creation date) into archive .TP -.B \-\-nobsdflags +.B \-\-nobsdflags deprecated, use \fB\-\-noflags\fP instead .TP -.B \-\-noflags +.B \-\-noflags do not read and store flags (e.g. NODUMP, IMMUTABLE) into archive .TP -.B \-\-noacls +.B \-\-noacls do not read and store ACLs into archive .TP -.B \-\-noxattrs +.B \-\-noxattrs do not read and store xattrs into archive .TP -.B \-\-sparse +.B \-\-sparse detect sparse holes in input (supported only by fixed chunker) .TP .BI \-\-files\-cache \ MODE operate files cache in MODE. default: ctime,size,inode .TP -.B \-\-read\-special +.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 @@ -274,86 +274,84 @@ select compression algorithm, see the output of the "borg help compression" comm .nf .ft C # Backup ~/Documents into an archive named "my\-documents" -$ borg create /path/to/repo::my\-documents ~/Documents +$ borg create my\-documents ~/Documents # same, but list all files as we process them -$ borg create \-\-list /path/to/repo::my\-documents ~/Documents +$ borg create \-\-list my\-documents ~/Documents # Backup ~/Documents and ~/src but exclude pyc files -$ borg create /path/to/repo::my\-files \e +$ borg create 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 etc.) -$ borg create /path/to/repo::my\-files /home \e - \-\-exclude \(aqsh:/home/*/.thumbnails\(aq +$ borg create my\-files /home \-\-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 lz4 (fast, low compression ratio) -$ borg create \-C zlib,6 \-\-one\-file\-system /path/to/repo::root\-{now:%Y\-%m\-%d} / +$ borg create \-C zlib,6 \-\-one\-file\-system root\-{now:%Y\-%m\-%d} / -# Backup onto a remote host ("push" style) via ssh to port 2222, -# logging in as user "borg" and storing into /path/to/repo -$ borg create ssh://borg@backup.example.org:2222/path/to/repo::{fqdn}\-root\-{now} / +# Backup into an archive name like FQDN\-root\-TIMESTAMP +$ borg create \(aq{fqdn}\-root\-{now}\(aq / # 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} . +$ borg create 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 buzhash,10,23,16,4095 /path/to/repo::small /smallstuff +$ borg create \-\-chunker\-params buzhash,10,23,16,4095 small /smallstuff # Backup a raw device (must not be active/in use/mounted at that time) -$ borg create \-\-read\-special \-\-chunker\-params fixed,4194304 /path/to/repo::my\-sdx /dev/sdX +$ borg create \-\-read\-special \-\-chunker\-params fixed,4194304 my\-sdx /dev/sdX # Backup a sparse disk image (must not be active/in use/mounted at that time) -$ borg create \-\-sparse \-\-chunker\-params fixed,4194304 /path/to/repo::my\-disk my\-disk.raw +$ borg create \-\-sparse \-\-chunker\-params fixed,4194304 my\-disk my\-disk.raw # No compression (none) -$ borg create \-\-compression none /path/to/repo::arch ~ +$ borg create \-\-compression none arch ~ # Super fast, low compression (lz4, default) -$ borg create /path/to/repo::arch ~ +$ borg create arch ~ # Less fast, higher compression (zlib, N = 0..9) -$ borg create \-\-compression zlib,N /path/to/repo::arch ~ +$ borg create \-\-compression zlib,N arch ~ # Even slower, even higher compression (lzma, N = 0..9) -$ borg create \-\-compression lzma,N /path/to/repo::arch ~ +$ borg create \-\-compression lzma,N arch ~ # Only compress compressible data with lzma,N (N = 0..9) -$ borg create \-\-compression auto,lzma,N /path/to/repo::arch ~ +$ borg create \-\-compression auto,lzma,N arch ~ # Use short hostname, user name and current time in archive name -$ borg create /path/to/repo::{hostname}\-{user}\-{now} ~ +$ borg create \(aq{hostname}\-{user}\-{now}\(aq ~ # Similar, use the same datetime format that is default as of borg 1.1 -$ borg create /path/to/repo::{hostname}\-{user}\-{now:%Y\-%m\-%dT%H:%M:%S} ~ +$ borg create \(aq{hostname}\-{user}\-{now:%Y\-%m\-%dT%H:%M:%S}\(aq ~ # As above, but add nanoseconds -$ borg create /path/to/repo::{hostname}\-{user}\-{now:%Y\-%m\-%dT%H:%M:%S.%f} ~ +$ borg create \(aq{hostname}\-{user}\-{now:%Y\-%m\-%dT%H:%M:%S.%f}\(aq ~ # 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 +$ borg create \(aqdaily\-projectA\-{now:%Y\-%m\-%d}\(aq projectA # Use external command to determine files to archive # Use \-\-paths\-from\-stdin with find to only backup files less than 1MB in size -$ find ~ \-size \-1000k | borg create \-\-paths\-from\-stdin /path/to/repo::small\-files\-only +$ find ~ \-size \-1000k | borg create \-\-paths\-from\-stdin small\-files\-only # Use \-\-paths\-from\-command with find to only backup files from a given user -$ borg create \-\-paths\-from\-command /path/to/repo::joes\-files \-\- find /srv/samba/shared \-user joe +$ borg create \-\-paths\-from\-command joes\-files \-\- find /srv/samba/shared \-user joe # Use \-\-paths\-from\-stdin with \-\-paths\-delimiter (for example, for filenames with newlines in them) $ find ~ \-size \-1000k \-print0 | borg create \e \-\-paths\-from\-stdin \e \-\-paths\-delimiter "\e0" \e - /path/to/repo::smallfiles\-handle\-newline + smallfiles\-handle\-newline .ft P .fi .UNINDENT diff --git a/docs/man/borg-delete.1 b/docs/man/borg-delete.1 index 68639c1ea..7a213de1d 100644 --- a/docs/man/borg-delete.1 +++ b/docs/man/borg-delete.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-DELETE 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-delete \- Delete an existing repository or archives . .nr rst2man-indent-level 0 . @@ -30,21 +27,19 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-DELETE" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-delete \- Delete archives .SH SYNOPSIS .sp -borg [common options] delete [options] [REPOSITORY_OR_ARCHIVE] [ARCHIVE...] +borg [common options] delete [options] .SH DESCRIPTION .sp -This command deletes an archive from the repository or the complete repository. +This command deletes archives from the repository. .sp Important: When deleting archives, repository disk space is \fBnot\fP freed until you run \fBborg compact\fP\&. .sp -When you delete a complete repository, the security info and local cache for it -(if any) are also deleted. Alternatively, you can delete just the local cache -with the \fB\-\-cache\-only\fP option, or keep the security info with the -\fB\-\-keep\-security\-info\fP option. -.sp When in doubt, use \fB\-\-dry\-run \-\-list\fP to see what would be deleted. .sp When using \fB\-\-stats\fP, you will get some statistics about how much data was @@ -58,43 +53,35 @@ pattern to match multiple archives using the \fB\-\-glob\-archives GLOB\fP optio (for more info on these patterns, see \fIborg_patterns\fP). Note that these two options are mutually exclusive. .sp -To avoid accidentally deleting archives, especially when using glob patterns, -it might be helpful to use the \fB\-\-dry\-run\fP to test out the command without -actually making any changes to the repository. +Always first use \fB\-\-dry\-run \-\-list\fP to see what would be deleted. .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 delete -.TP -.B ARCHIVE -archives to delete -.UNINDENT .SS optional arguments .INDENT 0.0 .TP -.B \-n\fP,\fB \-\-dry\-run +.B \-n\fP,\fB \-\-dry\-run do not change repository .TP -.B \-\-list +.B \-\-list output verbose list of archives .TP -.B \-s\fP,\fB \-\-stats +.B \-\-consider\-checkpoints +consider checkpoint archives for deletion (default: not considered). +.TP +.B \-s\fP,\fB \-\-stats print statistics for the deleted archive .TP -.B \-\-cache\-only +.B \-\-cache\-only delete only the local cache for the given repository .TP -.B \-\-force +.B \-\-force force deletion of corrupted archives, use \fB\-\-force \-\-force\fP in case \fB\-\-force\fP does not work. .TP -.B \-\-keep\-security\-info +.B \-\-keep\-security\-info keep the local security info when deleting a repository .TP -.B \-\-save\-space +.B \-\-save\-space work slower, but using less space .UNINDENT .SS Archive filters @@ -122,26 +109,18 @@ consider last N archives after other filters were applied .nf .ft C # delete a single backup archive: -$ borg delete /path/to/repo::Monday +$ borg delete Monday # actually free disk space: -$ borg compact /path/to/repo +$ borg compact # delete all archives whose names begin with the machine\(aqs hostname followed by "\-" -$ borg delete \-\-prefix \(aq{hostname}\-\(aq /path/to/repo +$ borg delete \-\-prefix \(aq{hostname}\-\(aq # delete all archives whose names contain "\-2012\-" -$ borg delete \-\-glob\-archives \(aq*\-2012\-*\(aq /path/to/repo +$ borg delete \-a \(aq*\-2012\-*\(aq # see what would be deleted if delete was run without \-\-dry\-run -$ borg delete \-\-list \-\-dry\-run \-a \(aq*\-May\-*\(aq /path/to/repo - -# 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 +$ borg delete \-\-list \-\-dry\-run \-a \(aq*\-May\-*\(aq .ft P .fi .UNINDENT diff --git a/docs/man/borg-diff.1 b/docs/man/borg-diff.1 index 2ccf05e03..27b026d29 100644 --- a/docs/man/borg-diff.1 +++ b/docs/man/borg-diff.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-DIFF 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-diff \- Diff contents of two archives . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-DIFF" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-diff \- Diff contents of two archives .SH SYNOPSIS .sp -borg [common options] diff [options] REPO::ARCHIVE1 ARCHIVE2 [PATH...] +borg [common options] diff [options] ARCHIVE1 ARCHIVE2 [PATH...] .SH DESCRIPTION .sp This command finds differences (file contents, user/group/mode) between archives. @@ -57,11 +57,11 @@ 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 +.B ARCHIVE1 +ARCHIVE1 name .TP .B ARCHIVE2 -ARCHIVE2 name (no repository location allowed) +ARCHIVE2 name .TP .B PATH paths of items inside the archives to compare; patterns are supported @@ -69,19 +69,19 @@ paths of items inside the archives to compare; patterns are supported .SS optional arguments .INDENT 0.0 .TP -.B \-\-numeric\-owner +.B \-\-numeric\-owner deprecated, use \fB\-\-numeric\-ids\fP instead .TP -.B \-\-numeric\-ids +.B \-\-numeric\-ids only consider numeric user and group identifiers .TP -.B \-\-same\-chunker\-params +.B \-\-same\-chunker\-params Override check of chunker parameters. .TP -.B \-\-sort +.B \-\-sort Sort the output lines by file path. .TP -.B \-\-json\-lines +.B \-\-json\-lines Format output as JSON Lines. .UNINDENT .SS Exclusion options @@ -105,44 +105,17 @@ read include/exclude patterns from PATTERNFILE, one per line .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 . - -$ echo "testing 123" >> file1 -$ 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 - +17 B \-5 B file1 -added 0 B file4 -removed 0 B file3 - -$ borg diff testrepo::archive1 archive3 +$ borg diff archive1 archive2 +17 B \-5 B [\-rw\-r\-\-r\-\- \-> \-rwxr\-xr\-x] file1 +135 B \-252 B file2 added 0 B file4 removed 0 B file3 -$ borg diff \-\-json\-lines testrepo::archive1 archive3 +$ borg diff archive1 archive2 {"path": "file1", "changes": [{"type": "modified", "added": 17, "removed": 5}, {"type": "mode", "old_mode": "\-rw\-r\-\-r\-\-", "new_mode": "\-rwxr\-xr\-x"}]} {"path": "file2", "changes": [{"type": "modified", "added": 135, "removed": 252}]} {"path": "file4", "changes": [{"type": "added", "size": 0}]} -{"path": "file3", "changes": [{"type": "removed", "size": 0}] +{"path": "file3", "changes": [{"type": "removed", "size": 0}]} .ft P .fi .UNINDENT diff --git a/docs/man/borg-export-tar.1 b/docs/man/borg-export-tar.1 index 437aad535..93900a558 100644 --- a/docs/man/borg-export-tar.1 +++ b/docs/man/borg-export-tar.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-EXPORT-TAR 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-export-tar \- Export archive contents as a tarball . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-EXPORT-TAR" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-export-tar \- Export archive contents as a tarball .SH SYNOPSIS .sp -borg [common options] export\-tar [options] ARCHIVE FILE [PATH...] +borg [common options] export\-tar [options] NAME FILE [PATH...] .SH DESCRIPTION .sp This command creates a tarball from an archive. @@ -115,8 +115,8 @@ See \fIborg\-common(1)\fP for common options of Borg commands. .SS arguments .INDENT 0.0 .TP -.B ARCHIVE -archive to export +.B NAME +specify the archive name .TP .B FILE output tar file. "\-" to write to stdout instead. @@ -127,10 +127,10 @@ paths to extract; patterns are supported .SS optional arguments .INDENT 0.0 .TP -.B \-\-tar\-filter +.B \-\-tar\-filter filter program to pipe data through .TP -.B \-\-list +.B \-\-list output verbose list of items (files, dirs, ...) .TP .BI \-\-tar\-format \ FMT diff --git a/docs/man/borg-extract.1 b/docs/man/borg-extract.1 index 69d2a0e41..a5e70b1fd 100644 --- a/docs/man/borg-extract.1 +++ b/docs/man/borg-extract.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-EXTRACT 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-extract \- Extract archive contents . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-EXTRACT" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-extract \- Extract archive contents .SH SYNOPSIS .sp -borg [common options] extract [options] ARCHIVE [PATH...] +borg [common options] extract [options] NAME [PATH...] .SH DESCRIPTION .sp This command extracts the contents of an archive. By default the entire @@ -66,8 +66,8 @@ See \fIborg\-common(1)\fP for common options of Borg commands. .SS arguments .INDENT 0.0 .TP -.B ARCHIVE -archive to extract +.B NAME +specify the archive name .TP .B PATH paths to extract; patterns are supported @@ -75,34 +75,34 @@ paths to extract; patterns are supported .SS optional arguments .INDENT 0.0 .TP -.B \-\-list +.B \-\-list output verbose list of items (files, dirs, ...) .TP -.B \-n\fP,\fB \-\-dry\-run +.B \-n\fP,\fB \-\-dry\-run do not actually change any files .TP -.B \-\-numeric\-owner +.B \-\-numeric\-owner deprecated, use \fB\-\-numeric\-ids\fP instead .TP -.B \-\-numeric\-ids +.B \-\-numeric\-ids only obey numeric user and group identifiers .TP -.B \-\-nobsdflags +.B \-\-nobsdflags deprecated, use \fB\-\-noflags\fP instead .TP -.B \-\-noflags +.B \-\-noflags do not extract/set flags (e.g. NODUMP, IMMUTABLE) .TP -.B \-\-noacls +.B \-\-noacls do not extract/set ACLs .TP -.B \-\-noxattrs +.B \-\-noxattrs do not extract/set xattrs .TP -.B \-\-stdout +.B \-\-stdout write all extracted data to stdout .TP -.B \-\-sparse +.B \-\-sparse create holes in output sparse file from all\-zero chunks .UNINDENT .SS Exclusion options @@ -130,22 +130,22 @@ Remove the specified number of leading path elements. Paths with fewer elements .nf .ft C # Extract entire archive -$ borg extract /path/to/repo::my\-files +$ borg extract my\-files # Extract entire archive and list files while processing -$ borg extract \-\-list /path/to/repo::my\-files +$ borg extract \-\-list 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 +$ borg extract \-\-dry\-run my\-files # Extract the "src" directory -$ borg extract /path/to/repo::my\-files home/USERNAME/src +$ borg extract 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 +$ borg extract 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 +$ borg extract \-\-stdout my\-sdx | dd of=/dev/sdx bs=10M .ft P .fi .UNINDENT diff --git a/docs/man/borg-import-tar.1 b/docs/man/borg-import-tar.1 index dc8439775..9af05ce3b 100644 --- a/docs/man/borg-import-tar.1 +++ b/docs/man/borg-import-tar.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-IMPORT-TAR 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-import-tar \- Create a backup archive from a tarball . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-IMPORT-TAR" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-import-tar \- Create a backup archive from a tarball .SH SYNOPSIS .sp -borg [common options] import\-tar [options] ARCHIVE TARFILE +borg [common options] import\-tar [options] NAME TARFILE .SH DESCRIPTION .sp This command creates a backup archive from a tarball. @@ -86,8 +86,8 @@ 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) +.B NAME +specify the archive name .TP .B TARFILE input tar file. "\-" to read from stdin instead. @@ -95,19 +95,19 @@ input tar file. "\-" to read from stdin instead. .SS optional arguments .INDENT 0.0 .TP -.B \-\-tar\-filter +.B \-\-tar\-filter filter program to pipe data through .TP -.B \-s\fP,\fB \-\-stats +.B \-s\fP,\fB \-\-stats print statistics for the created archive .TP -.B \-\-list +.B \-\-list output verbose list of items (files, dirs, ...) .TP .BI \-\-filter \ STATUSCHARS only display items with the given status characters .TP -.B \-\-json +.B \-\-json output stats as JSON (implies \-\-stats) .UNINDENT .SS Archive options @@ -135,25 +135,25 @@ select compression algorithm, see the output of the "borg help compression" comm .nf .ft C # export as uncompressed tar -$ borg export\-tar /path/to/repo::Monday Monday.tar +$ borg export\-tar Monday Monday.tar # import an uncompressed tar -$ borg import\-tar /path/to/repo::Monday Monday.tar +$ borg import\-tar Monday Monday.tar # exclude some file types, compress using gzip -$ borg export\-tar /path/to/repo::Monday Monday.tar.gz \-\-exclude \(aq*.so\(aq +$ borg export\-tar Monday Monday.tar.gz \-\-exclude \(aq*.so\(aq # use higher compression level with gzip -$ borg export\-tar \-\-tar\-filter="gzip \-9" repo::Monday Monday.tar.gz +$ borg export\-tar \-\-tar\-filter="gzip \-9" Monday Monday.tar.gz # copy an archive from repoA to repoB -$ borg export\-tar \-\-tar\-format=BORG repoA::archive \- | borg import\-tar repoB::archive \- +$ borg \-r repoA export\-tar \-\-tar\-format=BORG archive \- | borg \-r repoB import\-tar archive \- # export a tar, but instead of storing it on disk, upload it to remote site using curl -$ borg export\-tar /path/to/repo::Monday \- | curl \-\-data\-binary @\- https://somewhere/to/POST +$ borg export\-tar Monday \- | curl \-\-data\-binary @\- https://somewhere/to/POST # remote extraction via "tarpipe" -$ borg export\-tar /path/to/repo::Monday \- | ssh somewhere "cd extracted; tar x" +$ borg export\-tar Monday \- | ssh somewhere "cd extracted; tar x" .ft P .fi .UNINDENT @@ -166,9 +166,9 @@ Outputs a script that copies all archives from repo1 to repo2: .sp .nf .ft C -for A T in \(gaborg list \-\-format=\(aq{archive} {time:%Y\-%m\-%dT%H:%M:%S}{LF}\(aq repo1\(ga +for A T in \(gaborg list \-\-format=\(aq{archive} {time:%Y\-%m\-%dT%H:%M:%S}{LF}\(aq\(ga do - echo "borg export\-tar \-\-tar\-format=BORG repo1::$A \- | borg import\-tar \-\-timestamp=$T repo2::$A \-" + echo "borg \-r repo1 export\-tar \-\-tar\-format=BORG $A \- | borg \-r repo2 import\-tar \-\-timestamp=$T $A \-" done .ft P .fi diff --git a/docs/man/borg-info.1 b/docs/man/borg-info.1 index dfc2487d5..fccf6ee69 100644 --- a/docs/man/borg-info.1 +++ b/docs/man/borg-info.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-INFO 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-info \- Show archive details such as disk space used . .nr rst2man-indent-level 0 . @@ -30,12 +27,15 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-INFO" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-info \- Show archive details such as disk space used .SH SYNOPSIS .sp -borg [common options] info [options] [REPOSITORY_OR_ARCHIVE] +borg [common options] info [options] .SH DESCRIPTION .sp -This command displays detailed information about the specified archive or repository. +This command displays detailed information about the specified archive. .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 @@ -53,16 +53,10 @@ This is shown as \fIutilization of maximum supported archive size\fP\&. .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 display information about -.UNINDENT .SS optional arguments .INDENT 0.0 .TP -.B \-\-json +.B \-\-json format output as JSON .UNINDENT .SS Archive filters @@ -89,58 +83,20 @@ consider last N archives after other filters were applied .sp .nf .ft C -$ borg info /path/to/repo::2017\-06\-29T11:00\-srv -Archive name: 2017\-06\-29T11:00\-srv -Archive fingerprint: b2f1beac2bd553b34e06358afa45a3c1689320d39163890c5bbbd49125f00fe5 +$ borg info Tuesday2022\-06\-25T20:51:39 +Archive name: Tuesday2022\-06\-25T20:51:39 +Archive fingerprint: f7dea0788dfc026cc2be1c0f5b94beb4e4084eb3402fc40c38d8719b1bf2d943 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 \-\-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 +Hostname: mba2020 +Username: tw +Time (start): Sat, 2022\-06\-25 20:51:40 +Time (end): Sat, 2022\-06\-25 20:51:40 +Duration: 0.03 seconds +Command line: /Users/tw/w/borg\-env/bin/borg \-r path/to/repo create \-\-stats \(aqTuesday{now}\(aq src \-\-progress +Utilization of maximum supported archive size: 0% +Number of files: 244 +Original size: 13.80 MB +Deduplicated size: 531 B .ft P .fi .UNINDENT diff --git a/docs/man/borg-key-change-algorithm.1 b/docs/man/borg-key-change-algorithm.1 index efcb6374b..6308a926b 100644 --- a/docs/man/borg-key-change-algorithm.1 +++ b/docs/man/borg-key-change-algorithm.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-CHANGE-ALGORITHM 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-key-change-algorithm \- Change repository key algorithm . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-KEY-CHANGE-ALGORITHM" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-key-change-algorithm \- Change repository key algorithm .SH SYNOPSIS .sp -borg [common options] key change\-algorithm [options] [REPOSITORY] ALGORITHM +borg [common options] key change\-algorithm [options] ALGORITHM .SH DESCRIPTION .sp Change the algorithm we use to encrypt and authenticate the borg key. @@ -77,8 +77,6 @@ borg key change\-algorithm /path/to/repo pbkdf2 .sp See \fIborg\-common(1)\fP for common options of Borg commands. .SS arguments -.sp -REPOSITORY .INDENT 0.0 .TP .B ALGORITHM diff --git a/docs/man/borg-key-change-location.1 b/docs/man/borg-key-change-location.1 index f40e83416..a0b626420 100644 --- a/docs/man/borg-key-change-location.1 +++ b/docs/man/borg-key-change-location.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-CHANGE-LOCATION 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-key-change-location \- Change repository key location . .nr rst2man-indent-level 0 . @@ -30,26 +27,30 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-KEY-CHANGE-LOCATION" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-key-change-location \- Change repository key location .SH SYNOPSIS .sp -borg [common options] key change\-location [options] [REPOSITORY] KEY_LOCATION +borg [common options] key change\-location [options] KEY_LOCATION .SH DESCRIPTION .sp Change the location of a borg key. The key can be stored at different locations: -.sp -keyfile: locally, usually in the home directory -repokey: inside the repo (in the repo config) .INDENT 0.0 -.TP -.B Note: this command does NOT change the crypto algorithms, just the key location, -thus you must ONLY give the key location (keyfile or repokey). +.IP \(bu 2 +keyfile: locally, usually in the home directory +.IP \(bu 2 +repokey: inside the repo (in the repo config) .UNINDENT +.sp +Please note: +.sp +This command does NOT change the crypto algorithms, just the key location, +thus you must ONLY give the key location (keyfile or repokey). .SH OPTIONS .sp See \fIborg\-common(1)\fP for common options of Borg commands. .SS arguments -.sp -REPOSITORY .INDENT 0.0 .TP .B KEY_LOCATION @@ -58,7 +59,7 @@ select key location .SS optional arguments .INDENT 0.0 .TP -.B \-\-keep +.B \-\-keep keep the key also at the current location (default: remove it) .UNINDENT .SH SEE ALSO diff --git a/docs/man/borg-key-change-passphrase.1 b/docs/man/borg-key-change-passphrase.1 index 9567c6ebb..1a3b169a9 100644 --- a/docs/man/borg-key-change-passphrase.1 +++ b/docs/man/borg-key-change-passphrase.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-CHANGE-PASSPHRASE 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-key-change-passphrase \- Change repository key file passphrase . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-KEY-CHANGE-PASSPHRASE" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-key-change-passphrase \- Change repository key file passphrase .SH SYNOPSIS .sp -borg [common options] key change\-passphrase [options] [REPOSITORY] +borg [common options] key change\-passphrase [options] .SH DESCRIPTION .sp The key files used for repository encryption are optionally passphrase @@ -45,9 +45,6 @@ does not protect future (nor past) backups to the same repository. .SH OPTIONS .sp See \fIborg\-common(1)\fP for common options of Borg commands. -.SS arguments -.sp -REPOSITORY .SH EXAMPLES .INDENT 0.0 .INDENT 3.5 @@ -55,7 +52,7 @@ REPOSITORY .nf .ft C # Create a key file protected repository -$ borg init \-\-encryption=keyfile \-v /path/to/repo +$ borg rcreate \-\-encryption=keyfile \-v Initializing repository at "/path/to/repo" Enter new passphrase: Enter same passphrase again: @@ -67,7 +64,7 @@ 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 +$ borg key change\-passphrase \-v Enter passphrase for key /root/.config/borg/keys/mnt_backup: Enter new passphrase: Enter same passphrase again: @@ -77,7 +74,7 @@ Key updated # Import a previously\-exported key into the specified # key file (creating or overwriting the output key) # (keyfile repositories only) -$ BORG_KEY_FILE=/path/to/output\-key borg key import /path/to/repo /path/to/exported +$ BORG_KEY_FILE=/path/to/output\-key borg key import /path/to/exported .ft P .fi .UNINDENT @@ -89,9 +86,9 @@ Fully automated using environment variables: .sp .nf .ft C -$ BORG_NEW_PASSPHRASE=old borg init \-e=repokey repo +$ BORG_NEW_PASSPHRASE=old borg rcreate \-e=repokey # now "old" is the current passphrase. -$ BORG_PASSPHRASE=old BORG_NEW_PASSPHRASE=new borg key change\-passphrase repo +$ BORG_PASSPHRASE=old BORG_NEW_PASSPHRASE=new borg key change\-passphrase # now "new" is the current passphrase. .ft P .fi diff --git a/docs/man/borg-key-export.1 b/docs/man/borg-key-export.1 index 1202f32a3..0405d58f3 100644 --- a/docs/man/borg-key-export.1 +++ b/docs/man/borg-key-export.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-EXPORT 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-key-export \- Export the repository key for backup . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-KEY-EXPORT" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-key-export \- Export the repository key for backup .SH SYNOPSIS .sp -borg [common options] key export [options] [REPOSITORY] [PATH] +borg [common options] key export [options] [PATH] .SH DESCRIPTION .sp If repository encryption is used, the repository is inaccessible @@ -78,8 +78,6 @@ borg key export \-\-qr\-html /path/to/repo encrypted\-key\-backup.html .sp See \fIborg\-common(1)\fP for common options of Borg commands. .SS arguments -.sp -REPOSITORY .INDENT 0.0 .TP .B PATH @@ -88,10 +86,10 @@ where to store the backup .SS optional arguments .INDENT 0.0 .TP -.B \-\-paper +.B \-\-paper Create an export suitable for printing and later type\-in .TP -.B \-\-qr\-html +.B \-\-qr\-html Create an html file suitable for printing and later type\-in or qr scan .UNINDENT .SH SEE ALSO diff --git a/docs/man/borg-key-import.1 b/docs/man/borg-key-import.1 index e2a7acd25..cb466d963 100644 --- a/docs/man/borg-key-import.1 +++ b/docs/man/borg-key-import.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY-IMPORT 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-key-import \- Import the repository key from backup . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-KEY-IMPORT" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-key-import \- Import the repository key from backup .SH SYNOPSIS .sp -borg [common options] key import [options] [REPOSITORY] [PATH] +borg [common options] key import [options] [PATH] .SH DESCRIPTION .sp This command restores a key previously backed up with the export command. @@ -53,8 +53,6 @@ key import\fP creates a new key file in \fB$BORG_KEYS_DIR\fP\&. .sp See \fIborg\-common(1)\fP for common options of Borg commands. .SS arguments -.sp -REPOSITORY .INDENT 0.0 .TP .B PATH @@ -63,7 +61,7 @@ path to the backup (\(aq\-\(aq to read from stdin) .SS optional arguments .INDENT 0.0 .TP -.B \-\-paper +.B \-\-paper interactively import from a backup done with \fB\-\-paper\fP .UNINDENT .SH SEE ALSO diff --git a/docs/man/borg-key.1 b/docs/man/borg-key.1 index 5cb2a6b40..82780ff82 100644 --- a/docs/man/borg-key.1 +++ b/docs/man/borg-key.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-KEY 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-key \- Manage a keyfile or repokey of a repository . .nr rst2man-indent-level 0 . @@ -30,6 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-KEY" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-key \- Manage a keyfile or repokey of a repository .SH SYNOPSIS .nf borg [common options] key export ... diff --git a/docs/man/borg-list.1 b/docs/man/borg-list.1 index d34fdf54e..6b366005d 100644 --- a/docs/man/borg-list.1 +++ b/docs/man/borg-list.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-LIST 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-list \- List archive or repository contents . .nr rst2man-indent-level 0 . @@ -30,12 +27,15 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-LIST" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-list \- List archive contents .SH SYNOPSIS .sp -borg [common options] list [options] [REPOSITORY_OR_ARCHIVE] [PATH...] +borg [common options] list [options] NAME [PATH...] .SH DESCRIPTION .sp -This command lists the contents of a repository or an archive. +This command lists the contents of an archive. .sp For more help on include/exclude patterns, see the \fIborg_patterns\fP command output. .SH OPTIONS @@ -44,8 +44,8 @@ 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 list contents of +.B NAME +specify the archive name .TP .B PATH paths to list; patterns are supported @@ -53,38 +53,14 @@ paths to list; patterns are supported .SS optional arguments .INDENT 0.0 .TP -.B \-\-consider\-checkpoints -Show checkpoint archives in the repository contents list (default: hidden). -.TP -.B \-\-short +.B \-\-short only print file/directory names, nothing else .TP .BI \-\-format \ FORMAT -specify format for file or archive listing (default for files: "{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}"; for archives: "{archive:<36} {time} [{id}]{NL}") +specify format for file listing (default: "{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}") .TP -.B \-\-json -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 \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 Archive filters -.INDENT 0.0 -.TP -.BI \-P \ PREFIX\fR,\fB \ \-\-prefix \ PREFIX -only consider archive names starting with this prefix. -.TP -.BI \-a \ GLOB\fR,\fB \ \-\-glob\-archives \ GLOB -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 -.BI \-\-sort\-by \ KEYS -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 +.B \-\-json\-lines +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 Exclusion options .INDENT 0.0 @@ -107,14 +83,7 @@ read include/exclude patterns from PATTERNFILE, one per line .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 +$ borg list 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 @@ -122,14 +91,14 @@ lrwxrwxrwx root root 0 Fri, 2015\-03\-27 20:24:26 bin/bzcmp \-> bzdif \-rwxr\-xr\-x root root 2140 Fri, 2015\-03\-27 20:24:22 bin/bzdiff \&... -$ borg list /path/to/repo::root\-2016\-02\-15 \-\-pattern "\- bin/ba*" +$ borg list root\-2016\-02\-15 \-\-pattern "\- bin/ba*" 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 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 \-\-format="{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}" +$ borg list archiveA \-\-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 @@ -137,11 +106,11 @@ 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.text \&... -$ borg list /path/to/repo/::archiveA \-\-pattern \(aqre:\e.ext$\(aq +$ borg list archiveA \-\-pattern \(aqre:\e.ext$\(aq \-rw\-rw\-r\-\- user user 1416192 Sun, 2015\-02\-01 11:00:00 code/myproject/file.ext \&... -$ borg list /path/to/repo/::archiveA \-\-pattern \(aqre:.ext$\(aq +$ borg list archiveA \-\-pattern \(aqre:.ext$\(aq \-rw\-rw\-r\-\- user user 1416192 Sun, 2015\-02\-01 11:00:00 code/myproject/file.ext \-rw\-rw\-r\-\- user user 1416192 Sun, 2015\-02\-01 11:00:00 code/myproject/file.text \&... @@ -160,23 +129,13 @@ Examples: .sp .nf .ft C -$ borg list \-\-format \(aq{archive}{NL}\(aq /path/to/repo -ArchiveFoo -ArchiveBar -\&... - -# {VAR:NUMBER} \- pad to NUMBER columns. -# Strings are left\-aligned, numbers are right\-aligned. -# Note: time columns except \(ga\(gaisomtime\(ga\(ga, \(ga\(gaisoctime\(ga\(ga and \(ga\(gaisoatime\(ga\(ga cannot be padded. -$ borg list \-\-format \(aq{archive:36} {time} [{id}]{NL}\(aq /path/to/repo -ArchiveFoo Thu, 2021\-12\-09 10:22:28 [0b8e9a312bef3f2f6e2d0fc110c196827786c15eba0188738e81697a7fa3b274] -$ borg list \-\-format \(aq{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}\(aq /path/to/repo::ArchiveFoo +$ borg list \-\-format \(aq{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}\(aq ArchiveFoo \-rw\-rw\-r\-\- user user 1024 Thu, 2021\-12\-09 10:22:17 file\-foo \&... # {VAR:NUMBER} \- pad to NUMBER columns right\-aligned. -$ borg list \-\-format \(aq{mode} {user:>6} {group:>6} {size:<8} {mtime} {path}{extra}{NL}\(aq /path/to/repo::ArchiveFoo +$ borg list \-\-format \(aq{mode} {user:>6} {group:>6} {size:<8} {mtime} {path}{extra}{NL}\(aq ArchiveFoo \-rw\-rw\-r\-\- user user 1024 Thu, 2021\-12\-09 10:22:17 file\-foo \&... .ft P @@ -185,50 +144,13 @@ $ borg list \-\-format \(aq{mode} {user:>6} {group:>6} {size:<8} {mtime} {path}{ .UNINDENT .sp The following keys are always available: -.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 and bpath keys below -.IP \(bu 2 -SPACE -.IP \(bu 2 -TAB -.IP \(bu 2 -CR -.IP \(bu 2 -LF -.UNINDENT -.sp -Keys available only when listing archives in a repository: -.INDENT 0.0 -.IP \(bu 2 -archive: archive name interpreted as text (might be missing non\-text characters, see barchive) -.IP \(bu 2 -name: alias of "archive" -.IP \(bu 2 -barchive: verbatim archive name, can contain any character except NUL -.IP \(bu 2 -comment: archive comment interpreted as text (might be missing non\-text characters, see bcomment) -.IP \(bu 2 -bcomment: verbatim archive comment, can contain any character except NUL -.IP \(bu 2 -id: internal ID of the archive -.IP \(bu 2 -start: time (start) of creation of the archive -.IP \(bu 2 -time: alias of "start" -.IP \(bu 2 -end: time (end) of creation of the archive -.IP \(bu 2 -command_line: command line which was used to create the archive -.IP \(bu 2 -hostname: hostname of host on which this archive was created -.IP \(bu 2 -username: username of user who created this archive -.UNINDENT +\- NEWLINE: OS dependent line separator +\- NL: alias of NEWLINE +\- NUL: NUL character for creating print0 / xargs \-0 like output, see barchive and bpath keys below +\- SPACE +\- TAB +\- CR +\- LF .sp Keys available only when listing files in an archive: .INDENT 0.0 @@ -249,20 +171,18 @@ 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) +source: link target for symlinks (identical to linktarget) .IP \(bu 2 linktarget .IP \(bu 2 +hlid: hard link identity (same if hardlinking same fs object) +.IP \(bu 2 flags .IP \(bu 2 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 diff --git a/docs/man/borg-mount.1 b/docs/man/borg-mount.1 index 182c5411a..4ebbcf753 100644 --- a/docs/man/borg-mount.1 +++ b/docs/man/borg-mount.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-MOUNT 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-mount \- Mount archive or an entire repository as a FUSE filesystem . .nr rst2man-indent-level 0 . @@ -30,9 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-MOUNT" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-mount \- Mount archive or an entire repository as a FUSE filesystem .SH SYNOPSIS .sp -borg [common options] mount [options] REPOSITORY_OR_ARCHIVE MOUNTPOINT [PATH...] +borg [common options] mount [options] MOUNTPOINT [PATH...] .SH DESCRIPTION .sp This command mounts an archive as a FUSE filesystem. This can be useful for @@ -98,9 +98,6 @@ 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 mount -.TP .B MOUNTPOINT where to mount filesystem .TP @@ -110,19 +107,19 @@ paths to extract; patterns are supported .SS optional arguments .INDENT 0.0 .TP -.B \-\-consider\-checkpoints +.B \-\-consider\-checkpoints Show checkpoint archives in the repository contents list (default: hidden). .TP -.B \-f\fP,\fB \-\-foreground +.B \-f\fP,\fB \-\-foreground stay in foreground, do not daemonize .TP -.B \-o +.B \-o Extra mount options .TP -.B \-\-numeric\-owner +.B \-\-numeric\-owner deprecated, use \fB\-\-numeric\-ids\fP instead .TP -.B \-\-numeric\-ids +.B \-\-numeric\-ids use numeric user and group identifiers from archive(s) .UNINDENT .SS Archive filters diff --git a/docs/man/borg-patterns.1 b/docs/man/borg-patterns.1 index 546cd9259..bcf2adf5c 100644 --- a/docs/man/borg-patterns.1 +++ b/docs/man/borg-patterns.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BORG-PATTERNS 1 "2022-04-14" "" "borg backup tool" -.SH NAME -borg-patterns \- Details regarding patterns . .nr rst2man-indent-level 0 . @@ -30,45 +27,47 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BORG-PATTERNS" 1 "2022-06-25" "" "borg backup tool" +.SH NAME +borg-patterns \- Details regarding patterns .SH DESCRIPTION .sp -The path/filenames used as input for the pattern matching start from the -currently active recursion root. You usually give the recursion root(s) -when invoking borg and these can be either relative or absolute paths. +When specifying one or more file paths in a Borg command that supports +patterns for the respective option or argument, you can apply the +patterns described here to include only desired files and/or exclude +unwanted ones. Patterns can be used +.INDENT 0.0 +.IP \(bu 2 +for \fB\-\-exclude\fP option, +.IP \(bu 2 +in the file given with \fB\-\-exclude\-from\fP option, +.IP \(bu 2 +for \fB\-\-pattern\fP option, +.IP \(bu 2 +in the file given with \fB\-\-patterns\-from\fP option and +.IP \(bu 2 +for \fBPATH\fP arguments that explicitly support them. +.UNINDENT .sp -So, when you give \fIrelative/\fP as root, the paths going into the matcher -will look like \fIrelative/.../file.ext\fP\&. When you give \fI/absolute/\fP as -root, they will look like \fI/absolute/.../file.ext\fP\&. +Borg always stores all file paths normalized and relative to the +current recursion root. The recursion root is also named \fBPATH\fP in +Borg commands like \fIborg create\fP that do a file discovery, so do not +confuse the root with the \fBPATH\fP argument of e.g. \fIborg extract\fP\&. .sp -File paths in Borg archives are always stored normalized and relative. -This means that e.g. \fBborg create /path/to/repo ../some/path\fP will -store all files as \fIsome/path/.../file.ext\fP and \fBborg create -/path/to/repo /home/user\fP will store all files as -\fIhome/user/.../file.ext\fP\&. +Starting with Borg 1.2, paths that are matched against patterns always +appear relative. If you give \fB/absolute/\fP as root, the paths going +into the matcher will look relative like \fBabsolute/.../file.ext\fP\&. +If you give \fB\&../some/path\fP as root, the paths will look like +\fBsome/path/.../file.ext\fP\&. .sp -A directory exclusion pattern can end either with or without a slash (\(aq/\(aq). -If it ends with a slash, such as \fIsome/path/\fP, the directory will be -included but not its content. If it does not end with a slash, such as -\fIsome/path\fP, both the directory and content will be excluded. -.sp -File patterns support these styles: fnmatch, shell, regular expressions, -path prefixes and path full\-matches. By default, fnmatch is used for -\fB\-\-exclude\fP patterns and shell\-style is used for the \fB\-\-pattern\fP -option. For commands that support patterns in their \fBPATH\fP argument -like (\fBborg list\fP), the default pattern is path prefix. -.sp -Starting with Borg 1.2, for all but regular expression pattern matching -styles, all paths are treated as relative, meaning that a leading path -separator is removed after normalizing and before matching. This allows -you to use absolute or relative patterns arbitrarily. -.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). +File patterns support five different styles. 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 if a non\-default style is desired +or when the desired pattern starts with two alphanumeric characters +followed by a colon (i.e. \fBaa:something/*\fP). .INDENT 0.0 .TP -.B \fI\%Fnmatch\fP, selector \fIfm:\fP +.B \fI\%Fnmatch\fP, selector \fBfm:\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 @@ -76,7 +75,7 @@ matching any single character specified, including ranges, and \(aq[!...]\(aq matching any character not specified. For the purpose of these patterns, the path separator (backslash 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 +match (i.e. \fB[?]\fP to match the literal character \(aq?\(aq). For a path to match a pattern, the full path must match, or it must match from the start of the full path to just before a path separator. Except for the root path, paths will never end in the path separator when @@ -84,33 +83,31 @@ matching is attempted. Thus, if a given pattern ends in a path separator, a \(aq*\(aq is appended before matching is attempted. A leading path separator is always removed. .TP -.B Shell\-style patterns, selector \fIsh:\fP +.B Shell\-style patterns, selector \fBsh:\fP This is the default style for \fB\-\-pattern\fP and \fB\-\-patterns\-from\fP\&. 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 +is that the pattern may include \fB**/\fP for matching zero or more directory +levels, \fB*\fP for matching zero or more arbitrary characters with the exception of any path separator. A leading path separator is always removed. .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 full +.B \fI\%Regular expressions\fP, selector \fBre:\fP +Unlike shell patterns, regular expressions are not required to match the full 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 (backslash 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\&. +always normalized to a forward slash \(aq/\(aq before applying a pattern. .TP -.B Path prefix, selector \fIpp:\fP +.B Path prefix, selector \fBpp:\fP This pattern style is useful to match whole sub\-directories. The pattern -\fIpp:root/somedir\fP matches \fIroot/somedir\fP and everything therein. A leading -path separator is always removed. +\fBpp:root/somedir\fP matches \fBroot/somedir\fP and everything therein. +A leading path separator is always removed. .TP -.B Path full\-match, selector \fIpf:\fP +.B Path full\-match, selector \fBpf:\fP This pattern style is (only) useful to match full paths. This is kind of a pseudo pattern as it can not have any variable or -unspecified parts \- the full path must be given. \fIpf:root/file.ext\fP matches -\fIroot/file.ext\fP only. A leading path separator is always removed. +unspecified parts \- the full path must be given. \fBpf:root/file.ext\fP +matches \fBroot/file.ext\fP only. A leading path separator is always +removed. .sp Implementation note: this is implemented via very time\-efficient O(1) hashtable lookups (this means you can have huge amounts of such patterns @@ -125,12 +122,12 @@ Same logic applies for exclude. \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. +\fBre:\fP, \fBsh:\fP and \fBfm:\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 \fBre:\fP patterns. Further, ensure that \fBsh:\fP and +\fBfm:\fP patterns only contain a handful of wildcards at most. .UNINDENT .UNINDENT .sp @@ -139,8 +136,8 @@ from within a shell, the patterns should be quoted to protect them from expansion. .sp 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 +file with one pattern per line. Lines empty or starting with the hash 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. @@ -155,74 +152,101 @@ Examples: .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 / +$ borg create \-e \(aq*.o\(aq archive / # 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 / +$ borg create \-e \(aqhome/*/junk\(aq archive / # Exclude the contents of \(aq/home/user/cache\(aq but not the directory itself: -$ borg create \-e home/user/cache/ backup / +$ borg create \-e home/user/cache/ archive / # The file \(aq/home/user/cache/important\(aq is *not* backed up: -$ borg create \-e /home/user/cache/ backup / /home/user/cache/important +$ borg create \-e home/user/cache/ archive / /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 / +$ borg create \-\-exclude \(aqre:^home/[^/]+\e.tmp/\(aq archive / # Load exclusions from file $ cat >exclude.txt <