From 4164c094fdd75824da6b6552be59588b9dad8281 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 24 Sep 2026 20:30:49 +0200 Subject: [PATCH] repository: clear error for borg 1.x repositories, fixes crash w/o --from-borg1 Opening a borg 1.x repository without --from-borg1 crashed with a traceback while loading the config/config store object, because a borg 1.x repository has a config *file* where borg 2 has the config/ namespace (NotADirectoryError for a local repository, HTTP 500 via ssh://; InvalidRepository on Windows). borgstore 0.6.4 reports a name below an object as ObjectNotFound on all platforms, so require it. If config/config is not found, look at "config" via the store backend and raise Repository.LegacyRepository (rc 29) if it is a file, telling the user to use --from-borg1 (e.g. with borg transfer). Co-Authored-By: Claude Opus 5.5 --- docs/installation.rst | 6 +++--- docs/internals/frontends.rst | 2 ++ pyproject.toml | 8 ++++---- src/borg/repository.py | 20 ++++++++++++++++++++ src/borg/testsuite/repository_test.py | 13 +++++++++++++ 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 796f8fdbdb..234a1e330d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -188,11 +188,11 @@ development header files (sometimes in a separate `-dev` or `-devel` package). - Additionally, your OS will need to have FUSE support installed (e.g. a package `fuse` for fuse 2 or a package `fuse3` for fuse 3 support). * Optionally, if you wish to use S3/B2 Backend: - - borgstore[rest,blake3,s3] ~= 0.6.1 (use `pip install borgbackup[s3]`) + - borgstore[rest,blake3,s3] ~= 0.6.4 (use `pip install borgbackup[s3]`) * Optionally, if you wish to use SFTP Backend: - - borgstore[rest,blake3,sftp] ~= 0.6.1 (use `pip install borgbackup[sftp]`) + - borgstore[rest,blake3,sftp] ~= 0.6.4 (use `pip install borgbackup[sftp]`) * Optionally, if you wish to use rclone Backend: - - borgstore[rest,blake3,rclone] ~= 0.6.1 (use `pip install borgbackup[rclone]`) + - borgstore[rest,blake3,rclone] ~= 0.6.4 (use `pip install borgbackup[rclone]`) * Optionally, if you wish to use the TUI (``borg --cockpit``): - textual >= 6.8.0 (use `pip install borgbackup[cockpit]`) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 2bb779edcf..686155c5d5 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -861,6 +861,8 @@ Errors Repository permission denied: {} Repository.KeyRequired rc: 28 traceback: yes Repository {} needs its key to access the store object {}, but no key was set. + Repository.LegacyRepository rc: 29 traceback: no + {} looks like a borg 1.x repository, use --from-borg1 to access it (e.g. with borg transfer). MandatoryFeatureUnsupported rc: 25 traceback: no Unsupported repository feature(s) {}. A newer version of Borg is required to access this repository. diff --git a/pyproject.toml b/pyproject.toml index 4ce900721d..2e88199d38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ license = "BSD-3-Clause" license-files = ["LICENSE", "AUTHORS"] dependencies = [ "borghash ~= 0.2.0", - "borgstore[rest,blake3] ~= 0.6.1", + "borgstore[rest,blake3] ~= 0.6.4", "msgpack >=1.0.3, <=1.2.2", "packaging", "platformdirs >=3.0.0, <5.0.0; sys_platform == 'darwin'", # for macOS: breaking changes in 3.0.0. @@ -57,9 +57,9 @@ mfusepy = ["mfusepy >= 3.1.0, <4.0.0"] # fuse 2+3, high-level # a pypi release of borgbackup can't contain a dependency on github! # mfusepym = ["mfusepy @ git+https://github.com/mxmlnkn/mfusepy.git@master"] nofuse = [] -s3 = ["borgstore[rest,blake3,s3] ~= 0.6.1"] -sftp = ["borgstore[rest,blake3,sftp] ~= 0.6.1"] -rclone = ["borgstore[rest,blake3,rclone] ~= 0.6.1"] +s3 = ["borgstore[rest,blake3,s3] ~= 0.6.4"] +sftp = ["borgstore[rest,blake3,sftp] ~= 0.6.4"] +rclone = ["borgstore[rest,blake3,rclone] ~= 0.6.4"] cockpit = ["textual>=6.8.0"] # might also work with older versions, untested [project.urls] diff --git a/src/borg/repository.py b/src/borg/repository.py index c08fa44515..2158d35cc1 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -886,6 +886,11 @@ class KeyRequired(ErrorWithTraceback): exit_mcode = 28 + class LegacyRepository(Error): + """{} looks like a borg 1.x repository, use --from-borg1 to access it (e.g. with borg transfer).""" + + exit_mcode = 29 + # Whole packs kept in memory for reads; the least recently used is evicted first. # Memory use is this count times the pack size. PACK_READER_CACHE_SIZE = 3 @@ -1141,6 +1146,8 @@ def _load_config(self): # only shows up here, when the first request fails with BackendDoesNotExist (#10365). raise self.DoesNotExist(str(self._location)) from None except StoreObjectNotFound: + if self._is_legacy_repository(): + raise self.LegacyRepository(str(self._location)) from None # the store exists, but has no repository config: a repository that lost its config, something # that never was a borg 2 repository, or the leftover of an interrupted repo-create (see create()). raise _ConfigMissing() from None @@ -1159,6 +1166,19 @@ def _load_config(self): # the crypto suite is recorded by both entries or by none, see save_config(). raise self.InvalidRepository(str(self._location)) + def _is_legacy_repository(self): + """Does the (opened) store look like a borg 1.x repository? + + A borg 1.x repository has a config *file* where borg 2 has the config/ namespace, so the store + object config/config is not found. The backend (not the Store, which only knows names within + namespaces) can look at that file-or-namespace directly. + """ + try: + info = self.store.backend.info("config") + except (StoreBackendError, OSError): # e.g. PermissionDenied, connection errors + return False + return info.exists and not info.directory + def looks_like_borg_store(self): """Does the (opened, config-less) store have the packs, archives, index and config namespaces? diff --git a/src/borg/testsuite/repository_test.py b/src/borg/testsuite/repository_test.py index 6a85a5ab12..ab803111c1 100644 --- a/src/borg/testsuite/repository_test.py +++ b/src/borg/testsuite/repository_test.py @@ -116,6 +116,19 @@ def failing_rest_backend(location): assert "unrecognized arguments: --rest" in str(exc_info.value) +@pytest.mark.parametrize("proto", ["file", "ssh"]) +def test_open_legacy_repository(tmp_path, proto): + # A borg 1.x repository (config is a file, not the config/ namespace) raises Repository.LegacyRepository. + path = tmp_path / "v1repo" + (path / "data").mkdir(parents=True) + (path / "config").write_text("[repository]\nversion = 1\nsegments_per_dir = 1000\nid = 00\n") + (path / "README").write_text("This is a Borg Backup repository.\n") + location = Location(os.fspath(path) if proto == "file" else f"ssh://__testsuite__/{os.fspath(path)}") + with pytest.raises(Repository.LegacyRepository): + with Repository(location, exclusive=True): + pass + + @pytest.fixture() def repository(tmp_path): repository_location = os.fspath(tmp_path / "repository")