Browse Source

Merge pull request #8472 from ThomasWaldmann/borgstore010

changes needed for borgstore 0.1.0
TW 7 months ago
parent
commit
dfbd3b7d5f

+ 1 - 1
.github/workflows/ci.yml

@@ -196,7 +196,7 @@ jobs:
 
   windows:
 
-    if: true  # build enabled
+    if: false  # build temporary disabled
     runs-on: windows-latest
     timeout-minutes: 120
     needs: linux

+ 7 - 7
docs/usage/general/repository-urls.rst.inc

@@ -14,21 +14,21 @@ Note: you may also prepend a ``file://`` to a filesystem path to get URL style.
 
 **Remote repositories** accessed via ssh user@host:
 
-``ssh://user@host:port/path/to/repo`` - absolute path
+``ssh://user@host:port//abs/path/to/repo`` - absolute path
 
-``ssh://user@host:port/./path/to/repo`` - path relative to current directory
-
-``ssh://user@host:port/~/path/to/repo`` - path relative to user's home directory
+``ssh://user@host:port/rel/path/to/repo`` - path relative to current directory
 
 **Remote repositories** accessed via sftp:
 
-``sftp://user@host:port/path/to/repo`` - absolute path
+``sftp://user@host:port//abs/path/to/repo`` - absolute path
+
+``sftp://user@host:port/rel/path/to/repo`` - path relative to current directory
 
 For ssh and sftp URLs, the ``user@`` and ``:port`` parts are optional.
 
 **Remote repositories** accessed via rclone:
 
-``rclone://remote:path`` - see the rclone docs for more details.
+``rclone:remote:path`` - see the rclone docs for more details about remote:path.
 
 
 If you frequently need the same repo URL, it is a good idea to set the
@@ -36,7 +36,7 @@ If you frequently need the same repo URL, it is a good idea to set the
 
 ::
 
-    export BORG_REPO='ssh://user@host:port/path/to/repo'
+    export BORG_REPO='ssh://user@host:port/rel/path/to/repo'
 
 Then just leave away the ``--repo`` option if you want
 to use the default - it will be read from BORG_REPO then.

+ 1 - 1
pyproject.toml

