From 835b2657cd0cb634c8bcb310cf7954a009946e54 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 14 Oct 2024 19:51:44 +0200 Subject: [PATCH] new ssh: URLs, see #8372 ssh://user@host:port/rel/path ssh://user@host:port//abs/path remove the /./ and /~/ hacks. --- src/borg/conftest.py | 2 +- src/borg/helpers/parseformat.py | 56 +++------ src/borg/remote.py | 8 +- src/borg/testsuite/helpers_test.py | 128 ++++++++------------ src/borg/testsuite/legacyrepository_test.py | 2 +- src/borg/testsuite/repository_test.py | 2 +- 6 files changed, 74 insertions(+), 124 deletions(-) diff --git a/src/borg/conftest.py b/src/borg/conftest.py index affd317ce..4478ac025 100644 --- a/src/borg/conftest.py +++ b/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 diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 33a73c214..373fc8c77 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -454,9 +454,9 @@ class Location: + optional_user_re + host_re + r""" # user@ (optional), host name or address - (?::(?P\d+))? # :port (optional) + (?::(?P\d+))?/ # :port (optional) + "/" as separator """ - + abs_path_re, + + path_re, re.VERBOSE, ) # path @@ -538,19 +538,13 @@ 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) 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.sftp_re.match(text) if m: @@ -558,7 +552,7 @@ class Location: 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: @@ -568,17 +562,17 @@ class Location: m = self.file_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.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.normpath(m.group("path")) return True return False @@ -615,31 +609,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 - if self.proto == "rclone": - return f"{self.proto}:{self.path}" - elif self.proto == "sftp": - 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"{path}" - ) - else: - 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! diff --git a/src/borg/remote.py b/src/borg/remote.py index f2e82d0ce..27ec9f68b 100644 --- a/src/borg/remote.py +++ b/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, diff --git a/src/borg/testsuite/helpers_test.py b/src/borg/testsuite/helpers_test.py index b6041d004..46969a07e 100644 --- a/src/borg/testsuite/helpers_test.py +++ b/src/borg/testsuite/helpers_test.py @@ -108,83 +108,69 @@ 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/relative/path")) + == "Location(proto='ssh', user='user', host='host', port=None, path='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@[::]: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@[::]/relative/path")) + == "Location(proto='ssh', user='user', 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@[2001:db8::]:1234/relative/path")) + == "Location(proto='ssh', user='user', host='2001:db8::', port=1234, path='relative/path')" ) assert ( - repr(Location("ssh://user@[::]:1234/some/path")) - == "Location(proto='ssh', user='user', host='::', port=1234, path='/some/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')" + 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 ( - 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::c0:ffee]:1234/relative/path")) + == "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=1234, 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]/relative/path")) + == "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=None, 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::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]:1234/some/path")) - == "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=1234, 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::c0:ffee]/some/path")) - == "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=None, 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]: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): @@ -250,41 +236,28 @@ class TestLocationWithoutEnv: 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) assert ( - repr(Location("some/relative/path")) - == "Location(proto='file', user=None, host=None, port=None, path='some/relative/path')" + repr(Location("relative/path")) + == "Location(proto='file', user=None, host=None, port=None, path='relative/path')" ) + assert Location("relative/path").to_key_filename() == keys_dir + "relative_path" assert ( - repr(Location("some/relative/path")) - == "Location(proto='file', user=None, host=None, port=None, path='some/relative/path')" + repr(Location("ssh://user@host/relative/path")) + == "Location(proto='ssh', user='user', host='host', port=None, path='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')" - ) - 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) @@ -310,7 +283,8 @@ class TestLocationWithoutEnv: "host:some/path", "host:~user/some/path", "socket:///some/path", - "ssh://host/some/path", + "ssh://host/relative/path", + "ssh://host//absolute/path", "ssh://user@host:1234/some/path", ] for location in locations: diff --git a/src/borg/testsuite/legacyrepository_test.py b/src/borg/testsuite/legacyrepository_test.py index df21d7df8..eed96a60e 100644 --- a/src/borg/testsuite/legacyrepository_test.py +++ b/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) diff --git a/src/borg/testsuite/repository_test.py b/src/borg/testsuite/repository_test.py index 60e40dbf4..7a142b26f 100644 --- a/src/borg/testsuite/repository_test.py +++ b/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)