From a2ea1e9883933bbbdc10d237a24e1faab82683d4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 13 Mar 2026 23:55:19 +0100 Subject: [PATCH] add rest store support --- .github/workflows/ci.yml | 12 +++--- pyproject.toml | 38 ++++++++++--------- src/borg/archiver/_common.py | 2 +- src/borg/helpers/parseformat.py | 22 ++++++++++- .../testsuite/helpers/parseformat_test.py | 8 ++++ 5 files changed, 56 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ce4fd62f7..cd7ba84d3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -281,13 +281,13 @@ jobs: - name: Install borgbackup run: | if [[ "$TOXENV" == *"llfuse"* ]]; then - pip install -ve ".[llfuse,cockpit,s3,sftp]" + pip install -ve ".[llfuse,cockpit,s3,sftp,rest,rclone]" elif [[ "$TOXENV" == *"pyfuse3"* ]]; then - pip install -ve ".[pyfuse3,cockpit,s3,sftp]" + pip install -ve ".[pyfuse3,cockpit,s3,sftp,rest,rclone]" elif [[ "$TOXENV" == *"mfusepy"* ]]; then - pip install -ve ".[mfusepy,cockpit,s3,sftp]" + pip install -ve ".[mfusepy,cockpit,s3,sftp,rest,rclone]" else - pip install -ve ".[cockpit,s3,sftp]" + pip install -ve ".[cockpit,s3,sftp,rest,rclone]" fi - name: Build Borg fat binaries (${{ matrix.binary }}) @@ -461,7 +461,7 @@ jobs: pip -V python -m pip install --upgrade pip wheel pip install -r requirements.d/development.lock.txt - pip install -e ".[mfusepy,cockpit,s3,sftp]" + pip install -e ".[mfusepy,cockpit,s3,sftp,rest,rclone]" tox -e py311-mfusepy if [[ "${{ matrix.do_binaries }}" == "true" && "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]]; then @@ -657,7 +657,7 @@ jobs: run: | # build borg.exe . env/bin/activate - pip install -e ".[cockpit,s3,sftp]" + pip install -e ".[cockpit,s3,sftp,rest,rclone]" mkdir -p dist/binary pyinstaller -y --clean --distpath=dist/binary scripts/borg.exe.spec # build sdist and wheel in dist/... diff --git a/pyproject.toml b/pyproject.toml index edd8f71d07..de1f852d51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ license = "BSD-3-Clause" license-files = ["LICENSE", "AUTHORS"] dependencies = [ "borghash ~= 0.1.0", - "borgstore ~= 0.3.0", + "borgstore ~= 0.4.0", "msgpack >=1.0.3, <=1.1.2", "packaging", "platformdirs >=3.0.0, <5.0.0; sys_platform == 'darwin'", # for macOS: breaking changes in 3.0.0. @@ -51,8 +51,10 @@ 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[s3] ~= 0.3.0"] -sftp = ["borgstore[sftp] ~= 0.3.0"] +s3 = ["borgstore[s3] ~= 0.4.0"] +sftp = ["borgstore[sftp] ~= 0.4.0"] +rclone = ["borgstore[rclone] ~= 0.4.0"] +rest = ["borgstore[rest] ~= 0.4.0"] cockpit = ["textual>=6.8.0"] # might also work with older versions, untested [project.urls] @@ -189,71 +191,71 @@ pass_env = ["*"] # needed by tox4, so env vars are visible for building borg [tool.tox.env.py310-llfuse] set_env = {BORG_FUSE_IMPL = "llfuse"} -extras = ["llfuse", "sftp", "s3"] +extras = ["llfuse", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py310-pyfuse3] set_env = {BORG_FUSE_IMPL = "pyfuse3"} -extras = ["pyfuse3", "sftp", "s3"] +extras = ["pyfuse3", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py310-mfusepy] set_env = {BORG_FUSE_IMPL = "mfusepy"} -extras = ["mfusepy", "sftp", "s3"] +extras = ["mfusepy", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py311-none] [tool.tox.env.py311-llfuse] set_env = {BORG_FUSE_IMPL = "llfuse"} -extras = ["llfuse", "sftp", "s3"] +extras = ["llfuse", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py311-pyfuse3] set_env = {BORG_FUSE_IMPL = "pyfuse3"} -extras = ["pyfuse3", "sftp", "s3"] +extras = ["pyfuse3", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py311-mfusepy] set_env = {BORG_FUSE_IMPL = "mfusepy"} -extras = ["mfusepy", "sftp", "s3"] +extras = ["mfusepy", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py312-none] [tool.tox.env.py312-llfuse] set_env = {BORG_FUSE_IMPL = "llfuse"} -extras = ["llfuse", "sftp", "s3"] +extras = ["llfuse", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py312-pyfuse3] set_env = {BORG_FUSE_IMPL = "pyfuse3"} -extras = ["pyfuse3", "sftp", "s3"] +extras = ["pyfuse3", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py312-mfusepy] set_env = {BORG_FUSE_IMPL = "mfusepy"} -extras = ["mfusepy", "sftp", "s3"] +extras = ["mfusepy", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py313-none] [tool.tox.env.py313-llfuse] set_env = {BORG_FUSE_IMPL = "llfuse"} -extras = ["llfuse", "sftp", "s3"] +extras = ["llfuse", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py313-pyfuse3] set_env = {BORG_FUSE_IMPL = "pyfuse3"} -extras = ["pyfuse3", "sftp", "s3"] +extras = ["pyfuse3", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py313-mfusepy] set_env = {BORG_FUSE_IMPL = "mfusepy"} -extras = ["mfusepy", "sftp", "s3"] +extras = ["mfusepy", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py314-none] [tool.tox.env.py314-llfuse] set_env = {BORG_FUSE_IMPL = "llfuse"} -extras = ["llfuse", "sftp", "s3"] +extras = ["llfuse", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py314-pyfuse3] set_env = {BORG_FUSE_IMPL = "pyfuse3"} -extras = ["pyfuse3", "sftp", "s3"] +extras = ["pyfuse3", "sftp", "s3", "rest", "rclone"] [tool.tox.env.py314-mfusepy] set_env = {BORG_FUSE_IMPL = "mfusepy"} -extras = ["mfusepy", "sftp", "s3"] +extras = ["mfusepy", "sftp", "s3", "rest", "rclone"] [tool.tox.env.ruff] skip_install = true diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index 19f08c319b..0ff6642a0e 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -39,7 +39,7 @@ def get_repository(location, *, create, exclusive, lock_wait, lock, args, v1_or_ ) elif ( - location.proto in ("sftp", "file", "rclone", "s3", "b2") and not v1_or_v2 + location.proto in ("sftp", "file", "http", "https", "rclone", "s3", "b2") and not v1_or_v2 ): # stuff directly supported by borgstore repository = Repository(location, create=create, exclusive=exclusive, lock_wait=lock_wait, lock=lock) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index d71af52798..bb38092d03 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -552,6 +552,17 @@ class Location: re.VERBOSE, ) + # BorgStore REST server + # (http|https)://user:pass@host:port/ + http_re = re.compile( + r"(?Phttp|https)://" + + r"((?P[^:@]+):(?P[^@]+)@)?" + + host_re + + optional_port_re + + r"(?P/)", + re.VERBOSE, + ) + # (s3|b2):[(profile|(access_key_id:access_key_secret))@][scheme://hostname[:port]]/bucket/path s3_re = re.compile( r""" @@ -620,6 +631,15 @@ def _parse(self, text): self.port = m.group("port") and int(m.group("port")) or None self.path = os.path.normpath(m.group("path")) return True + m = self.http_re.match(text) + if m: + self.proto = m.group("proto") + self.user = m.group("user") + self._pass = True if m.group("pass") else False + self._host = m.group("host") + self.port = m.group("port") and int(m.group("port")) or None + self.path = m.group("path") + return True m = self.rclone_re.match(text) if m: self.proto = m.group("proto") @@ -683,7 +703,7 @@ def canonical_path(self): return self.path if self.proto == "rclone": return f"{self.proto}:{self.path}" - if self.proto in ("sftp", "ssh", "s3", "b2"): + if self.proto in ("sftp", "ssh", "s3", "b2", "http", "https"): return ( f"{self.proto}://" f"{(self.user + '@') if self.user else ''}" diff --git a/src/borg/testsuite/helpers/parseformat_test.py b/src/borg/testsuite/helpers/parseformat_test.py index 82026f0b0f..c9cc1b5d59 100644 --- a/src/borg/testsuite/helpers/parseformat_test.py +++ b/src/borg/testsuite/helpers/parseformat_test.py @@ -194,6 +194,14 @@ def test_sftp(self, monkeypatch, keys_dir): ) assert Location("sftp://user@host:1234//abs/path").to_key_filename() == keys_dir + "host___abs_path" + def test_http(self, monkeypatch, keys_dir): + monkeypatch.delenv("BORG_REPO", raising=False) + assert ( + repr(Location("http://user:pass@host:1234/")) + == "Location(proto='http', user='user', pass='REDACTED', host='host', port=1234, path='/')" + ) + assert Location("http://user:pass@host:1234/").to_key_filename() == keys_dir + "host__" + def test_socket(self, monkeypatch, keys_dir): monkeypatch.delenv("BORG_REPO", raising=False) url = "socket:///c:/repo/path" if is_win32 else "socket:///repo/path"