]> arthur.barton.de Git - bup.git/blobdiff - lib/bup/metadata.py
metadata: accept EOPNOTSUPP for lchmod()
[bup.git] / lib / bup / metadata.py
index da79dc24b7bad57bca1decdf25c08f5f2128c385..83f04f771475457641c96e369d500b45be946a0b 100644 (file)
@@ -6,15 +6,13 @@
 # Public License as described in the bup LICENSE file.
 
 from __future__ import absolute_import, print_function
-from binascii import hexlify
 from copy import deepcopy
 from errno import EACCES, EINVAL, ENOTTY, ENOSYS, EOPNOTSUPP
 from io import BytesIO
 from time import gmtime, strftime
-import errno, os, sys, stat, time, pwd, grp, socket, struct
+import errno, os, sys, stat, time, socket, struct
 
-from bup import compat, vint, xstat
-from bup.compat import py_maj
+from bup import vint, xstat
 from bup.drecurse import recursive_dirlist
 from bup.helpers import add_error, mkdirp, log, is_superuser, format_filesize
 from bup.io import path_msg
@@ -40,14 +38,10 @@ if sys.platform.startswith('linux'):
             log('Warning: python-xattr module is too old; '
                 'upgrade or install python-pyxattr instead.\n')
 
-posix1e = None
-if not (sys.platform.startswith('cygwin') \
-        or sys.platform.startswith('darwin') \
-        or sys.platform.startswith('netbsd')):
-    try:
-        import posix1e
-    except ImportError:
-        log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
+try:
+    from bup._helpers import read_acl, apply_acl
+except ImportError:
+    read_acl = apply_acl = None
 
 try:
     from bup._helpers import get_linux_file_attr, set_linux_file_attr
@@ -459,8 +453,13 @@ class Metadata:
         if _have_lchmod:
             try:
                 os.lchmod(path, stat.S_IMODE(self.mode))
-            except errno.ENOSYS:  # Function not implemented
-                pass
+            except OSError as e:
+                # - "Function not implemented"
+                # - "Operation not supported" might be generated by glibc
+                if e.errno in (errno.ENOSYS, errno.EOPNOTSUPP):
+                    pass
+                else:
+                    raise
         elif not stat.S_ISLNK(self.mode):
             os.chmod(path, stat.S_IMODE(self.mode))
 
@@ -483,6 +482,9 @@ class Metadata:
         try:
             if stat.S_ISLNK(st.st_mode):
                 self.symlink_target = os.readlink(path)
+                # might have read a different link than the
+                # one that was in place when we did stat()
+                self.size = len(self.symlink_target)
         except OSError as e:
             add_error('readlink: %s' % e)
 
@@ -523,31 +525,11 @@ class Metadata:
     # The numeric/text distinction only matters when reading/restoring
     # a stored record.
     def _add_posix1e_acl(self, path, st):
-        if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
+        if not read_acl:
             return
         if not stat.S_ISLNK(st.st_mode):
-            acls = None
-            def_acls = None
-            try:
-                if posix1e.has_extended(path):
-                    acl = posix1e.ACL(file=path)
-                    acls = [acl, acl] # txt and num are the same
-                    if stat.S_ISDIR(st.st_mode):
-                        def_acl = posix1e.ACL(filedef=(path if py_maj < 3
-                                                       else path.decode('iso-8859-1')))
-                        def_acls = [def_acl, def_acl]
-            except EnvironmentError as e:
-                if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
-                    raise
-            if acls:
-                txt_flags = posix1e.TEXT_ABBREVIATE
-                num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
-                acl_rep = [acls[0].to_any_text('', b'\n', txt_flags),
-                           acls[1].to_any_text('', b'\n', num_flags)]
-                if def_acls:
-                    acl_rep.append(def_acls[0].to_any_text('', b'\n', txt_flags))
-                    acl_rep.append(def_acls[1].to_any_text('', b'\n', num_flags))
-                self.posix1e_acl = acl_rep
+            isdir = 1 if stat.S_ISDIR(st.st_mode) else 0
+            self.posix1e_acl = read_acl(path, isdir)
 
     def _same_posix1e_acl(self, other):
         """Return true or false to indicate similarity in the hardlink sense."""
