diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96f1e07a06..b8d20ded41 100644 --- a/.github/workflows/ci.yml +++ b/.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 diff --git a/docs/usage/general/repository-urls.rst.inc b/docs/usage/general/repository-urls.rst.inc index 3d7464d486..17f9560b98 100644 --- a/docs/usage/general/repository-urls.rst.inc +++ b/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. diff --git a/pyproject.toml b/pyproject.toml index 62c8e42f2d..f515195dd3 100644 --- a/pyproject.toml +++ b/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, diff --git a/scripts/msys2-install-deps b/scripts/msys2-install-deps index bf83d5510a..9f43144c53 100644 --- a/scripts/msys2-install-deps +++ b/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 diff --git a/src/borg/archiver/repo_create_cmd.py b/src/borg/archiver/repo_create_cmd.py index ccb278ab5a..ac76a44e75 100644 --- a/src/borg/archiver/repo_create_cmd.py +++ b/src/borg/archiver/repo_create_cmd.py @@ -69,6 +69,10 @@ def build_parser_repo_create(self, subparsers, common_parser, mid_common_parser) 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 ++++++++++++++++++++ diff --git a/src/borg/conftest.py b/src/borg/conftest.py index affd317ce6..4478ac0259 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 5558d52788..f995c84670 100644 --- a/src/borg/helpers/parseformat.py +++ b/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[^@:/]+)@)? - """ - - # 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([^:]|(:(?!:)))+) # 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(([^/]*)/([^:]|(:(?!:)))+)) # 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(/([^:]|(:(?!:)))+)) # start with /, then any chars, but no "::" - """ + optional_user_re = r"(?:(?P[^@:/]+)@)?" # 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""" - (?Pssh):// # ssh:// - """ - + optional_user_re - + host_re - + r""" # user@ (optional), host name or address - (?::(?P\d+))? # :port (optional) - """ - + abs_path_re, - re.VERBOSE, - ) # path + # :port (optional) + optional_port_re = r"(?::(?P\d+))?" - sftp_re = re.compile( - r""" - (?Psftp):// # sftp:// - """ - + optional_user_re - + host_re - + r""" # user@ (optional), host name or address - (?::(?P\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.+) + """ - rclone_re = re.compile( - r""" - (?Prclone):// # rclone:// - (?P(.*)) - """, - re.VERBOSE, - ) # path + # abs_path must start with a slash. + abs_path_re = r"(?P/.+)" - socket_re = re.compile( - r""" - (?Psocket):// # socket:// - """ - + abs_path_re, - re.VERBOSE, - ) # path + # path may or may not start with a slash. + abs_or_rel_path_re = r"(?P.+)" - file_re = re.compile( - r""" - (?Pfile):// # file:// - """ - + file_path_re, + # regexes for misc. kinds of supported location specifiers: + ssh_or_sftp_re = re.compile( + r"(?P(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"(?Prclone):(?P(.*))", re.VERBOSE) - win_file_re = re.compile( - r""" - (?:file://)? # optional file protocol - (?P - (?:[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(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 @@ def parse(self, text, overrides={}): 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 @@ def __str__(self): 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 @@ def host(self): 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! diff --git a/src/borg/remote.py b/src/borg/remote.py index f2e82d0ce5..27ec9f68b5 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -361,12 +361,8 @@ def negotiate(self, client_data): 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/repository.py b/src/borg/repository.py index fc671861fc..a107d477ed 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -5,6 +5,7 @@ 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 @@ def __init__( 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 @@ def id_str(self): 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() diff --git a/src/borg/testsuite/archiver/checks_test.py b/src/borg/testsuite/archiver/checks_test.py index 761093a0ed..eff167027f 100644 --- a/src/borg/testsuite/archiver/checks_test.py +++ b/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]) diff --git a/src/borg/testsuite/helpers_test.py b/src/borg/testsuite/helpers_test.py index e7ce9eea5d..102ead54b5 100644 --- a/src/borg/testsuite/helpers_test.py +++ b/src/borg/testsuite/helpers_test.py @@ -108,100 +108,93 @@ def keys_dir(self, tmpdir, monkeypatch): 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 @@ def test_socket(self, monkeypatch, keys_dir): 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 @@ def test_file(self, monkeypatch, keys_dir): 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 @@ def test_smb(self, monkeypatch, keys_dir): 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 @@ def test_with_colons(self, monkeypatch, keys_dir): 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 ( diff --git a/src/borg/testsuite/legacyrepository_test.py b/src/borg/testsuite/legacyrepository_test.py index df21d7df86..eed96a60ea 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 60e40dbf44..7a142b26f3 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)