add rest store support

This commit is contained in:
Thomas Waldmann 2026-03-13 23:55:19 +01:00
parent 93dfcf4565
commit a2ea1e9883
No known key found for this signature in database
GPG key ID: 243ACFA951F78E01
5 changed files with 56 additions and 26 deletions

View file

@ -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/...

View file

@ -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

View file

@ -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)

View file

@ -552,6 +552,17 @@ class Location:
re.VERBOSE,
)
# BorgStore REST server
# (http|https)://user:pass@host:port/
http_re = re.compile(
r"(?P<proto>http|https)://"
+ r"((?P<user>[^:@]+):(?P<pass>[^@]+)@)?"
+ host_re
+ optional_port_re
+ r"(?P<path>/)",
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 @@ class Location:
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 @@ class Location:
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 ''}"

View file

@ -194,6 +194,14 @@ class TestLocationWithoutEnv:
)
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"