]> arthur.barton.de Git - bup.git/blobdiff - lib/bup/metadata.py
Don't try to restore owner unless root; handle nonexistent owner/group.
[bup.git] / lib / bup / metadata.py
index 3a960d1e69cb056d196da4d33db4a9002ec20ff6..b41b1f94953633d1ac1431987ad81269bb0c77a2 100644 (file)
@@ -9,8 +9,8 @@ import errno, os, sys, stat, pwd, grp, struct, xattr, posix1e, re
 
 from cStringIO import StringIO
 from bup import vint
-from bup.helpers import mkdirp, log
-from bup._helpers import get_linux_file_attr, set_linux_file_attr, lutimes
+from bup.helpers import add_error, mkdirp, log, utime, lutime, lstat
+from bup._helpers import get_linux_file_attr, set_linux_file_attr
 
 # WARNING: the metadata encoding is *not* stable yet.  Caveat emptor!
 
@@ -129,11 +129,23 @@ def _clean_up_extract_path(p):
         return result
 
 
+def _normalize_ts(stamp):
+    # For the purposes of normalization, t = s + ns.
+    s = stamp[0]
+    ns = stamp[1]
+    if ns < 0 or ns >= 10**9:
+        t = (s * 10**9) + ns
+        if t == 0:
+            return (0, 0)
+        return ((t / 10**9), t % 10**9)
+    return stamp
+
+
 # These tags are currently conceptually private to Metadata, and they
 # must be unique, and must *never* be changed.
 _rec_tag_end = 0
 _rec_tag_path = 1
-_rec_tag_common = 2           # times, user, group, type, perms, etc.
+_rec_tag_common = 2           # times, owner, group, type, perms, etc.
 _rec_tag_symlink_target = 3
 _rec_tag_posix1e_acl = 4      # getfacl(1), setfacl(1), etc.
 _rec_tag_nfsv4_acl = 5        # intended to supplant posix1e acls?
@@ -141,6 +153,16 @@ _rec_tag_linux_attr = 6       # lsattr(1) chattr(1)
 _rec_tag_linux_xattr = 7      # getfattr(1) setfattr(1)
 
 
+class MetadataAcquisitionError(Exception):
+    # Thrown when unable to extract any given bit of metadata from a path.
+    pass
+
+
+class MetadataApplicationError(Exception):
+    # Thrown when unable to apply any given bit of metadata to a path.
+    pass
+
+
 class Metadata:
     # Metadata is stored as a sequence of tagged binary records.  Each
     # record will have some subset of add, encode, load, create, and
@@ -148,50 +170,68 @@ class Metadata:
 
     ## Common records
 
+    # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
+    # must be non-negative and < 10**9.
+
     def _add_common(self, path, st):
         self.mode = st.st_mode
         self.uid = st.st_uid
         self.gid = st.st_gid
+        self.rdev = st.st_rdev
         self.atime = st.st_atime
         self.mtime = st.st_mtime
         self.ctime = st.st_ctime
-        self.rdev = st.st_rdev
-        self.user = pwd.getpwuid(st.st_uid)[0]
+        self.owner = pwd.getpwuid(st.st_uid)[0]
         self.group = grp.getgrgid(st.st_gid)[0]
 
     def _encode_common(self):
