From 8a133095394afafbb14eb606dfe6cb45f0a4cdbc Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 3 Jun 2026 00:45:16 +0200 Subject: [PATCH] allow 'borg repo-list' for Borg 1.x repositories via --from-borg1 - Add --from-borg1 option to borg repo-list command. - Add allow_v1 argument to @with_repository decorator. - If allow_v1 is True and v1_legacy is requested, allow version 1 repositories and load Manifest with RepoObj1 object class. - Support LegacyRemoteRepository in Manifest class. - Add test_repo_list_from_borg1. --- src/borg/archiver/_common.py | 29 +++++++++++++++---- src/borg/archiver/repo_list_cmd.py | 3 +- src/borg/manifest.py | 3 +- .../testsuite/archiver/repo_list_cmd_test.py | 28 ++++++++++++++++++ 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index 60d504587..921b7c263 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -74,7 +74,14 @@ def compat_check(*, create, manifest, key, cache, compatibility, decorator_name) def with_repository( - create=False, lock=True, exclusive=False, manifest=True, cache=False, secure=True, compatibility=None + create=False, + lock=True, + exclusive=False, + manifest=True, + cache=False, + secure=True, + compatibility=None, + allow_v1=False, ): """ Method decorator for subcommand-handling methods: do_XYZ(self, args, repository, …) @@ -88,6 +95,7 @@ def with_repository( :param secure: do assert_secure after loading manifest :param compatibility: mandatory if not create and (manifest or cache), specifies mandatory feature categories to check + :param allow_v1: (bool) allow legacy Borg 1.x repositories """ # Note: with_repository decorator does not have a "key" argument (yet?) compatibility = compat_check( @@ -116,6 +124,8 @@ def with_repository( assert isinstance(exclusive, bool) lock = getattr(args, "lock", _lock) + v1_legacy = getattr(args, "v1_legacy", False) if allow_v1 else False + repository = get_repository( location, create=create, @@ -123,18 +133,25 @@ def with_repository( lock_wait=self.lock_wait, lock=lock, args=args, - v1_legacy=False, + v1_legacy=v1_legacy, ) with repository: - if repository.version not in (4,): + acceptable_versions = (1,) if v1_legacy else (4,) + if repository.version not in acceptable_versions: raise Error( - f"This borg version only accepts version 4 repos for -r/--repo, " - f"but not version {repository.version}. " + f"This borg version only accepts version {' or '.join(str(v) for v in acceptable_versions)} " + f"repos for -r/--repo, but not version {repository.version}. " f"You can use 'borg transfer' to copy archives from old to new repos." ) if manifest or cache: - manifest_ = Manifest.load(repository, compatibility, other=False) + if repository.version > 1: + ro_cls = RepoObj + else: + from ..legacy.repoobj import RepoObj1 + + ro_cls = RepoObj1 + manifest_ = Manifest.load(repository, compatibility, other=False, ro_cls=ro_cls) kwargs["manifest"] = manifest_ if "compression" in args: manifest_.repo_objs.compressor = args.compression.compressor diff --git a/src/borg/archiver/repo_list_cmd.py b/src/borg/archiver/repo_list_cmd.py index 5ae41ea95..64399288b 100644 --- a/src/borg/archiver/repo_list_cmd.py +++ b/src/borg/archiver/repo_list_cmd.py @@ -14,7 +14,7 @@ logger = create_logger() class RepoListMixIn: - @with_repository(compatibility=(Manifest.Operation.READ,)) + @with_repository(compatibility=(Manifest.Operation.READ,), allow_v1=True) def do_repo_list(self, args, repository, manifest): """List the archives contained in a repository.""" if args.format is not None: @@ -91,6 +91,7 @@ class RepoListMixIn: subparser.add_argument( "--short", dest="short", action="store_true", help="only print the archive IDs, nothing else" ) + subparser.add_argument("--from-borg1", dest="v1_legacy", action="store_true", help="repository is Borg 1.x") subparser.add_argument( "--format", metavar="FORMAT", diff --git a/src/borg/manifest.py b/src/borg/manifest.py index db1053fb6..696fac71a 100644 --- a/src/borg/manifest.py +++ b/src/borg/manifest.py @@ -448,9 +448,10 @@ class Manifest: def __init__(self, key, repository, item_keys=None, ro_cls=RepoObj): from .legacy.repository import LegacyRepository + from .legacy.remote import LegacyRemoteRepository from .legacy.archives import LegacyArchives - if isinstance(repository, LegacyRepository): + if isinstance(repository, (LegacyRepository, LegacyRemoteRepository)): self.archives: ArchivesInterface = LegacyArchives(repository, self) else: self.archives: ArchivesInterface = Archives(repository, self) diff --git a/src/borg/testsuite/archiver/repo_list_cmd_test.py b/src/borg/testsuite/archiver/repo_list_cmd_test.py index 002225d8f..d360cc0f7 100644 --- a/src/borg/testsuite/archiver/repo_list_cmd_test.py +++ b/src/borg/testsuite/archiver/repo_list_cmd_test.py @@ -1,6 +1,8 @@ import json import os +import pytest + from ...constants import * # NOQA from . import cmd, checkts, create_regular_file, generate_archiver_tests, RK_ENCRYPTION from .prune_cmd_test import _create_archive_ts @@ -169,3 +171,29 @@ def test_repo_list_deleted(archivers, request, backup_files): assert "normal2" not in output assert "deleted1" in output assert "deleted2" in output + + +def test_repo_list_from_borg1(archivers, request, monkeypatch): + archiver = request.getfixturevalue(archivers) + if archiver.get_kind() in ["remote", "binary"]: + pytest.skip("only works locally") + + import tarfile + + repo12_tar = os.path.join(os.path.dirname(__file__), "repo12.tar.gz") + original_location = archiver.repository_location + extract_dir = f"{original_location}1" + os.makedirs(extract_dir) + with tarfile.open(repo12_tar) as tf: + tf.extractall(extract_dir) + + monkeypatch.setenv("BORG_PASSPHRASE", "waytooeasyonlyfortests") + monkeypatch.setenv("BORG_TESTONLY_WEAKEN_KDF", "0") + + # Set repository location to the extracted Borg 1.x repository + archiver.repository_location = extract_dir + archiver.repository_path = extract_dir + + output = cmd(archiver, "repo-list", "--from-borg1") + assert "archive1" in output + assert "archive2" in output