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)