-        result = vint.pack('VVsVsVVVVVVV',
+        atime = _normalize_ts(self.atime)
+        mtime = _normalize_ts(self.mtime)
+        ctime = _normalize_ts(self.ctime)
+        result = vint.pack('VVsVsVvVvVvV',
                            self.mode,
                            self.uid,
-                           self.user,
+                           self.owner,
                            self.gid,
                            self.group,
-                           int(self.atime),
-                           int(self.mtime),
-                           int(self.ctime),
-                           int(self.atime * 1e9) % 1000000000,
-                           int(self.mtime * 1e9) % 1000000000,
-                           int(self.ctime * 1e9) % 1000000000,
-                           self.rdev)
+                           self.rdev,
+                           atime[0],
+                           atime[1],
+                           mtime[0],
+                           mtime[1],
+                           ctime[0],
+                           ctime[1])
         return result
 
     def _load_common_rec(self, port):
         data = vint.read_bvec(port)
         (self.mode,
          self.uid,
-         self.user,
+         self.owner,
          self.gid,
          self.group,
-         atime_s,
-         mtime_s,
-         ctime_s,
+         self.rdev,
+         self.atime,
          atime_ns,
+         self.mtime,
          mtime_ns,
-         ctime_ns,
-         self.rdev) = vint.unpack('VVsVsVVVVVVV', data)
-        self.atime = atime_s + (atime_ns / 1e9)
-        self.mtime = mtime_s + (mtime_ns / 1e9)
-        self.ctime = ctime_s + (ctime_ns / 1e9)
+         self.ctime,
+         ctime_ns) = vint.unpack('VVsVsVvVvVvV', data)
+        self.atime = (self.atime, atime_ns)
+        self.mtime = (self.mtime, mtime_ns)
+        self.ctime = (self.ctime, ctime_ns)
+        if self.atime[1] >= 10**9:
+            path = ' for ' + self.path if self.path else ''
+            log('bup: warning - normalizing bad atime%s\n' % (path))
+            self.atime = _normalize_ts(self.atime)
+        if self.mtime[1] >= 10**9:
+            path = ' for ' + self.path if self.path else ''
+            log('bup: warning - normalizing bad mtime%s\n' % (path))
+            self.mtime = _normalize_ts(self.mtime)
+        if self.ctime[1] >= 10**9:
+            path = ' for ' + self.path if self.path else ''
+            log('bup: warning - normalizing bad ctime%s\n' % (path))
+            self.ctime = _normalize_ts(self.ctime)
 
     def _create_via_common_rec(self, path, create_symlinks=True):
         if stat.S_ISREG(self.mode):
@@ -213,9 +253,9 @@ class Metadata:
     def _apply_common_rec(self, path, restore_numeric_ids=False):
         # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
         if stat.S_ISLNK(self.mode):
-            lutimes(path, (self.atime, self.mtime))
+            lutime(path, (self.atime, self.mtime))
         else:
-            os.utime(path, (self.atime, self.mtime))
+            utime(path, (self.atime, self.mtime))
         if stat.S_ISREG(self.mode) \
                 | stat.S_ISDIR(self.mode) \
                 | stat.S_ISCHR(self.mode) \
@@ -228,11 +268,27 @@ class Metadata:
             elif not stat.S_ISLNK(self.mode):
                 os.chmod(path, 0)
 
+            # Don't try to restore owner unless we're root, and even
+            # if asked, don't try to restore the owner or group if
+            # it doesn't exist in the system db.
             uid = self.uid
             gid = self.gid
             if not restore_numeric_ids:
-                uid = pwd.getpwnam(self.user)[2]
-                gid = grp.getgrnam(self.group)[2]
+                if os.geteuid() == 0:
+                    try:
+                        uid = pwd.getpwnam(self.owner)[2]
+                    except KeyError:
+                        uid = -1
+                        log('bup: ignoring unknown owner %s for "%s"\n'
+                            % (self.owner, path))
+                else:
+                    uid = -1 # Not root; assume we can't change owner.
+                try:
+                    gid = grp.getgrnam(self.group)[2]
+                except KeyError:
+                    gid = -1
+                    log('bup: ignoring unknown group %s for "%s"\n'
+                        % (self.group, path))
             os.lchown(path, uid, gid)
 
             if _have_lchmod:
@@ -448,22 +504,28 @@ class Metadata:
         if not path:
             raise Exception('Metadata.apply_to_path() called with no path');
         num_ids = restore_numeric_ids
-        self._apply_common_rec(path, restore_numeric_ids=num_ids)
-        self._apply_posix1e_acl_rec(path, restore_numeric_ids=num_ids)
-        self._apply_linux_attr_rec(path, restore_numeric_ids=num_ids)
-        self._apply_linux_xattr_rec(path, restore_numeric_ids=num_ids)
+        try: # Later we may want to push this down and make it finer grained.
+            self._apply_common_rec(path, restore_numeric_ids=num_ids)
+            self._apply_posix1e_acl_rec(path, restore_numeric_ids=num_ids)
+            self._apply_linux_attr_rec(path, restore_numeric_ids=num_ids)
+            self._apply_linux_xattr_rec(path, restore_numeric_ids=num_ids)
+        except Exception, e:
+            raise MetadataApplicationError(str(e))
 
 
 def from_path(path, archive_path=None, save_symlinks=True):
     result = Metadata()
     result.path = archive_path
