diff --git a/README.rst b/README.rst
index 2dc2a798d..bf35f0d12 100644
--- a/README.rst
+++ b/README.rst
@@ -116,6 +116,16 @@ Now doing another backup, just to show off the great deduplication::
For a graphical frontend refer to our complementary project `BorgWeb `_.
+Helping, Donations and Bounties
+-------------------------------
+
+Your help is always welcome!
+Spread the word, give feedback, help with documentation, testing or development.
+
+You can also give monetary support to the project, see there for details:
+
+https://borgbackup.readthedocs.io/en/stable/support.html#bounties-and-fundraisers
+
Links
-----
@@ -123,9 +133,8 @@ Links
* `Releases `_,
`PyPI packages `_ and
`ChangeLog `_
-* `GitHub `_,
- `Issue Tracker `_ and
- `Bounties & Fundraisers `_
+* `GitHub `_ and
+ `Issue Tracker `_.
* `Web-Chat (IRC) `_ and
`Mailing List `_
* `License `_
diff --git a/borg/archiver.py b/borg/archiver.py
index f749d2435..9aa8bde06 100644
--- a/borg/archiver.py
+++ b/borg/archiver.py
@@ -557,19 +557,26 @@ class Archiver:
logger.info("Cache deleted.")
return self.exit_code
- @with_repository()
- def do_mount(self, args, repository, manifest, key):
+ def do_mount(self, args):
"""Mount archive or an entire repository as a FUSE filesystem"""
+ # Perform these checks before opening the repository and asking for a passphrase.
+
try:
- from .fuse import FuseOperations
+ import borg.fuse
except ImportError as e:
- self.print_error('Loading fuse support failed [ImportError: %s]' % str(e))
+ self.print_error('borg mount not available: loading fuse support failed [ImportError: %s]' % str(e))
return self.exit_code
if not os.path.isdir(args.mountpoint) or not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK):
self.print_error('%s: Mountpoint must be a writable directory' % args.mountpoint)
return self.exit_code
+ return self._do_mount(args)
+
+ @with_repository()
+ def _do_mount(self, args, repository, manifest, key):
+ from .fuse import FuseOperations
+
with cache_if_remote(repository) as cached_repo:
if args.location.archive:
archive = Archive(repository, key, manifest, args.location.archive)
diff --git a/borg/fuse.py b/borg/fuse.py
index 67a7a94af..e2337178c 100644
--- a/borg/fuse.py
+++ b/borg/fuse.py
@@ -245,7 +245,7 @@ class FuseOperations(llfuse.Operations):
def getxattr(self, inode, name, ctx=None):
item = self.get_item(inode)
try:
- return item.get(b'xattrs', {})[name]
+ return item.get(b'xattrs', {})[name] or b''
except KeyError:
raise llfuse.FUSEError(llfuse.ENOATTR) from None
diff --git a/borg/helpers.py b/borg/helpers.py
index 824d8a4d5..a067b6af8 100644
--- a/borg/helpers.py
+++ b/borg/helpers.py
@@ -884,7 +884,7 @@ def bin_to_hex(binary):
class Location:
"""Object representing a repository / archive location
"""
- proto = user = host = port = path = archive = None
+ proto = user = _host = port = path = archive = None
# user must not contain "@", ":" or "/".
# Quoting adduser error message:
@@ -929,7 +929,7 @@ class Location:
ssh_re = re.compile(r"""
(?Pssh):// # ssh://
""" + optional_user_re + r""" # user@ (optional)
- (?P[^:/]+)(?::(?P\d+))? # host or host:port
+ (?P([^:/]+|\[[0-9a-fA-F:.]+\]))(?::(?P\d+))? # host or host:port or [ipv6] or [ipv6]:port
""" + abs_path_re + optional_archive_re, re.VERBOSE) # path or path::archive
file_re = re.compile(r"""
@@ -940,7 +940,7 @@ class Location:
scp_re = re.compile(r"""
(
""" + optional_user_re + r""" # user@ (optional)
- (?P[^:/]+): # host: (don't match / in host to disambiguate from file:)
+ (?P([^:/]+|\[[0-9a-fA-F:.]+\])): # host: (don't match / or [ipv6] in host to disambiguate from file:)
)? # user@host: part is optional
""" + scp_path_re + optional_archive_re, re.VERBOSE) # path with optional archive
@@ -956,7 +956,7 @@ class Location:
def __init__(self, text=''):
self.orig = text
if not self.parse(self.orig):
- raise ValueError
+ raise ValueError('Location: parse failed: %s' % self.orig)
def parse(self, text):
text = replace_placeholders(text)
@@ -986,7 +986,7 @@ class Location:
if m:
self.proto = m.group('proto')
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.path = normpath_special(m.group('path'))
self.archive = m.group('archive')
@@ -1000,10 +1000,10 @@ class Location:
m = self.scp_re.match(text)
if m:
self.user = m.group('user')
- self.host = m.group('host')
+ self._host = m.group('host')
self.path = normpath_special(m.group('path'))
self.archive = m.group('archive')
- self.proto = self.host and 'ssh' or 'file'
+ self.proto = self._host and 'ssh' or 'file'
return True
return False
@@ -1027,6 +1027,12 @@ class Location:
def __repr__(self):
return "Location(%s)" % self
+ @property
+ def host(self):
+ # strip square brackets used for IPv6 addrs
+ if self._host is not None:
+ return self._host.lstrip('[').rstrip(']')
+
def canonical_path(self):
if self.proto == 'file':
return self.path
@@ -1038,7 +1044,7 @@ class Location:
else:
path = self.path
return 'ssh://{}{}{}{}'.format('{}@'.format(self.user) if self.user else '',
- self.host,
+ self._host, # needed for ipv6 addrs
':{}'.format(self.port) if self.port else '',
path)
diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py
index 75168edf2..aa2689953 100644
--- a/borg/testsuite/archiver.py
+++ b/borg/testsuite/archiver.py
@@ -289,6 +289,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
# into "fakeroot space". Because the xattrs exposed by borgfs are these of an underlying file
# (from fakeroots point of view) they are invisible to the test process inside the fakeroot.
xattr.setxattr(os.path.join(self.input_path, 'fusexattr'), 'user.foo', b'bar')
+ xattr.setxattr(os.path.join(self.input_path, 'fusexattr'), 'user.empty', b'')
# XXX this always fails for me
# ubuntu 14.04, on a TMP dir filesystem with user_xattr, using fakeroot
# same for newer ubuntu and centos.
@@ -1159,8 +1160,10 @@ class ArchiverTestCase(ArchiverTestCaseBase):
in_fn = 'input/fusexattr'
out_fn = os.path.join(mountpoint, 'input', 'fusexattr')
if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path):
- assert no_selinux(xattr.listxattr(out_fn)) == ['user.foo', ]
+ assert sorted(no_selinux(xattr.listxattr(out_fn))) == ['user.empty', 'user.foo', ]
assert xattr.getxattr(out_fn, 'user.foo') == b'bar'
+ # Special case: getxattr returns None (not b'') when reading an empty xattr.
+ assert xattr.getxattr(out_fn, 'user.empty') is None
else:
assert xattr.listxattr(out_fn) == []
try:
diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py
index 7b9ad965d..ebeadfed1 100644
--- a/borg/testsuite/helpers.py
+++ b/borg/testsuite/helpers.py
@@ -42,6 +42,30 @@ class TestLocationWithoutEnv:
"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)"
+ assert repr(Location('ssh://user@[::]:1234/some/path::archive')) == \
+ "Location(proto='ssh', user='user', host='::', port=1234, path='/some/path', archive='archive')"
+ assert repr(Location('ssh://user@[::]:1234/some/path')) == \
+ "Location(proto='ssh', user='user', host='::', port=1234, path='/some/path', archive=None)"
+ assert repr(Location('ssh://user@[::]/some/path')) == \
+ "Location(proto='ssh', user='user', host='::', port=None, path='/some/path', archive=None)"
+ assert repr(Location('ssh://user@[2001:db8::]:1234/some/path::archive')) == \
+ "Location(proto='ssh', user='user', host='2001:db8::', port=1234, path='/some/path', archive='archive')"
+ assert repr(Location('ssh://user@[2001:db8::]:1234/some/path')) == \
+ "Location(proto='ssh', user='user', host='2001:db8::', port=1234, path='/some/path', archive=None)"
+ assert repr(Location('ssh://user@[2001:db8::]/some/path')) == \
+ "Location(proto='ssh', user='user', host='2001:db8::', port=None, path='/some/path', archive=None)"
+ assert repr(Location('ssh://user@[2001:db8::c0:ffee]:1234/some/path::archive')) == \
+ "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=1234, path='/some/path', archive='archive')"
+ 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', archive=None)"
+ 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', archive=None)"
+ assert repr(Location('ssh://user@[2001:db8::192.0.2.1]:1234/some/path::archive')) == \
+ "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=1234, path='/some/path', archive='archive')"
+ 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', archive=None)"
+ 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', archive=None)"
def test_file(self, monkeypatch):
monkeypatch.delenv('BORG_REPO', raising=False)
@@ -56,6 +80,22 @@ class TestLocationWithoutEnv:
"Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive='archive')"
assert repr(Location('user@host:/some/path')) == \
"Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)"
+ assert repr(Location('user@[::]:/some/path::archive')) == \
+ "Location(proto='ssh', user='user', host='::', port=None, path='/some/path', archive='archive')"
+ assert repr(Location('user@[::]:/some/path')) == \
+ "Location(proto='ssh', user='user', host='::', port=None, path='/some/path', archive=None)"
+ assert repr(Location('user@[2001:db8::]:/some/path::archive')) == \
+ "Location(proto='ssh', user='user', host='2001:db8::', port=None, path='/some/path', archive='archive')"
+ assert repr(Location('user@[2001:db8::]:/some/path')) == \
+ "Location(proto='ssh', user='user', host='2001:db8::', port=None, path='/some/path', archive=None)"
+ assert repr(Location('user@[2001:db8::c0:ffee]:/some/path::archive')) == \
+ "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=None, path='/some/path', archive='archive')"
+ assert repr(Location('user@[2001:db8::c0:ffee]:/some/path')) == \
+ "Location(proto='ssh', user='user', host='2001:db8::c0:ffee', port=None, path='/some/path', archive=None)"
+ assert repr(Location('user@[2001:db8::192.0.2.1]:/some/path::archive')) == \
+ "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path', archive='archive')"
+ assert repr(Location('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', archive=None)"
def test_smb(self, monkeypatch):
monkeypatch.delenv('BORG_REPO', raising=False)