diff --git a/src/borg/archiver.py b/src/borg/archiver.py index cea035ab0..1b668f618 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -174,8 +174,10 @@ def with_repository(fake=False, invert_fake=False, create=False, lock=True, def decorator(method): @functools.wraps(method) def wrapper(self, args, **kwargs): + location = getattr(args, 'location') + if not location.valid: # location always must be given + raise Error('missing repository, please use --repo or BORG_REPO env var!') lock = getattr(args, 'lock', _lock) - location = args.location # note: 'location' must be always present in args append_only = getattr(args, 'append_only', False) storage_quota = getattr(args, 'storage_quota', None) make_parent_dirs = getattr(args, 'make_parent_dirs', False) @@ -220,8 +222,8 @@ def with_other_repository(manifest=False, key=False, cache=False, compatibility= def decorator(method): @functools.wraps(method) def wrapper(self, args, **kwargs): - location = getattr(args, 'other_location', None) - if location is None: # nothing to do + location = getattr(args, 'other_location') + if not location.valid: # nothing to do return method(self, args, **kwargs) repository = get_repository(location, create=False, exclusive=True, @@ -3203,8 +3205,9 @@ class Archiver: 'compatible file can be generated by suffixing FILE with ".pyprof".') add_common_option('--rsh', metavar='RSH', dest='rsh', help="Use this command to connect to the 'borg serve' process (default: 'ssh')") - add_common_option('--repo', metavar='REPO', dest='location', type=location_validator(), - help="repository to use") # XXXYYY + add_common_option('--repo', metavar='REPO', dest='location', + type=location_validator(other=False), default=Location(other=False), + help="repository to use") def define_exclude_and_patterns(add_option, *, tag_files=False, strip_components=False): add_option('-e', '--exclude', metavar='PATTERN', dest='patterns', @@ -4165,7 +4168,7 @@ class Archiver: subparser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true', help='do not change repository, just check') subparser.add_argument('--other-repo', metavar='SRC_REPOSITORY', dest='other_location', - type=location_validator(other=True), + type=location_validator(other=True), default=Location(other=True), help='source repository') define_archive_filters_group(subparser) @@ -4502,7 +4505,7 @@ class Archiver: help='initialize empty repository') subparser.set_defaults(func=self.do_init) subparser.add_argument('--other-repo', metavar='SRC_REPOSITORY', dest='other_location', - type=location_validator(other=True), + type=location_validator(other=True), default=Location(other=True), help='reuse the key material from the other repository') subparser.add_argument('-e', '--encryption', metavar='MODE', dest='encryption', required=True, choices=key_argument_names(), diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index e4b54f0c8..7c25437b0 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -365,15 +365,6 @@ class Location: local_re = re.compile( local_path_re + optional_archive_re, re.VERBOSE) # local path with optional archive - # get the repo from BORG_REPO env and the optional archive from param. - # if the syntax requires giving REPOSITORY (see "borg mount"), - # use "::" to let it use the env var. - # if REPOSITORY argument is optional, it'll automatically use the env. - env_re = re.compile(r""" # the repo part is fetched from BORG_REPO - (?:::$) # just "::" is ok (when a pos. arg is required, no archive) - | # or - """ + optional_archive_re, re.VERBOSE) # archive name (optional, may be empty) - win_file_re = re.compile(r""" (?:file://)? # optional file protocol (?P @@ -384,27 +375,29 @@ class Location: def __init__(self, text='', overrides={}, other=False): self.repo_env_var = 'BORG_OTHER_REPO' if other else 'BORG_REPO' - if not self.parse(text, overrides): - raise ValueError('Invalid location format: "%s"' % self.processed) + self.valid = False + self.proto = None + self.user = None + self._host = None + self.port = None + self.path = None + self.archive = None + self.parse(text, overrides) def parse(self, text, overrides={}): + if not text: + # we did not get a text to parse, so we try to fetch from the environment + text = os.environ.get(self.repo_env_var) + if text is None: + return + self.raw = text # as given by user, might contain placeholders - self.processed = text = replace_placeholders(text, overrides) # after placeholder replacement - valid = self._parse(text) + self.processed = replace_placeholders(self.raw, overrides) # after placeholder replacement + valid = self._parse(self.processed) if valid: - return True - m = self.env_re.match(text) - if not m: - return False - repo_raw = os.environ.get(self.repo_env_var) - if repo_raw is None: - return False - repo = replace_placeholders(repo_raw, overrides) - valid = self._parse(repo) - self.archive = m.group('archive') - self.raw = repo_raw if not self.archive else repo_raw + self.raw - self.processed = repo if not self.archive else f'{repo}::{self.archive}' - return valid + self.valid = True + else: + raise ValueError('Invalid location format: "%s"' % self.processed) def _parse(self, text): def normpath_special(p): diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index fd0414116..81469a133 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -166,15 +166,6 @@ class TestLocationWithoutEnv: assert repr(Location('path::archive-{utcnow}').with_timestamp(datetime(2002, 9, 19, tzinfo=timezone.utc))) == \ "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive-2002-09-19T00:00:00')" - def test_underspecified(self, monkeypatch): - monkeypatch.delenv('BORG_REPO', raising=False) - with pytest.raises(ValueError): - Location('::archive') - with pytest.raises(ValueError): - Location('::') - with pytest.raises(ValueError): - Location() - def test_no_slashes(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) with pytest.raises(ValueError): @@ -213,77 +204,6 @@ class TestLocationWithoutEnv: assert loc_without_archive.processed == "ssh://user@host:1234/repos/%s" % hostname -class TestLocationWithEnv: - def test_ssh(self, monkeypatch): - monkeypatch.setenv('BORG_REPO', 'ssh://user@host:1234/some/path') - assert repr(Location('::archive')) == \ - "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')" - assert repr(Location('::')) == \ - "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)" - assert repr(Location()) == \ - "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)" - - def test_ssh_placeholder(self, monkeypatch): - from borg.platform import hostname - monkeypatch.setenv('BORG_REPO', 'ssh://user@host:1234/{hostname}') - assert repr(Location('::archive')) == \ - f"Location(proto='ssh', user='user', host='host', port=1234, path='/{hostname}', archive='archive')" - assert repr(Location('::')) == \ - f"Location(proto='ssh', user='user', host='host', port=1234, path='/{hostname}', archive=None)" - assert repr(Location()) == \ - f"Location(proto='ssh', user='user', host='host', port=1234, path='/{hostname}', archive=None)" - - def test_file(self, monkeypatch): - monkeypatch.setenv('BORG_REPO', 'file:///some/path') - assert repr(Location('::archive')) == \ - "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive')" - assert repr(Location('::')) == \ - "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive=None)" - assert repr(Location()) == \ - "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive=None)" - - def test_folder(self, monkeypatch): - monkeypatch.setenv('BORG_REPO', 'path') - assert repr(Location('::archive')) == \ - "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive')" - assert repr(Location('::')) == \ - "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)" - assert repr(Location()) == \ - "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)" - - def test_abspath(self, monkeypatch): - monkeypatch.setenv('BORG_REPO', '/some/absolute/path') - assert repr(Location('::archive')) == \ - "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive='archive')" - assert repr(Location('::')) == \ - "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)" - assert repr(Location()) == \ - "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)" - - def test_relpath(self, monkeypatch): - monkeypatch.setenv('BORG_REPO', 'some/relative/path') - assert repr(Location('::archive')) == \ - "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive='archive')" - assert repr(Location('::')) == \ - "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)" - assert repr(Location()) == \ - "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)" - - def test_with_colons(self, monkeypatch): - monkeypatch.setenv('BORG_REPO', '/abs/path:w:cols') - assert repr(Location('::arch:col')) == \ - "Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols', archive='arch:col')" - assert repr(Location('::')) == \ - "Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols', archive=None)" - assert repr(Location()) == \ - "Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols', archive=None)" - - def test_no_slashes(self, monkeypatch): - monkeypatch.setenv('BORG_REPO', '/some/absolute/path') - with pytest.raises(ValueError): - Location('::archive_name_with/slashes/is_invalid') - - class FormatTimedeltaTestCase(BaseTestCase): def test(self):