Просмотр исходного кода

Merge pull request #8472 from ThomasWaldmann/borgstore010

changes needed for borgstore 0.1.0
TW 7 месяцев назад
Родитель
Сommit
dfbd3b7d5f

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

@@ -196,7 +196,7 @@ jobs:
 
 
   windows:
   windows:
 
 
-    if: true  # build enabled
+    if: false  # build temporary disabled
     runs-on: windows-latest
     runs-on: windows-latest
     timeout-minutes: 120
     timeout-minutes: 120
     needs: linux
     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:
 **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:
 **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.
 For ssh and sftp URLs, the ``user@`` and ``:port`` parts are optional.
 
 
 **Remote repositories** accessed via rclone:
 **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
 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
 Then just leave away the ``--repo`` option if you want
 to use the default - it will be read from BORG_REPO then.
 to use the default - it will be read from BORG_REPO then.

+ 1 - 1
pyproject.toml

@@ -30,7 +30,7 @@ classifiers = [
 ]
 ]
 license = {text="BSD"}
 license = {text="BSD"}
 dependencies = [
 dependencies = [
-  "borgstore ~= 0.0.4",
+  "borgstore ~= 0.1.0",
   "msgpack >=1.0.3, <=1.1.0",
   "msgpack >=1.0.3, <=1.1.0",
   "packaging",
   "packaging",
   "platformdirs >=3.0.0, <5.0.0; sys_platform == 'darwin'",  # for macOS: breaking changes in 3.0.0,
   "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
         This command creates a new, empty repository. A repository is a ``borgstore`` store
         containing the deduplicated data from zero or more archives.
         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
         Encryption mode TLDR
         ++++++++++++++++++++
         ++++++++++++++++++++
 
 

+ 1 - 1
src/borg/conftest.py

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

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

@@ -401,6 +401,7 @@ def parse_stringified_list(s):
 class Location:
 class Location:
     """Object representing a repository location"""
     """Object representing a repository location"""
 
 
+    # user@ (optional)
     # user must not contain "@", ":" or "/".
     # user must not contain "@", ":" or "/".
     # Quoting adduser error message:
     # Quoting adduser error message:
     # "To avoid problems, the username should consist only of letters, digits,
     # "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)."
     # (as defined by IEEE Std 1003.1-2001)."
     # We use "@" as separator between username and hostname, so we must
     # We use "@" as separator between username and hostname, so we must
     # disallow it within the pure username part.
     # 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 NAME, or host IP ADDRESS (v4 or v6, v6 must be in square brackets)
     host_re = r"""
     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,
         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):
     def __init__(self, text="", overrides={}, other=False):
         self.repo_env_var = "BORG_OTHER_REPO" if other else "BORG_REPO"
         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)
             raise ValueError('Invalid location format: "%s"' % self.processed)
 
 
     def _parse(self, text):
     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:
         if m:
             self.proto = m.group("proto")
             self.proto = m.group("proto")
             self.user = m.group("user")
             self.user = m.group("user")
             self._host = m.group("host")
             self._host = m.group("host")
             self.port = m.group("port") and int(m.group("port")) or None
             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
             return True
         m = self.rclone_re.match(text)
         m = self.rclone_re.match(text)
         if m:
         if m:
             self.proto = m.group("proto")
             self.proto = m.group("proto")
             self.path = m.group("path")
             self.path = m.group("path")
             return True
             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:
         if m:
             self.proto = m.group("proto")
             self.proto = m.group("proto")
-            self.path = normpath_special(m.group("path"))
+            self.path = os.path.normpath(m.group("path"))
             return True
             return True
         m = self.local_re.match(text)
         m = self.local_re.match(text)
         if m:
         if m:
             self.proto = "file"
             self.proto = "file"
-            self.path = normpath_special(m.group("path"))
+            self.path = os.path.abspath(os.path.normpath(m.group("path")))
             return True
             return True
         return False
         return False
 
 
@@ -587,7 +517,7 @@ class Location:
         return ", ".join(items)
         return ", ".join(items)
 
 
     def to_key_filename(self):
     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"):
         if self.proto not in ("file", "socket", "rclone"):
             name = re.sub(r"[^\w]", "_", self.host) + "__" + name
             name = re.sub(r"[^\w]", "_", self.host) + "__" + name
         if len(name) > 100:
         if len(name) > 100:
@@ -609,20 +539,17 @@ class Location:
     def canonical_path(self):
     def canonical_path(self):
         if self.proto in ("file", "socket"):
         if self.proto in ("file", "socket"):
             return self.path
             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):
     def with_timestamp(self, timestamp):
         # note: this only affects the repository URL/path, not the archive name!
         # 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):
     def _resolve_path(self, path):
         if isinstance(path, bytes):
         if isinstance(path, bytes):
             path = os.fsdecode(path)
             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(
     def open(
         self,
         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.store import ObjectNotFound as StoreObjectNotFound
 from borgstore.backends.errors import BackendError as StoreBackendError
 from borgstore.backends.errors import BackendError as StoreBackendError
 from borgstore.backends.errors import BackendDoesNotExist as StoreBackendDoesNotExist
 from borgstore.backends.errors import BackendDoesNotExist as StoreBackendDoesNotExist
+from borgstore.backends.errors import BackendAlreadyExists as StoreBackendAlreadyExists
 
 
 from .checksums import xxh64
 from .checksums import xxh64
 from .constants import *  # NOQA
 from .constants import *  # NOQA
@@ -117,6 +118,7 @@ class Repository:
             url = "file://%s" % os.path.abspath(path_or_location)
             url = "file://%s" % os.path.abspath(path_or_location)
             location = Location(url)
             location = Location(url)
         self._location = location
         self._location = location
+        self.url = url
         # lots of stuff in data: use 2 levels by default (data/00/00/ .. data/ff/ff/ dirs)!
         # 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"))
         data_levels = int(os.environ.get("BORG_STORE_DATA_LEVELS", "2"))
         levels_config = {
         levels_config = {
@@ -174,13 +176,24 @@ class Repository:
 
 
     def create(self):
     def create(self):
         """Create a new empty repository"""
         """Create a new empty repository"""
-        self.store.create()
+        try:
+            self.store.create()
+        except StoreBackendAlreadyExists:
+            raise self.AlreadyExists(self.url)
         self.store.open()
         self.store.open()
         try:
         try:
             self.store.store("config/readme", REPOSITORY_README.encode())
             self.store.store("config/readme", REPOSITORY_README.encode())
             self.version = 3
             self.version = 3
             self.store.store("config/version", str(self.version).encode())
             self.store.store("config/version", str(self.version).encode())
             self.store.store("config/id", bin_to_hex(os.urandom(32)).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:
         finally:
             self.store.close()
             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")
     mountpoint = os.path.join(archiver.tmpdir, "mountpoint")
     os.mkdir(mountpoint)
     os.mkdir(mountpoint)
     # XXX this might hang if it doesn't raise an error
     # XXX this might hang if it doesn't raise an error
-    archiver.repository_location += "::test"
     cmd_raises_unknown_feature(archiver, ["mount", mountpoint])
     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):
     def test_ssh(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         monkeypatch.delenv("BORG_REPO", raising=False)
         assert (
         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 (
         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 (
         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 (
         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 (
         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 (
         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 (
         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 (
         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 (
         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 (
         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 (
         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 (
         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 (
         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 (
         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', "
             == "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 (
         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', "
             == "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):
     def test_rclone(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         monkeypatch.delenv("BORG_REPO", raising=False)
         assert (
         assert (
-            repr(Location("rclone://remote:path"))
+            repr(Location("rclone:remote:path"))
             == "Location(proto='rclone', user=None, host=None, port=None, path='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):
     def test_sftp(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         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 (
         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):
     def test_socket(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         monkeypatch.delenv("BORG_REPO", raising=False)
@@ -209,7 +202,7 @@ class TestLocationWithoutEnv:
             repr(Location("socket:///repo/path"))
             repr(Location("socket:///repo/path"))
             == "Location(proto='socket', user=None, host=None, port=None, path='/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):
     def test_file(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         monkeypatch.delenv("BORG_REPO", raising=False)
@@ -221,7 +214,7 @@ class TestLocationWithoutEnv:
             repr(Location("file:///some/path"))
             repr(Location("file:///some/path"))
             == "Location(proto='file', user=None, host=None, port=None, path='/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):
     def test_smb(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         monkeypatch.delenv("BORG_REPO", raising=False)
@@ -229,55 +222,40 @@ class TestLocationWithoutEnv:
             repr(Location("file:////server/share/path"))
             repr(Location("file:////server/share/path"))
             == "Location(proto='file', user=None, host=None, port=None, path='//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):
     def test_folder(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         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):
     def test_abspath(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         monkeypatch.delenv("BORG_REPO", raising=False)
         assert (
         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 (
         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):
     def test_relpath(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         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 (
         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):
     def test_with_colons(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         monkeypatch.delenv("BORG_REPO", raising=False)
@@ -293,18 +271,22 @@ class TestLocationWithoutEnv:
             repr(Location("/abs/path:with:colons"))
             repr(Location("/abs/path:with:colons"))
             == "Location(proto='file', user=None, host=None, port=None, path='/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):
     def test_canonical_path(self, monkeypatch):
         monkeypatch.delenv("BORG_REPO", raising=False)
         monkeypatch.delenv("BORG_REPO", raising=False)
         locations = [
         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:
         for location in locations:
             assert (
             assert (

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

@@ -30,7 +30,7 @@ def repository(tmp_path):
 def remote_repository(tmp_path):
 def remote_repository(tmp_path):
     if is_win32:
     if is_win32:
         pytest.skip("Remote repository does not yet work on Windows.")
         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)
     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):
 def remote_repository(tmp_path):
     if is_win32:
     if is_win32:
         pytest.skip("Remote repository does not yet work on Windows.")
         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)
     yield RemoteRepository(repository_location, exclusive=True, create=True)