Explorar o código

Merge pull request #2559 from enkore/bp2

Backports for 1.0.11
enkore %!s(int64=8) %!d(string=hai) anos
pai
achega
3a5ae844a1
Modificáronse 6 ficheiros con 82 adicións e 17 borrados
  1. 12 3
      README.rst
  2. 11 4
      borg/archiver.py
  3. 1 1
      borg/fuse.py
  4. 14 8
      borg/helpers.py
  5. 4 1
      borg/testsuite/archiver.py
  6. 40 0
      borg/testsuite/helpers.py

+ 12 - 3
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 <https://borgweb.readthedocs.io/>`_.
 
+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 <https://github.com/borgbackup/borg/releases>`_,
   `PyPI packages <https://pypi.python.org/pypi/borgbackup>`_ and
   `ChangeLog <https://github.com/borgbackup/borg/blob/master/docs/changes.rst>`_
-* `GitHub <https://github.com/borgbackup/borg>`_,
-  `Issue Tracker <https://github.com/borgbackup/borg/issues>`_ and
-  `Bounties & Fundraisers <https://www.bountysource.com/teams/borgbackup>`_
+* `GitHub <https://github.com/borgbackup/borg>`_ and
+  `Issue Tracker <https://github.com/borgbackup/borg/issues>`_.
 * `Web-Chat (IRC) <http://webchat.freenode.net/?randomnick=1&channels=%23borgbackup&uio=MTY9dHJ1ZSY5PXRydWUa8>`_ and
   `Mailing List <https://mail.python.org/mailman/listinfo/borgbackup>`_
 * `License <https://borgbackup.readthedocs.org/en/stable/authors.html#license>`_

+ 11 - 4
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)

+ 1 - 1
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
 

+ 14 - 8
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"""
         (?P<proto>ssh)://                                   # ssh://
         """ + optional_user_re + r"""                       # user@  (optional)
-        (?P<host>[^:/]+)(?::(?P<port>\d+))?                 # host or host:port
+        (?P<host>([^:/]+|\[[0-9a-fA-F:.]+\]))(?::(?P<port>\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>[^:/]+):                               # host: (don't match / in host to disambiguate from file:)
+            (?P<host>([^:/]+|\[[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)
 

+ 4 - 1
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:

+ 40 - 0
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)