-    st = os.lstat(path)
-    result._add_common(path, st)
-    if(save_symlinks):
-        result._add_symlink_target(path, st)
-    result._add_posix1e_acl(path, st)
-    result._add_linux_attr(path, st)
-    result._add_linux_xattr(path, st)
+    st = lstat(path)
+    try: # Later we may want to push this down and make it finer grained.
+        result._add_common(path, st)
+        if(save_symlinks):
+            result._add_symlink_target(path, st)
+        result._add_posix1e_acl(path, st)
+        result._add_linux_attr(path, st)
+        result._add_linux_xattr(path, st)
+    except Exception, e:
+        raise MetadataAcquisitionError(str(e))
     return result
 
 
@@ -477,22 +539,28 @@ def save_tree(output_file, paths,
             log('bup: archiving "%s" as "%s"\n' % (p, safe_path))
 
         # Handle path itself.
-        m = from_path(p, archive_path=safe_path, save_symlinks=save_symlinks)
+        try:
+            m = from_path(p, archive_path=safe_path,
+                          save_symlinks=save_symlinks)
+        except MetadataAcquisitionError, e:
+            add_error(e)
+
         if verbose:
             print >> sys.stderr, m.path
         m.write(output_file, include_path=write_paths)
 
         if recurse and os.path.isdir(p):
-            def raise_error(x):
-                raise x
-            for root, dirs, files in os.walk(p, onerror=raise_error):
+            for root, dirs, files in os.walk(p, onerror=add_error):
                 items = files + dirs
                 for sub_path in items:
                     full_path = os.path.join(root, sub_path)
                     safe_path = _clean_up_path_for_archive(full_path)
-                    m = from_path(full_path,
-                                  archive_path=safe_path,
-                                  save_symlinks=save_symlinks)
+                    try:
+                        m = from_path(full_path,
+                                      archive_path=safe_path,
+                                      save_symlinks=save_symlinks)
+                    except MetadataAcquisitionError, e:
+                        add_error(e)
                     if verbose:
                         print >> sys.stderr, m.path
                     m.write(output_file, include_path=write_paths)
@@ -556,8 +624,11 @@ def finish_extract(file, restore_numeric_ids=False):
             else:
                 if verbose:
                     print >> sys.stderr, meta.path
-                meta.apply_to_path(path=xpath,
-                                   restore_numeric_ids=restore_numeric_ids)
+                try:
+                    meta.apply_to_path(path=xpath,
+                                       restore_numeric_ids=restore_numeric_ids)
+                except MetadataApplicationError, e:
+                    add_error(e)
 
     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
     for dir in all_dirs:
@@ -565,8 +636,11 @@ def finish_extract(file, restore_numeric_ids=False):
         xpath = _clean_up_extract_path(dir.path)
         if verbose:
             print >> sys.stderr, dir.path
-        dir.apply_to_path(path=xpath,
-                          restore_numeric_ids=restore_numeric_ids)
+        try:
+            dir.apply_to_path(path=xpath,
+                              restore_numeric_ids=restore_numeric_ids)
+        except MetadataApplicationError, e:
+            add_error(e)
 
 
 def extract(file, restore_numeric_ids=False, create_symlinks=True):
@@ -587,7 +661,10 @@ def extract(file, restore_numeric_ids=False, create_symlinks=True):
             else:
                 if verbose:
                     print >> sys.stderr, '=', meta.path
-                meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
+                try:
+                    meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
+                except MetadataApplicationError, e:
+                    add_error(e)
     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
     for dir in all_dirs:
         # Don't need to check xpath -- won't be in all_dirs if not OK.
@@ -595,5 +672,8 @@ def extract(file, restore_numeric_ids=False, create_symlinks=True):
         if verbose:
             print >> sys.stderr, '=', meta.path
         # Shouldn't have to check for risky paths here (omitted above).
-        dir.apply_to_path(path=dir.path,
-                          restore_numeric_ids=restore_numeric_ids)
+        try:
+            dir.apply_to_path(path=dir.path,
+                              restore_numeric_ids=restore_numeric_ids)
+        except MetadataApplicationError, e:
+            add_error(e)