@@ -570,42 +552,31 @@ class Metadata:
         self.posix1e_acl = acl_rep
 
     def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
-        def apply_acl(acl_rep, kind):
-            try:
-                acl = posix1e.ACL(text=acl_rep.decode('ascii'))
-            except IOError as e:
-                if e.errno == 0:
-                    # pylibacl appears to return an IOError with errno
-                    # set to 0 if a group referred to by the ACL rep
-                    # doesn't exist on the current system.
-                    raise ApplyError("POSIX1e ACL: can't create %r for %r"
-                                     % (acl_rep, path_msg(path)))
-                else:
-                    raise
-            try:
-                acl.applyto(path, kind)
-            except IOError as e:
-                if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
-                    raise ApplyError('POSIX1e ACL applyto: %s' % e)
-                else:
-                    raise
+        if not self.posix1e_acl:
+            return
 
-        if not posix1e:
-            if self.posix1e_acl:
-                add_error("%s: can't restore ACLs; posix1e support missing.\n"
-                          % path_msg(path))
+        if not apply_acl:
+            add_error("%s: can't restore ACLs; posix1e support missing.\n"
+                      % path_msg(path))
             return
-        if self.posix1e_acl:
+
+        try:
             acls = self.posix1e_acl
+            offs = 1 if restore_numeric_ids else 0
             if len(acls) > 2:
-                if restore_numeric_ids:
-                    apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
-                else:
-                    apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
-            if restore_numeric_ids:
-                apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
+                apply_acl(path, acls[offs], acls[offs + 2])
             else:
-                apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
+                apply_acl(path, acls[offs])
+        except IOError as e:
+            if e.errno == errno.EINVAL:
+                # libacl returns with errno set to EINVAL if a user
+                # (or group) doesn't exist
+                raise ApplyError("POSIX1e ACL: can't create %r for %r"
+                                 % (acls, path_msg(path)))
+            elif e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
+                raise ApplyError('POSIX1e ACL applyto: %s' % e)
+            else:
+                raise
 
 
     ## Linux attributes (lsattr(1), chattr(1))
@@ -817,6 +788,10 @@ class Metadata:
         return ''.join(result)
 
     def write(self, port, include_path=True):
+        port.write(self.encode(include_path=include_path))
+
+    def encode(self, include_path=True):
+        ret = []
         records = include_path and [(_rec_tag_path, self._encode_path())] or []
         records.extend([(_rec_tag_common_v3, self._encode_common()),
                         (_rec_tag_symlink_target,
@@ -828,14 +803,10 @@ class Metadata:
                         (_rec_tag_linux_xattr, self._encode_linux_xattr())])
         for tag, data in records:
             if data:
-                vint.write_vuint(port, tag)
-                vint.write_bvec(port, data)
-        vint.write_vuint(port, _rec_tag_end)
-
-    def encode(self, include_path=True):
-        port = BytesIO()
-        self.write(port, include_path)
-        return port.getvalue()
+                ret.extend((vint.encode_vuint(tag),
+                            vint.encode_bvec(data)))
+        ret.append(vint.encode_vuint(_rec_tag_end))
+        return b''.join(ret)
 
     def copy(self):
         return deepcopy(self)
@@ -919,13 +890,16 @@ class Metadata:
 
 def from_path(path, statinfo=None, archive_path=None,
               save_symlinks=True, hardlink_target=None,
-              normalized=False):
+              normalized=False, after_stat=None):
+    # This function is also a test hook; see test-save-errors
     """Return the metadata associated with the path.  When normalized is
     true, return the metadata appropriate for a typical save, which
     may or may not be all of it."""
     result = Metadata()
     result.path = archive_path
     st = statinfo or xstat.lstat(path)
+    if after_stat:
+        after_stat(path)
     result._add_common(path, st)
     if save_symlinks:
         result._add_symlink_target(path, st)