@@ -30,7 +30,7 @@ classifiers = [
 ]
 license = {text="BSD"}
 dependencies = [
-  "borgstore ~= 0.0.4",
+  "borgstore ~= 0.1.0",
   "msgpack >=1.0.3, <=1.1.0",
   "packaging",
   "platformdirs >=3.0.0, <5.0.0; sys_platform == 'darwin'",  # for macOS: breaking changes in 3.0.0,

+ 9 - 9
scripts/msys2-install-deps

@@ -1,9 +1,9 @@
-#!/bin/bash
-
-pacman -S --needed --noconfirm git mingw-w64-ucrt-x86_64-{toolchain,pkgconf,zstd,lz4,xxhash,openssl,python-msgpack,python-argon2_cffi,python-platformdirs,python,cython,python-setuptools,python-wheel,python-build,python-pkgconfig,python-packaging,python-pip,python-paramiko}
-python -m pip install --upgrade pip 
-pip install pyinstaller==6.3.0
-
-if [ "$1" = "development" ]; then
-	pacman -S --needed --noconfirm mingw-w64-ucrt-x86_64-python-{pytest,pytest-benchmark,pytest-cov,pytest-forked,pytest-xdist}
-fi
+#!/bin/bash
+
+pacman -S --needed --noconfirm git mingw-w64-ucrt-x86_64-{toolchain,pkgconf,zstd,lz4,xxhash,openssl,rclone,python-msgpack,python-argon2_cffi,python-platformdirs,python,cython,python-setuptools,python-wheel,python-build,python-pkgconfig,python-packaging,python-pip,python-paramiko}
+python -m pip install --upgrade pip
+pip install pyinstaller==6.10.0
+
+if [ "$1" = "development" ]; then
+	pacman -S --needed --noconfirm mingw-w64-ucrt-x86_64-python-{pytest,pytest-benchmark,pytest-cov,pytest-forked,pytest-xdist}
+fi

+ 4 - 0
src/borg/archiver/repo_create_cmd.py

@@ -69,6 +69,10 @@ class RepoCreateMixIn:
         This command creates a new, empty repository. A repository is a ``borgstore`` store
         containing the deduplicated data from zero or more archives.
 
+        Repository creation can be quite slow for some kinds of stores (e.g. for ``sftp:``) -
+        this is due to borgstore pre-creating all directories needed, making usage of the
+        store faster.
+
         Encryption mode TLDR
         ++++++++++++++++++++
 

+ 1 - 1
src/borg/conftest.py

@@ -120,7 +120,7 @@ def archiver(tmp_path, set_env_variables):
 
 @pytest.fixture()
 def remote_archiver(archiver):
-    archiver.repository_location = "ssh://__testsuite__" + str(archiver.repository_path)
+    archiver.repository_location = "ssh://__testsuite__/" + str(archiver.repository_path)
     yield archiver
 
 

+ 43 - 116
src/borg/helpers/parseformat.py

@@ -401,6 +401,7 @@ def parse_stringified_list(s):
 class Location:
     """Object representing a repository location"""
 
+    # user@ (optional)
     # user must not contain "@", ":" or "/".
     # Quoting adduser error message:
     # "To avoid problems, the username should consist only of letters, digits,
@@ -408,28 +409,7 @@ class Location:
     # (as defined by IEEE Std 1003.1-2001)."
     # We use "@" as separator between username and hostname, so we must
     # disallow it within the pure username part.
-    optional_user_re = r"""
-        (?:(?P<user>[^@:/]+)@)?
-    """
-
-    # path must not contain :: (it ends at :: or string end), but may contain single colons.
-    # to avoid ambiguities with other regexes, it must also not start with ":" nor with "//" nor with "ssh://".
-    local_path_re = r"""
-        (?!(:|//|ssh://|socket://))                         # not starting with ":" or // or ssh:// or socket://
-        (?P<path>([^:]|(:(?!:)))+)                          # any chars, but no "::"
-        """
-
-    # file_path must not contain :: (it ends at :: or string end), but may contain single colons.
-    # it must start with a / and that slash is part of the path.
-    file_path_re = r"""
-        (?P<path>(([^/]*)/([^:]|(:(?!:)))+))                # start opt. servername, then /, then any chars, but no "::"
-        """
-
-    # abs_path must not contain :: (it ends at :: or string end), but may contain single colons.
-    # it must start with a / and that slash is part of the path.
-    abs_path_re = r"""
-        (?P<path>(/([^:]|(:(?!:)))+))                       # start with /, then any chars, but no "::"
-        """
+    optional_user_re = r"(?:(?P<user>[^@:/]+)@)?"
 
     # host NAME, or host IP ADDRESS (v4 or v6, v6 must be in square brackets)
     host_re = r"""
@@ -440,69 +420,38 @@ class Location:
         )
     """
 
-    # regexes for misc. kinds of supported location specifiers:
-    ssh_re = re.compile(
-        r"""
-        (?P<proto>ssh)://                                       # ssh://
-        """
-        + optional_user_re
-        + host_re
-        + r"""                 # user@  (optional), host name or address
-        (?::(?P<port>\d+))?                                     # :port (optional)
-        """
-        + abs_path_re,
-        re.VERBOSE,
-    )  # path
+    # :port (optional)
+    optional_port_re = r"(?::(?P<port>\d+))?"
 
-    sftp_re = re.compile(
-        r"""
-        (?P<proto>sftp)://                                      # sftp://
-        """
-        + optional_user_re
-        + host_re
-        + r"""                 # user@  (optional), host name or address
-        (?::(?P<port>\d+))?                                     # :port (optional)
-        """
-        + abs_path_re,
-        re.VERBOSE,
-    )  # path
+    # path may contain any chars. to avoid ambiguities with other regexes,
+    # it must not start with "//" nor with "scheme://" nor with "rclone:".
+    local_path_re = r"""
+        (?!(//|(ssh|socket|sftp|file)://|rclone:))
+        (?P<path>.+)
+    """
 
-    rclone_re = re.compile(
-        r"""
-        (?P<proto>rclone)://                                    # rclone://
-        (?P<path>(.*))
-        """,
-        re.VERBOSE,
-    )  # path
+    # abs_path must start with a slash.
+    abs_path_re = r"(?P<path>/.+)"
 
-    socket_re = re.compile(
-        r"""
-        (?P<proto>socket)://                                    # socket://
-        """
-        + abs_path_re,
-        re.VERBOSE,
-    )  # path
+    # path may or may not start with a slash.
+    abs_or_rel_path_re = r"(?P<path>.+)"
 
-    file_re = re.compile(
-        r"""
-        (?P<proto>file)://                                      # file://
-        """
-        + file_path_re,
+    # regexes for misc. kinds of supported location specifiers:
+    ssh_or_sftp_re = re.compile(
+        r"(?P<proto>(ssh|sftp))://"
+        + optional_user_re
+        + host_re
+        + optional_port_re
+        + r"/"  # this is the separator, not part of the path!
+        + abs_or_rel_path_re,
         re.VERBOSE,
-    )  # servername/path or path
+    )
 
-    local_re = re.compile(local_path_re, re.VERBOSE)  # local path
+    rclone_re = re.compile(r"(?P<proto>rclone):(?P<path>(.*))", re.VERBOSE)
 
-    win_file_re = re.compile(
-        r"""
-        (?:file://)?                                        # optional file protocol
-        (?P<path>
-            (?:[a-zA-Z]:)?                                  # Drive letter followed by a colon (optional)
-            (?:[^:]+)                                       # Anything which does not contain a :, at least one char
-        )
-        """,
-        re.VERBOSE,
-    )
+    file_or_socket_re = re.compile(r"(?P<proto>(file|socket))://" + abs_path_re, re.VERBOSE)
+
+    local_re = re.compile(local_path_re, re.VERBOSE)
 
     def __init__(self, text="", overrides={}, other=False):
         self.repo_env_var = "BORG_OTHER_REPO" if other else "BORG_REPO"
@@ -532,47 +481,28 @@ class Location:
             raise ValueError('Invalid location format: "%s"' % self.processed)
 
     def _parse(self, text):
-        def normpath_special(p):
-            # avoid that normpath strips away our relative path hack and even makes p absolute
-            relative = p.startswith("/./")
-            p = os.path.normpath(p)
-            return ("/." + p) if relative else p
-
-        m = self.ssh_re.match(text)
+        m = self.ssh_or_sftp_re.match(text)
         if m:
             self.proto = m.group("proto")
             self.user = m.group("user")
             self._host = m.group("host")
             self.port = m.group("port") and int(m.group("port")) or None
-            self.path = normpath_special(m.group("path"))
-            return True
-        m = self.sftp_re.match(text)
-        if m:
-            self.proto = m.group("proto")
-            self.user = m.group("user")
-            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.path = os.path.normpath(m.group("path"))
             return True
         m = self.rclone_re.match(text)
         if m:
             self.proto = m.group("proto")
             self.path = m.group("path")
             return True
-        m = self.file_re.match(text)
-        if m:
-            self.proto = m.group("proto")
-            self.path = normpath_special(m.group("path"))
-            return True
-        m = self.socket_re.match(text)
+        m = self.file_or_socket_re.match(text)
         if m:
             self.proto = m.group("proto")
-            self.path = normpath_special(m.group("path"))
+            self.path = os.path.normpath(m.group("path"))
             return True
         m = self.local_re.match(text)
         if m:
             self.proto = "file"
-            self.path = normpath_special(m.group("path"))
+            self.path = os.path.abspath(os.path.normpath(m.group("path")))
             return True
         return False
 
@@ -587,7 +517,7 @@ class Location:
         return ", ".join(items)
 
     def to_key_filename(self):
-        name = re.sub(r"[^\w]", "_", self.path).strip("_")
+        name = re.sub(r"[^\w]", "_", self.path.rstrip("/"))
         if self.proto not in ("file", "socket", "rclone"):
             name = re.sub(r"[^\w]", "_", self.host) + "__" + name
         if len(name) > 100:
@@ -609,20 +539,17 @@ class Location:
     def canonical_path(self):
         if self.proto in ("file", "socket"):
             return self.path
-        else:
-            if self.path and self.path.startswith("~"):
-                path = "/" + self.path  # /~/x = path x relative to home dir
-            elif self.path and not self.path.startswith("/"):
-                path = "/./" + self.path  # /./x = path x relative to cwd
-            else:
-                path = self.path
-            return "{}://{}{}{}{}".format(
-                self.proto if self.proto else "???",
-                f"{self.user}@" if self.user else "",
-                self._host if self._host else "",  # needed for ipv6 addrs
-                f":{self.port}" if self.port else "",
-                path,
+        if self.proto == "rclone":
+            return f"{self.proto}:{self.path}"
+        if self.proto in ("sftp", "ssh"):
+            return (
+                f"{self.proto}://"
+                f"{(self.user + '@') if self.user else ''}"
+                f"{self._host if self._host else ''}"
+                f"{self.port if self.port else ''}/"
+                f"{self.path}"
             )
+        raise NotImplementedError(self.proto)
 
     def with_timestamp(self, timestamp):
         # note: this only affects the repository URL/path, not the archive name!

+ 2 - 6
src/borg/remote.py

@@ -361,12 +361,8 @@ class RepositoryServer:  # pragma: no cover
     def _resolve_path(self, path):
         if isinstance(path, bytes):
             path = os.fsdecode(path)
-        if path.startswith("/~/"):  # /~/x = path x relative to own home dir
-            home_dir = os.environ.get("HOME") or os.path.expanduser("~%s" % os.environ.get("USER", ""))
-            path = os.path.join(home_dir, path[3:])
-        elif path.startswith("/./"):  # /./x = path x relative to cwd
-            path = path[3:]
-        return os.path.realpath(path)
+        path = os.path.realpath(path)
+        return path
 
     def open(
         self,

+ 14 - 1
src/borg/repository.py

@@ -5,6 +5,7 @@ from borgstore.store import Store
 from borgstore.store import ObjectNotFound as StoreObjectNotFound
 from borgstore.backends.errors import BackendError as StoreBackendError
 from borgstore.backends.errors import BackendDoesNotExist as StoreBackendDoesNotExist
+from borgstore.backends.errors import BackendAlreadyExists as StoreBackendAlreadyExists
 
 from .checksums import xxh64
 from .constants import *  # NOQA
@@ -117,6 +118,7 @@ class Repository:
             url = "file://%s" % os.path.abspath(path_or_location)
             location = Location(url)
         self._location = location
+        self.url = url
         # lots of stuff in data: use 2 levels by default (data/00/00/ .. data/ff/ff/ dirs)!
         data_levels = int(os.environ.get("BORG_STORE_DATA_LEVELS", "2"))
         levels_config = {
@@ -174,13 +176,24 @@ class Repository:
 
     def create(self):
         """Create a new empty repository"""
-        self.store.create()
+        try:
+            self.store.create()
+        except StoreBackendAlreadyExists:
+            raise self.AlreadyExists(self.url)
         self.store.open()
         try:
             self.store.store("config/readme", REPOSITORY_README.encode())
             self.version = 3
             self.store.store("config/version", str(self.version).encode())
             self.store.store("config/id", bin_to_hex(os.urandom(32)).encode())
+            # we know repo/data/ still does not have any chunks stored in it,
+            # but for some stores, there might be a lot of empty directories and
+            # listing them all might be rather slow, so we better cache an empty
+            # ChunkIndex from here so that the first repo operation does not have
+            # to build the ChunkIndex the slow way by listing all the directories.
+            from borg.cache import write_chunkindex_to_repo_cache
+
+            write_chunkindex_to_repo_cache(self, ChunkIndex(), compact=True, clear=True, force_write=True)
         finally:
             self.store.close()
 

+ 0 - 1
src/borg/testsuite/archiver/checks_test.py

@@ -250,7 +250,6 @@ def test_unknown_feature_on_mount(archivers, request):
     mountpoint = os.path.join(archiver.tmpdir, "mountpoint")
     os.mkdir(mountpoint)
     # XXX this might hang if it doesn't raise an error
-    archiver.repository_location += "::test"
     cmd_raises_unknown_feature(archiver, ["mount", mountpoint])
 
 

+ 78 - 96
src/borg/testsuite/helpers_test.py

@@ -108,100 +108,93 @@ class TestLocationWithoutEnv:
     def test_ssh(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         assert (
-            repr(Location("ssh://user@host:1234/some/path"))
-            == "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path')"
+            repr(Location("ssh://user@host:1234//absolute/path"))
+            == "Location(proto='ssh', user='user', host='host', port=1234, path='/absolute/path')"
         )
-        assert Location("ssh://user@host:1234/some/path").to_key_filename() == keys_dir + "host__some_path"
+        assert Location("ssh://user@host:1234//absolute/path").to_key_filename() == keys_dir + "host___absolute_path"
         assert (
-            repr(Location("ssh://user@host:1234/some/path"))
-            == "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path')"
+            repr(Location("ssh://user@host:1234/relative/path"))
+            == "Location(proto='ssh', user='user', host='host', port=1234, path='relative/path')"
         )
+        assert Location("ssh://user@host:1234/relative/path").to_key_filename() == keys_dir + "host__relative_path"
         assert (
-            repr(Location("ssh://user@host/some/path"))
-            == "Location(proto='ssh', user='user', host='host', port=None, path='/some/path')"
+            repr(Location("ssh://user@host/relative/path"))
+            == "Location(proto='ssh', user='user', host='host', port=None, path='relative/path')"
         )
         assert (
-            repr(Location("ssh://user@[::]:1234/some/path"))
-            == "Location(proto='ssh', user='user', host='::', port=1234, path='/some/path')"
+            repr(Location("ssh://user@[::]:1234/relative/path"))
+            == "Location(proto='ssh', user='user', host='::', port=1234, path='relative/path')"
         )
+        assert Location("ssh://user@[::]:1234/relative/path").to_key_filename() == keys_dir + "____relative_path"
         assert (
-            repr(Location("ssh://user@[::]:1234/some/path"))
-            == "Location(proto='ssh', user='user', host='::', port=1234, path='/some/path')"
+            repr(Location("ssh://user@[::]/relative/path"))
+            == "Location(proto='ssh', user='user', host='::', port=None, path='relative/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')"
+            repr(Location("ssh://user@[2001:db8::]:1234/relative/path"))
+            == "Location(proto='ssh', user='user', host='2001:db8::', port=1234, path='relative/path')"
         )
         assert (
-            repr(Location("ssh://user@[2001:db8::]:1234/some/path"))
-            == "Location(proto='ssh', user='user', host='2001:db8::', port=1234, path='/some/path')"
+            Location("ssh://user@[2001:db8::]:1234/relative/path").to_key_filename()
+            == keys_dir + "2001_db8____relative_path"
         )
         assert (
-            repr(Location("ssh://user@[2001:db8::]:1234/some/path"))
-            == "Location(proto='ssh', user='user', host='2001:db8::', port=1234, path='/some/path')"
+            repr(Location("ssh://user@[2001:db8::]/relative/path"))
+            == "Location(proto='ssh', user='user', host='2001:db8::', port=None, path='relative/path')"
         )
         assert (
-            Location("ssh://user@[2001:db8::]:1234/some/path").to_key_filename() == keys_dir + "2001_db8____some_path"
+            repr(Location("ssh://user@[2001:db8::c0:ffee]:1234/relative/path"))
+            == "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=1234, path='relative/path')"
         )
         assert (
-            repr(Location("ssh://user@[2001:db8::]/some/path"))
-            == "Location(proto='ssh', user='user', host='2001:db8::', port=None, path='/some/path')"
+            repr(Location("ssh://user@[2001:db8::c0:ffee]/relative/path"))
+            == "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=None, path='relative/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')"
+            repr(Location("ssh://user@[2001:db8::192.0.2.1]:1234/relative/path"))
+            == "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=1234, path='relative/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')"
+            repr(Location("ssh://user@[2001:db8::192.0.2.1]/relative/path"))
+            == "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='relative/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')"
+            Location("ssh://user@[2001:db8::192.0.2.1]/relative/path").to_key_filename()
+            == keys_dir + "2001_db8__192_0_2_1__relative_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]: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')"
-        )
-        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"))
+            repr(Location("ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]/relative/path"))
             == "Location(proto='ssh', user='user', "
-            "host='2a02:0001:0002:0003:0004:0005:0006:0007', port=None, path='/some/path')"
+            "host='2a02:0001:0002:0003:0004:0005:0006:0007', port=None, path='relative/path')"
         )
         assert (
-            repr(Location("ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]:1234/some/path"))
+            repr(Location("ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]:1234/relative/path"))
             == "Location(proto='ssh', user='user', "
-            "host='2a02:0001:0002:0003:0004:0005:0006:0007', port=1234, path='/some/path')"
+            "host='2a02:0001:0002:0003:0004:0005:0006:0007', port=1234, path='relative/path')"
         )
 
     def test_rclone(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         assert (
-            repr(Location("rclone://remote:path"))
+            repr(Location("rclone:remote:path"))
             == "Location(proto='rclone', user=None, host=None, port=None, path='remote:path')"
         )
-        assert Location("rclone://remote:path").to_key_filename() == keys_dir + "remote_path"
+        assert Location("rclone:remote:path").to_key_filename() == keys_dir + "remote_path"
 
     def test_sftp(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
+        # relative path
+        assert (
+            repr(Location("sftp://user@host:1234/rel/path"))
+            == "Location(proto='sftp', user='user', host='host', port=1234, path='rel/path')"
+        )
+        assert Location("sftp://user@host:1234/rel/path").to_key_filename() == keys_dir + "host__rel_path"
+        # absolute path
         assert (
-            repr(Location("sftp://user@host:1234/some/path"))
-            == "Location(proto='sftp', user='user', host='host', port=1234, path='/some/path')"
+            repr(Location("sftp://user@host:1234//abs/path"))
+            == "Location(proto='sftp', user='user', host='host', port=1234, path='/abs/path')"
         )
-        assert Location("sftp://user@host:1234/some/path").to_key_filename() == keys_dir + "host__some_path"
+        assert Location("sftp://user@host:1234//abs/path").to_key_filename() == keys_dir + "host___abs_path"
 
     def test_socket(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
@@ -209,7 +202,7 @@ class TestLocationWithoutEnv:
             repr(Location("socket:///repo/path"))
             == "Location(proto='socket', user=None, host=None, port=None, path='/repo/path')"
         )
-        assert Location("socket:///some/path").to_key_filename() == keys_dir + "some_path"
+        assert Location("socket:///some/path").to_key_filename() == keys_dir + "_some_path"
 
     def test_file(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
@@ -221,7 +214,7 @@ class TestLocationWithoutEnv:
             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"
+        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)
@@ -229,55 +222,40 @@ class TestLocationWithoutEnv:
             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"
+        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")) == "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):
-        monkeypatch.delenv("BORG_REPO", raising=False)
-        assert Location(os.path.join(*(40 * ["path"]))).to_key_filename() == keys_dir + "_".join(20 * ["path"]) + "_"
+        rel_path = "path"
+        abs_path = os.path.abspath(rel_path)
+        assert repr(Location(rel_path)) == f"Location(proto='file', user=None, host=None, port=None, path='{abs_path}')"
+        assert Location("path").to_key_filename().endswith(rel_path)
 
     def test_abspath(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         assert (
-            repr(Location("/some/absolute/path"))
-            == "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path')"
+            repr(Location("/absolute/path"))
+            == "Location(proto='file', user=None, host=None, port=None, path='/absolute/path')"
         )
+        assert Location("/absolute/path").to_key_filename() == keys_dir + "_absolute_path"
         assert (
-            repr(Location("/some/absolute/path"))
-            == "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path')"
+            repr(Location("ssh://user@host//absolute/path"))
+            == "Location(proto='ssh', user='user', host='host', port=None, path='/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')"
-        )
-        assert Location("ssh://user@host/some/path").to_key_filename() == keys_dir + "host__some_path"
+        assert Location("ssh://user@host//absolute/path").to_key_filename() == keys_dir + "host___absolute_path"
 
     def test_relpath(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
+        # for a local path, borg creates a Location instance with an absolute path
+        rel_path = "relative/path"
+        abs_path = os.path.abspath(rel_path)
+        assert repr(Location(rel_path)) == f"Location(proto='file', user=None, host=None, port=None, path='{abs_path}')"
+        assert Location(rel_path).to_key_filename().endswith("relative_path")
         assert (
-            repr(Location("some/relative/path"))
-            == "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')"
-        )
-        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')"
+            repr(Location("ssh://user@host/relative/path"))
+            == "Location(proto='ssh', user='user', host='host', port=None, path='relative/path')"
         )
-        assert Location("ssh://user@host/~/some/path").to_key_filename() == keys_dir + "host__some_path"
+        assert Location("ssh://user@host/relative/path").to_key_filename() == keys_dir + "host__relative_path"
 
     def test_with_colons(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
@@ -293,18 +271,22 @@ class TestLocationWithoutEnv:
             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"
+        assert Location("/abs/path:with:colons").to_key_filename() == keys_dir + "_abs_path_with_colons"
 
     def test_canonical_path(self, monkeypatch):
         monkeypatch.delenv("BORG_REPO", raising=False)
         locations = [
-            "some/path",
-            "file://some/path",
-            "host:some/path",
-            "host:~user/some/path",
-            "socket:///some/path",
-            "ssh://host/some/path",
-            "ssh://user@host:1234/some/path",
+            "relative/path",
+            "/absolute/path",
+            "file:///absolute/path",
+            "socket:///absolute/path",
+            "ssh://host/relative/path",
+            "ssh://host//absolute/path",
+            "ssh://user@host:1234/relative/path",
+            "sftp://host/relative/path",
+            "sftp://host//absolute/path",
+            "sftp://user@host:1234/relative/path",
+            "rclone:remote:path",
         ]
         for location in locations:
             assert (

+ 1 - 1
src/borg/testsuite/legacyrepository_test.py

@@ -30,7 +30,7 @@ def repository(tmp_path):
 def remote_repository(tmp_path):
     if is_win32:
         pytest.skip("Remote repository does not yet work on Windows.")
-    repository_location = Location("ssh://__testsuite__" + os.fspath(tmp_path / "repository"))
+    repository_location = Location("ssh://__testsuite__/" + os.fspath(tmp_path / "repository"))
     yield LegacyRemoteRepository(repository_location, exclusive=True, create=True)
 
 

+ 1 - 1
src/borg/testsuite/repository_test.py

@@ -25,7 +25,7 @@ def repository(tmp_path):
 def remote_repository(tmp_path):
     if is_win32:
         pytest.skip("Remote repository does not yet work on Windows.")
-    repository_location = Location("ssh://__testsuite__" + os.fspath(tmp_path / "repository"))
+    repository_location = Location("ssh://__testsuite__/" + os.fspath(tmp_path / "repository"))
     yield RemoteRepository(repository_location, exclusive=True, create=True)