Skip to content

Commit

Permalink
allow pathes with colons, fixes borgbackup#1705
Browse files Browse the repository at this point in the history
also:
- refactor / deduplicate the location parsing regexes
- add comments
- add more tests for Location parsing
  • Loading branch information
ThomasWaldmann committed Oct 12, 2016
1 parent 7a83bea commit cf0359e
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 17 deletions.
19 changes: 13 additions & 6 deletions borg/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,20 +768,27 @@ class Location:
"""Object representing a repository / archive location
"""
proto = user = host = port = path = archive = None
# path may 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 ":".
path_re = r'(?!:)(?P<path>([^:]|(:(?!:)))+)'
# optional ::archive_name at the end, archive name must not contain "/".
# borg mount's FUSE filesystem creates one level of directories from
# the archive names. Thus, we must not accept "/" in archive names.
# the archive names and of course "/" is not valid in a directory name.
optional_archive_re = r'(?:::(?P<archive>[^/]+))?$'
# regexes for misc. kinds of supported location specifiers:
ssh_re = re.compile(r'(?P<proto>ssh)://(?:(?P<user>[^@]+)@)?'
r'(?P<host>[^:/#]+)(?::(?P<port>\d+))?'
r'(?P<path>[^:]+)(?:::(?P<archive>[^/]+))?$')
+ path_re + optional_archive_re)
file_re = re.compile(r'(?P<proto>file)://'
r'(?P<path>[^:]+)(?:::(?P<archive>[^/]+))?$')
+ path_re + optional_archive_re)
# note: scp_re is also use for local pathes
scp_re = re.compile(r'((?:(?P<user>[^@]+)@)?(?P<host>[^:/]+):)?'
r'(?P<path>[^:]+)(?:::(?P<archive>[^/]+))?$')
# get the repo from BORG_RE env and the optional archive from param.
+ path_re + optional_archive_re)
# 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'(?:::(?P<archive>[^/]+)?)?$')
env_re = re.compile(r'(?:::$)|' + optional_archive_re)

def __init__(self, text=''):
self.orig = text
Expand Down
49 changes: 38 additions & 11 deletions borg/testsuite/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def test_ssh(self, monkeypatch):
"Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')"
assert repr(Location('ssh://user@host:1234/some/path')) == \
"Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)"
assert repr(Location('ssh://user@host/some/path')) == \
"Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)"

def test_file(self, monkeypatch):
monkeypatch.delenv('BORG_REPO', raising=False)
Expand Down Expand Up @@ -75,6 +77,15 @@ def test_relpath(self, monkeypatch):
assert repr(Location('some/relative/path')) == \
"Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)"

def test_with_colons(self, monkeypatch):
monkeypatch.delenv('BORG_REPO', raising=False)
assert repr(Location('/abs/path:w:cols::arch:col')) == \
"Location(proto='file', user=None, host=None, port=None, path='/abs/path:w:cols', archive='arch:col')"
assert repr(Location('/abs/path:with:colons::archive')) == \
"Location(proto='file', user=None, host=None, port=None, path='/abs/path:with:colons', archive='archive')"
assert repr(Location('/abs/path:with:colons')) == \
"Location(proto='file', user=None, host=None, port=None, path='/abs/path:with:colons', archive=None)"

def test_underspecified(self, monkeypatch):
monkeypatch.delenv('BORG_REPO', raising=False)
with pytest.raises(ValueError):
Expand All @@ -84,11 +95,6 @@ def test_underspecified(self, monkeypatch):
with pytest.raises(ValueError):
Location()

def test_no_double_colon(self, monkeypatch):
monkeypatch.delenv('BORG_REPO', raising=False)
with pytest.raises(ValueError):
Location('ssh://localhost:22/path:archive')

def test_no_slashes(self, monkeypatch):
monkeypatch.delenv('BORG_REPO', raising=False)
with pytest.raises(ValueError):
Expand Down Expand Up @@ -119,43 +125,64 @@ 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()) == \
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_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()) == \
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_scp(self, monkeypatch):
monkeypatch.setenv('BORG_REPO', 'user@host:/some/path')
assert repr(Location('::archive')) == \
"Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive='archive')"
assert repr(Location()) == \
assert repr(Location('::')) == \
"Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)"
assert repr(Location()) == \
"Location(proto='ssh', user='user', host='host', 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()) == \
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()) == \
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()) == \
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')
Expand Down

0 comments on commit cf0359e

Please sign in to comment.