1 """Metadata read/write support for bup."""
3 # Copyright (C) 2010 Rob Browning
5 # This code is covered under the terms of the GNU Library General
6 # Public License as described in the bup LICENSE file.
8 from __future__ import absolute_import, print_function
9 from binascii import hexlify
10 from copy import deepcopy
11 from errno import EACCES, EINVAL, ENOTTY, ENOSYS, EOPNOTSUPP
12 from io import BytesIO
13 from time import gmtime, strftime
14 import errno, os, sys, stat, time, pwd, grp, socket, struct
16 from bup import vint, xstat
17 from bup.drecurse import recursive_dirlist
18 from bup.helpers import add_error, mkdirp, log, is_superuser, format_filesize
19 from bup.io import path_msg
20 from bup.pwdgrp import pwd_from_uid, pwd_from_name, grp_from_gid, grp_from_name
21 from bup.xstat import utime, lutime
24 if sys.platform.startswith('linux'):
25 # prefer python-pyxattr (it's a lot faster), but fall back to python-xattr
26 # as the two are incompatible and only one can be installed on a system
30 log('Warning: Linux xattr support missing; install python-pyxattr.\n')
31 if xattr and getattr(xattr, 'get_all', None) is None:
33 from xattr import pyxattr_compat as xattr
34 if not isinstance(xattr.NS_USER, bytes):
39 log('Warning: python-xattr module is too old; '
40 'upgrade or install python-pyxattr instead.\n')
43 if not (sys.platform.startswith('cygwin') \
44 or sys.platform.startswith('darwin') \
45 or sys.platform.startswith('netbsd')):
49 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
52 from bup._helpers import get_linux_file_attr, set_linux_file_attr
54 # No need for a warning here; the only reason they won't exist is that we're
55 # not on Linux, in which case files don't have any linux attrs anyway, so
56 # lacking the functions isn't a problem.
57 get_linux_file_attr = set_linux_file_attr = None
60 # See the bup_get_linux_file_attr() comments.
61 _suppress_linux_file_attr = \
62 sys.byteorder == 'big' and struct.calcsize('@l') > struct.calcsize('@i')
64 def check_linux_file_attr_api():
65 global get_linux_file_attr, set_linux_file_attr
66 if not (get_linux_file_attr or set_linux_file_attr):
68 if _suppress_linux_file_attr:
69 log('Warning: Linux attr support disabled (see "bup help index").\n')
70 get_linux_file_attr = set_linux_file_attr = None
73 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
75 # Q: Consider hardlink support?
76 # Q: Is it OK to store raw linux attr (chattr) flags?
77 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
78 # Q: Is the application of posix1e has_extended() correct?
79 # Q: Is one global --numeric-ids argument sufficient?
80 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
81 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
83 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
84 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
85 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
86 # FIXME: Consider pack('vvvvsss', ...) optimization.
90 # osx (varies between hfs and hfs+):
91 # type - regular dir char block fifo socket ...
92 # perms - rwxrwxrwxsgt
93 # times - ctime atime mtime
96 # hard-link-info (hfs+ only)
99 # attributes-osx see chflags
105 # type - regular dir ...
106 # times - creation, modification, posix change, access
109 # attributes - see attrib
111 # forks (alternate data streams)
115 # type - regular dir ...
116 # perms - rwxrwxrwx (maybe - see wikipedia)
117 # times - creation, modification, access
118 # attributes - see attrib
122 _have_lchmod = hasattr(os, 'lchmod')
125 def _clean_up_path_for_archive(p):
126 # Not the most efficient approach.
129 # Take everything after any '/../'.
130 pos = result.rfind(b'/../')
132 result = result[result.rfind(b'/../') + 4:]
134 # Take everything after any remaining '../'.
135 if result.startswith(b"../"):
138 # Remove any '/./' sequences.
139 pos = result.find(b'/./')
141 result = result[0:pos] + b'/' + result[pos + 3:]
142 pos = result.find(b'/./')
144 # Remove any leading '/'s.
145 result = result.lstrip(b'/')
147 # Replace '//' with '/' everywhere.
148 pos = result.find(b'//')
150 result = result[0:pos] + b'/' + result[pos + 2:]
151 pos = result.find(b'//')
153 # Take everything after any remaining './'.
154 if result.startswith(b'./'):
157 # Take everything before any remaining '/.'.
158 if result.endswith(b'/.'):
161 if result == b'' or result.endswith(b'/..'):
168 if p.startswith(b'/'):
170 if p.find(b'/../') != -1:
172 if p.startswith(b'../'):
174 if p.endswith(b'/..'):
179 def _clean_up_extract_path(p):
180 result = p.lstrip(b'/')
183 elif _risky_path(result):
189 # These tags are currently conceptually private to Metadata, and they
190 # must be unique, and must *never* be changed.
193 _rec_tag_common_v1 = 2 # times, user, group, type, perms, etc. (legacy/broken)
194 _rec_tag_symlink_target = 3
195 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
196 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
197 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
198 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
199 _rec_tag_hardlink_target = 8 # hard link target path
200 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
201 _rec_tag_common_v3 = 10 # adds optional size to v2
203 _warned_about_attr_einval = None
206 class ApplyError(Exception):
207 # Thrown when unable to apply any given bit of metadata to a path.
212 # Metadata is stored as a sequence of tagged binary records. Each
213 # record will have some subset of add, encode, load, create, and
214 # apply methods, i.e. _add_foo...
216 # We do allow an "empty" object as a special case, i.e. no
217 # records. One can be created by trying to write Metadata(), and
218 # for such an object, read() will return None. This is used by
219 # "bup save", for example, as a placeholder in cases where
222 # NOTE: if any relevant fields are added or removed, be sure to
223 # update same_file() below.
227 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
228 # must be non-negative and < 10**9.
230 def _add_common(self, path, st):
231 assert(st.st_uid >= 0)
232 assert(st.st_gid >= 0)
233 self.size = st.st_size
236 self.atime = st.st_atime
237 self.mtime = st.st_mtime
238 self.ctime = st.st_ctime
239 self.user = self.group = b''
240 entry = pwd_from_uid(st.st_uid)
242 self.user = entry.pw_name
243 entry = grp_from_gid(st.st_gid)
245 self.group = entry.gr_name
246 self.mode = st.st_mode
247 # Only collect st_rdev if we might need it for a mknod()
248 # during restore. On some platforms (i.e. kFreeBSD), it isn't
249 # stable for other file types. For example "cp -a" will
250 # change it for a plain file.
251 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
252 self.rdev = st.st_rdev
256 def _same_common(self, other):
257 """Return true or false to indicate similarity in the hardlink sense."""
258 return self.uid == other.uid \
259 and self.gid == other.gid \
260 and self.rdev == other.rdev \
261 and self.mtime == other.mtime \
262 and self.ctime == other.ctime \
263 and self.user == other.user \
264 and self.group == other.group \
265 and self.size == other.size
267 def _encode_common(self):
270 atime = xstat.nsecs_to_timespec(self.atime)
271 mtime = xstat.nsecs_to_timespec(self.mtime)
272 ctime = xstat.nsecs_to_timespec(self.ctime)
273 result = vint.pack('vvsvsvvVvVvVv',
286 self.size if self.size is not None else -1)
289 def _load_common_rec(self, port, version=3):
291 # Added trailing size to v2, negative when None.
292 unpack_fmt = 'vvsvsvvVvVvVv'
294 unpack_fmt = 'vvsvsvvVvVvV'
296 unpack_fmt = 'VVsVsVvVvVvV'
298 raise Exception('unexpected common_rec version %d' % version)
299 data = vint.read_bvec(port)
300 values = vint.unpack(unpack_fmt, data)
302 (self.mode, self.uid, self.user, self.gid, self.group,
304 self.atime, atime_ns,
305 self.mtime, mtime_ns,
306 self.ctime, ctime_ns, size) = values
310 (self.mode, self.uid, self.user, self.gid, self.group,
312 self.atime, atime_ns,
313 self.mtime, mtime_ns,
314 self.ctime, ctime_ns) = values
315 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
316 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
317 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
319 def _recognized_file_type(self):
320 return stat.S_ISREG(self.mode) \
321 or stat.S_ISDIR(self.mode) \
322 or stat.S_ISCHR(self.mode) \
323 or stat.S_ISBLK(self.mode) \
324 or stat.S_ISFIFO(self.mode) \
325 or stat.S_ISSOCK(self.mode) \
326 or stat.S_ISLNK(self.mode)
328 def _create_via_common_rec(self, path, create_symlinks=True):
330 raise ApplyError('no metadata - cannot create path '
333 # If the path already exists and is a dir, try rmdir.
334 # If the path already exists and is anything else, try unlink.
337 st = xstat.lstat(path)
339 if e.errno != errno.ENOENT:
342 if stat.S_ISDIR(st.st_mode):
346 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
347 raise Exception('refusing to overwrite non-empty dir '
353 if stat.S_ISREG(self.mode):
354 assert(self._recognized_file_type())
355 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0o600)
357 elif stat.S_ISDIR(self.mode):
358 assert(self._recognized_file_type())
359 os.mkdir(path, 0o700)
360 elif stat.S_ISCHR(self.mode):
361 assert(self._recognized_file_type())
362 os.mknod(path, 0o600 | stat.S_IFCHR, self.rdev)
363 elif stat.S_ISBLK(self.mode):
364 assert(self._recognized_file_type())
365 os.mknod(path, 0o600 | stat.S_IFBLK, self.rdev)
366 elif stat.S_ISFIFO(self.mode):
367 assert(self._recognized_file_type())
368 os.mkfifo(path, 0o600 | stat.S_IFIFO)
369 elif stat.S_ISSOCK(self.mode):
371 os.mknod(path, 0o600 | stat.S_IFSOCK)
373 if e.errno in (errno.EINVAL, errno.EPERM):
374 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
378 elif stat.S_ISLNK(self.mode):
379 assert(self._recognized_file_type())
380 if self.symlink_target and create_symlinks:
381 # on MacOS, symlink() permissions depend on umask, and there's
382 # no way to chown a symlink after creating it, so we have to
384 oldumask = os.umask((self.mode & 0o777) ^ 0o777)
386 os.symlink(self.symlink_target, path)
389 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
391 assert(not self._recognized_file_type())
392 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
393 % (path_msg(path), self.mode))
395 def _apply_common_rec(self, path, restore_numeric_ids=False):
397 raise ApplyError('no metadata - cannot apply to ' + path_msg(path))
399 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
400 # EACCES errors at this stage are fatal for the current path.
401 if lutime and stat.S_ISLNK(self.mode):
403 lutime(path, (self.atime, self.mtime))
405 if e.errno == errno.EACCES:
406 raise ApplyError('lutime: %s' % e)
411 utime(path, (self.atime, self.mtime))
413 if e.errno == errno.EACCES:
414 raise ApplyError('utime: %s' % e)
418 uid = gid = -1 # By default, do nothing.
422 if not restore_numeric_ids:
423 if self.uid != 0 and self.user:
424 entry = pwd_from_name(self.user)
427 if self.gid != 0 and self.group:
428 entry = grp_from_name(self.group)
431 else: # not superuser - only consider changing the group/gid
432 user_gids = os.getgroups()
433 if self.gid in user_gids:
435 if not restore_numeric_ids and self.gid != 0:
436 # The grp might not exist on the local system.
437 grps = filter(None, [grp_from_gid(x) for x in user_gids])
438 if self.group in [x.gr_name for x in grps]:
439 g = grp_from_name(self.group)
443 if uid != -1 or gid != -1:
445 os.lchown(path, uid, gid)
447 if e.errno == errno.EPERM:
448 add_error('lchown: %s' % e)
449 elif sys.platform.startswith('cygwin') \
450 and e.errno == errno.EINVAL:
451 add_error('lchown: unknown uid/gid (%d/%d) for %s'
452 % (uid, gid, path_msg(path)))
458 os.lchmod(path, stat.S_IMODE(self.mode))
459 except errno.ENOSYS: # Function not implemented
461 elif not stat.S_ISLNK(self.mode):
462 os.chmod(path, stat.S_IMODE(self.mode))
467 def _encode_path(self):
469 return vint.pack('s', self.path)
473 def _load_path_rec(self, port):
474 self.path = vint.unpack('s', vint.read_bvec(port))[0]
479 def _add_symlink_target(self, path, st):
481 if stat.S_ISLNK(st.st_mode):
482 self.symlink_target = os.readlink(path)
484 add_error('readlink: %s' % e)
486 def _encode_symlink_target(self):
487 return self.symlink_target
489 def _load_symlink_target_rec(self, port):
490 target = vint.read_bvec(port)
491 self.symlink_target = target
492 if self.size is None:
493 self.size = len(target)
495 assert(self.size == len(target))
500 def _add_hardlink_target(self, target):
501 self.hardlink_target = target
503 def _same_hardlink_target(self, other):
504 """Return true or false to indicate similarity in the hardlink sense."""
505 return self.hardlink_target == other.hardlink_target
507 def _encode_hardlink_target(self):
508 return self.hardlink_target
510 def _load_hardlink_target_rec(self, port):
511 self.hardlink_target = vint.read_bvec(port)
514 ## POSIX1e ACL records
516 # Recorded as a list:
517 # [txt_id_acl, num_id_acl]
518 # or, if a directory:
519 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
520 # The numeric/text distinction only matters when reading/restoring
522 def _add_posix1e_acl(self, path, st):
523 if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
525 if not stat.S_ISLNK(st.st_mode):
529 if posix1e.has_extended(path):
530 acl = posix1e.ACL(file=path)
531 acls = [acl, acl] # txt and num are the same
532 if stat.S_ISDIR(st.st_mode):
533 def_acl = posix1e.ACL(filedef=path)
534 def_acls = [def_acl, def_acl]
535 except EnvironmentError as e:
536 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
539 txt_flags = posix1e.TEXT_ABBREVIATE
540 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
541 acl_rep = [acls[0].to_any_text(b'', b'\n', txt_flags),
542 acls[1].to_any_text(b'', b'\n', num_flags)]
544 acl_rep.append(def_acls[0].to_any_text(b'', b'\n', txt_flags))
545 acl_rep.append(def_acls[1].to_any_text(b'', b'\n', num_flags))
546 self.posix1e_acl = acl_rep
548 def _same_posix1e_acl(self, other):
549 """Return true or false to indicate similarity in the hardlink sense."""
550 return self.posix1e_acl == other.posix1e_acl
552 def _encode_posix1e_acl(self):
553 # Encode as two strings (w/default ACL string possibly empty).
555 acls = self.posix1e_acl
557 acls.extend([b'', b''])
558 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
562 def _load_posix1e_acl_rec(self, port):
563 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
564 if acl_rep[2] == b'':
565 acl_rep = acl_rep[:2]
566 self.posix1e_acl = acl_rep
568 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
569 def apply_acl(acl_rep, kind):
571 acl = posix1e.ACL(text = acl_rep)
574 # pylibacl appears to return an IOError with errno
575 # set to 0 if a group referred to by the ACL rep
576 # doesn't exist on the current system.
577 raise ApplyError("POSIX1e ACL: can't create %r for %r"
578 % (acl_rep, path_msg(path)))
582 acl.applyto(path, kind)
584 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
585 raise ApplyError('POSIX1e ACL applyto: %s' % e)
591 add_error("%s: can't restore ACLs; posix1e support missing.\n"
595 acls = self.posix1e_acl
597 if restore_numeric_ids:
598 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
600 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
601 if restore_numeric_ids:
602 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
604 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
607 ## Linux attributes (lsattr(1), chattr(1))
609 def _add_linux_attr(self, path, st):
610 check_linux_file_attr_api()
611 if not get_linux_file_attr: return
612 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
614 attr = get_linux_file_attr(path)
616 self.linux_attr = attr
618 if e.errno == errno.EACCES:
619 add_error('read Linux attr: %s' % e)
620 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
621 # Assume filesystem doesn't support attrs.
623 elif e.errno == EINVAL:
624 global _warned_about_attr_einval
625 if not _warned_about_attr_einval:
626 log("Ignoring attr EINVAL;"
627 + " if you're not using ntfs-3g, please report: "
628 + path_msg(path) + '\n')
629 _warned_about_attr_einval = True
634 def _same_linux_attr(self, other):
635 """Return true or false to indicate similarity in the hardlink sense."""
636 return self.linux_attr == other.linux_attr
638 def _encode_linux_attr(self):
640 return vint.pack('V', self.linux_attr)
644 def _load_linux_attr_rec(self, port):
645 data = vint.read_bvec(port)
646 self.linux_attr = vint.unpack('V', data)[0]
648 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
650 check_linux_file_attr_api()
651 if not set_linux_file_attr:
652 add_error("%s: can't restore linuxattrs: "
653 "linuxattr support missing.\n" % path_msg(path))
656 set_linux_file_attr(path, self.linux_attr)
658 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
659 raise ApplyError('Linux chattr: %s (0x%s)'
660 % (e, hex(self.linux_attr)))
661 elif e.errno == EINVAL:
662 msg = "if you're not using ntfs-3g, please report"
663 raise ApplyError('Linux chattr: %s (0x%s) (%s)'
664 % (e, hex(self.linux_attr), msg))
669 ## Linux extended attributes (getfattr(1), setfattr(1))
671 def _add_linux_xattr(self, path, st):
674 self.linux_xattr = xattr.get_all(path, nofollow=True)
675 except EnvironmentError as e:
676 if e.errno != errno.EOPNOTSUPP:
679 def _same_linux_xattr(self, other):
680 """Return true or false to indicate similarity in the hardlink sense."""
681 return self.linux_xattr == other.linux_xattr
683 def _encode_linux_xattr(self):
685 result = vint.pack('V', len(self.linux_xattr))
686 for name, value in self.linux_xattr:
687 result += vint.pack('ss', name, value)
692 def _load_linux_xattr_rec(self, file):
693 data = vint.read_bvec(file)
694 memfile = BytesIO(data)
696 for i in range(vint.read_vuint(memfile)):
697 key = vint.read_bvec(memfile)
698 value = vint.read_bvec(memfile)
699 result.append((key, value))
700 self.linux_xattr = result
702 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
705 add_error("%s: can't restore xattr; xattr support missing.\n"
708 if not self.linux_xattr:
711 existing_xattrs = set(xattr.list(path, nofollow=True))
713 if e.errno == errno.EACCES:
714 raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
717 for k, v in self.linux_xattr:
718 if k not in existing_xattrs \
719 or v != xattr.get(path, k, nofollow=True):
721 xattr.set(path, k, v, nofollow=True)
723 if e.errno == errno.EPERM \
724 or e.errno == errno.EOPNOTSUPP:
725 raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
728 existing_xattrs -= frozenset([k])
729 for k in existing_xattrs:
731 xattr.remove(path, k, nofollow=True)
733 if e.errno in (errno.EPERM, errno.EACCES):
734 raise ApplyError('xattr.remove %r: %s' % (path_msg(path), e))
739 self.mode = self.uid = self.gid = self.user = self.group = None
740 self.atime = self.mtime = self.ctime = None
744 self.symlink_target = None
745 self.hardlink_target = None
746 self.linux_attr = None
747 self.linux_xattr = None
748 self.posix1e_acl = None
750 def __eq__(self, other):
751 if not isinstance(other, Metadata): return False
752 if self.mode != other.mode: return False
753 if self.mtime != other.mtime: return False
754 if self.ctime != other.ctime: return False
755 if self.atime != other.atime: return False
756 if self.path != other.path: return False
757 if self.uid != other.uid: return False
758 if self.gid != other.gid: return False
759 if self.size != other.size: return False
760 if self.user != other.user: return False
761 if self.group != other.group: return False
762 if self.symlink_target != other.symlink_target: return False
763 if self.hardlink_target != other.hardlink_target: return False
764 if self.linux_attr != other.linux_attr: return False
765 if self.posix1e_acl != other.posix1e_acl: return False
768 def __ne__(self, other):
769 return not self.__eq__(other)
772 return hash((self.mode,
783 self.hardlink_target,
788 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
789 if self.path is not None:
790 result += ' path:' + repr(self.path)
791 if self.mode is not None:
792 result += ' mode: %o (%s)' % (self.mode, xstat.mode_str(self.mode))
793 if self.uid is not None:
794 result += ' uid:' + str(self.uid)
795 if self.gid is not None:
796 result += ' gid:' + str(self.gid)
797 if self.user is not None:
798 result += ' user:' + repr(self.user)
799 if self.group is not None:
800 result += ' group:' + repr(self.group)
801 if self.size is not None:
802 result += ' size:' + repr(self.size)
803 for name, val in (('atime', self.atime),
804 ('mtime', self.mtime),
805 ('ctime', self.ctime)):
807 result += ' %s:%r (%d)' \
809 strftime('%Y-%m-%d %H:%M %z',
810 gmtime(xstat.fstime_floor_secs(val))),
813 return ''.join(result)
815 def write(self, port, include_path=True):
816 records = include_path and [(_rec_tag_path, self._encode_path())] or []
817 records.extend([(_rec_tag_common_v3, self._encode_common()),
818 (_rec_tag_symlink_target,
819 self._encode_symlink_target()),
820 (_rec_tag_hardlink_target,
821 self._encode_hardlink_target()),
822 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
823 (_rec_tag_linux_attr, self._encode_linux_attr()),
824 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
825 for tag, data in records:
827 vint.write_vuint(port, tag)
828 vint.write_bvec(port, data)
829 vint.write_vuint(port, _rec_tag_end)
831 def encode(self, include_path=True):
833 self.write(port, include_path)
834 return port.getvalue()
837 return deepcopy(self)
841 # This method should either return a valid Metadata object,
842 # return None if there was no information at all (just a
843 # _rec_tag_end), throw EOFError if there was nothing at all to
844 # read, or throw an Exception if a valid object could not be
846 tag = vint.read_vuint(port)
847 if tag == _rec_tag_end:
849 try: # From here on, EOF is an error.
851 while True: # only exit is error (exception) or _rec_tag_end
852 if tag == _rec_tag_path:
853 result._load_path_rec(port)
854 elif tag == _rec_tag_common_v3:
855 result._load_common_rec(port, version=3)
856 elif tag == _rec_tag_common_v2:
857 result._load_common_rec(port, version=2)
858 elif tag == _rec_tag_symlink_target:
859 result._load_symlink_target_rec(port)
860 elif tag == _rec_tag_hardlink_target:
861 result._load_hardlink_target_rec(port)
862 elif tag == _rec_tag_posix1e_acl:
863 result._load_posix1e_acl_rec(port)
864 elif tag == _rec_tag_linux_attr:
865 result._load_linux_attr_rec(port)
866 elif tag == _rec_tag_linux_xattr:
867 result._load_linux_xattr_rec(port)
868 elif tag == _rec_tag_end:
870 elif tag == _rec_tag_common_v1: # Should be very rare.
871 result._load_common_rec(port, version=1)
872 else: # unknown record
874 tag = vint.read_vuint(port)
876 raise Exception("EOF while reading Metadata")
879 return stat.S_ISDIR(self.mode)
881 def create_path(self, path, create_symlinks=True):
882 self._create_via_common_rec(path, create_symlinks=create_symlinks)
884 def apply_to_path(self, path=None, restore_numeric_ids=False):
885 # apply metadata to path -- file must exist
889 raise Exception('Metadata.apply_to_path() called with no path')
890 if not self._recognized_file_type():
891 add_error('not applying metadata to "%s"' % path_msg(path)
892 + ' with unrecognized mode "0x%x"\n' % self.mode)
894 num_ids = restore_numeric_ids
895 for apply_metadata in (self._apply_common_rec,
896 self._apply_posix1e_acl_rec,
897 self._apply_linux_attr_rec,
898 self._apply_linux_xattr_rec):
900 apply_metadata(path, restore_numeric_ids=num_ids)
901 except ApplyError as e:
904 def same_file(self, other):
905 """Compare this to other for equivalency. Return true if
906 their information implies they could represent the same file
907 on disk, in the hardlink sense. Assume they're both regular
909 return self._same_common(other) \
910 and self._same_hardlink_target(other) \
911 and self._same_posix1e_acl(other) \
912 and self._same_linux_attr(other) \
913 and self._same_linux_xattr(other)
916 def from_path(path, statinfo=None, archive_path=None,
917 save_symlinks=True, hardlink_target=None,
919 """Return the metadata associated with the path. When normalized is
920 true, return the metadata appropriate for a typical save, which
921 may or may not be all of it."""
923 result.path = archive_path
924 st = statinfo or xstat.lstat(path)
925 result._add_common(path, st)
927 result._add_symlink_target(path, st)
928 result._add_hardlink_target(hardlink_target)
929 result._add_posix1e_acl(path, st)
930 result._add_linux_attr(path, st)
931 result._add_linux_xattr(path, st)
933 # Only store sizes for regular files and symlinks for now.
934 if not (stat.S_ISREG(result.mode) or stat.S_ISLNK(result.mode)):
939 def save_tree(output_file, paths,
945 # Issue top-level rewrite warnings.
947 safe_path = _clean_up_path_for_archive(path)
948 if safe_path != path:
949 log('archiving "%s" as "%s"\n'
950 % (path_msg(path), path_msg(safe_path)))
954 safe_path = _clean_up_path_for_archive(p)
956 if stat.S_ISDIR(st.st_mode):
958 m = from_path(p, statinfo=st, archive_path=safe_path,
959 save_symlinks=save_symlinks)
961 print(m.path, file=sys.stderr)
962 m.write(output_file, include_path=write_paths)
964 start_dir = os.getcwd()
966 for (p, st) in recursive_dirlist(paths, xdev=xdev):
967 dirlist_dir = os.getcwd()
969 safe_path = _clean_up_path_for_archive(p)
970 m = from_path(p, statinfo=st, archive_path=safe_path,
971 save_symlinks=save_symlinks)
973 print(m.path, file=sys.stderr)
974 m.write(output_file, include_path=write_paths)
975 os.chdir(dirlist_dir)
980 def _set_up_path(meta, create_symlinks=True):
981 # Allow directories to exist as a special case -- might have
982 # been created by an earlier longer path.
986 parent = os.path.dirname(meta.path)
989 meta.create_path(meta.path, create_symlinks=create_symlinks)
992 all_fields = frozenset(['path',
1009 def summary_bytes(meta, numeric_ids = False, classification = None,
1010 human_readable = False):
1011 """Return bytes containing the "ls -l" style listing for meta.
1012 Classification may be "all", "type", or None."""
1013 user_str = group_str = size_or_dev_str = '?'
1014 symlink_target = None
1017 mode_str = xstat.mode_str(meta.mode).encode('ascii')
1018 symlink_target = meta.symlink_target
1019 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
1020 mtime_str = strftime('%Y-%m-%d %H:%M',
1021 time.localtime(mtime_secs)).encode('ascii')
1022 if meta.user and not numeric_ids:
1023 user_str = meta.user
1024 elif meta.uid != None:
1025 user_str = str(meta.uid).encode()
1026 if meta.group and not numeric_ids:
1027 group_str = meta.group
1028 elif meta.gid != None:
1029 group_str = str(meta.gid).encode()
1030 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
1032 size_or_dev_str = ('%d,%d' % (os.major(meta.rdev),
1033 os.minor(meta.rdev))).encode()
1034 elif meta.size != None:
1036 size_or_dev_str = format_filesize(meta.size).encode()
1038 size_or_dev_str = str(meta.size).encode()
1040 size_or_dev_str = b'-'
1042 classification_str = \
1043 xstat.classification_str(meta.mode,
1044 classification == 'all').encode()
1046 mode_str = b'?' * 10
1047 mtime_str = b'????-??-?? ??:??'
1048 classification_str = b'?'
1052 name += classification_str
1054 name += b' -> ' + meta.symlink_target
1056 return b'%-10s %-11s %11s %16s %s' % (mode_str,
1057 user_str + b'/' + group_str,
1063 def detailed_bytes(meta, fields = None):
1064 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
1065 # 0", "link-target:", etc.
1070 if 'path' in fields:
1071 path = meta.path or b''
1072 result.append(b'path: ' + path)
1073 if 'mode' in fields:
1074 result.append(b'mode: %o (%s)'
1075 % (meta.mode, xstat.mode_str(meta.mode).encode('ascii')))
1076 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
1077 result.append(b'link-target: ' + meta.symlink_target)
1078 if 'rdev' in fields:
1080 result.append(b'rdev: %d,%d' % (os.major(meta.rdev),
1081 os.minor(meta.rdev)))
1083 result.append(b'rdev: 0')
1084 if 'size' in fields and meta.size is not None:
1085 result.append(b'size: %d' % meta.size)
1087 result.append(b'uid: %d' % meta.uid)
1089 result.append(b'gid: %d' % meta.gid)
1090 if 'user' in fields:
1091 result.append(b'user: ' + meta.user)
1092 if 'group' in fields:
1093 result.append(b'group: ' + meta.group)
1094 if 'atime' in fields:
1095 # If we don't have xstat.lutime, that means we have to use
1096 # utime(), and utime() has no way to set the mtime/atime of a
1097 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1098 # so let's not report it. (That way scripts comparing
1099 # before/after won't trigger.)
1100 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1101 result.append(b'atime: ' + xstat.fstime_to_sec_bytes(meta.atime))
1103 result.append(b'atime: 0')
1104 if 'mtime' in fields:
1105 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1106 result.append(b'mtime: ' + xstat.fstime_to_sec_bytes(meta.mtime))
1108 result.append(b'mtime: 0')
1109 if 'ctime' in fields:
1110 result.append(b'ctime: ' + xstat.fstime_to_sec_bytes(meta.ctime))
1111 if 'linux-attr' in fields and meta.linux_attr:
1112 result.append(b'linux-attr: %x' % meta.linux_attr)
1113 if 'linux-xattr' in fields and meta.linux_xattr:
1114 for name, value in meta.linux_xattr:
1115 result.append(b'linux-xattr: %s -> %s' % (name, value))
1116 if 'posix1e-acl' in fields and meta.posix1e_acl:
1117 acl = meta.posix1e_acl[0]
1118 result.append(b'posix1e-acl: ' + acl + b'\n')
1119 if stat.S_ISDIR(meta.mode):
1120 def_acl = meta.posix1e_acl[2]
1121 result.append(b'posix1e-acl-default: ' + def_acl + b'\n')
1122 return b'\n'.join(result)
1125 class _ArchiveIterator:
1128 return Metadata.read(self._file)
1130 raise StopIteration()
1137 def __init__(self, file):
1141 def display_archive(file, out):
1144 for meta in _ArchiveIterator(file):
1147 out.write(detailed_bytes(meta))
1151 for meta in _ArchiveIterator(file):
1152 out.write(summary_bytes(meta))
1155 for meta in _ArchiveIterator(file):
1157 log('bup: no metadata path, but asked to only display path'
1158 ' (increase verbosity?)')
1160 out.write(meta.path)
1164 def start_extract(file, create_symlinks=True):
1165 for meta in _ArchiveIterator(file):
1166 if not meta: # Hit end record.
1169 print(path_msg(meta.path), file=sys.stderr)
1170 xpath = _clean_up_extract_path(meta.path)
1172 add_error(Exception('skipping risky path "%s"'
1173 % path_msg(meta.path)))
1176 _set_up_path(meta, create_symlinks=create_symlinks)
1179 def finish_extract(file, restore_numeric_ids=False):
1181 for meta in _ArchiveIterator(file):
1182 if not meta: # Hit end record.
1184 xpath = _clean_up_extract_path(meta.path)
1186 add_error(Exception('skipping risky path "%s"'
1187 % path_msg(dir.path)))
1189 if os.path.isdir(meta.path):
1190 all_dirs.append(meta)
1193 print(path_msg(meta.path), file=sys.stderr)
1194 meta.apply_to_path(path=xpath,
1195 restore_numeric_ids=restore_numeric_ids)
1196 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1197 for dir in all_dirs:
1198 # Don't need to check xpath -- won't be in all_dirs if not OK.
1199 xpath = _clean_up_extract_path(dir.path)
1201 print(path_msg(dir.path), file=sys.stderr)
1202 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1205 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1206 # For now, just store all the directories and handle them last,
1209 for meta in _ArchiveIterator(file):
1210 if not meta: # Hit end record.
1212 xpath = _clean_up_extract_path(meta.path)
1214 add_error(Exception('skipping risky path "%s"'
1215 % path_msg(meta.path)))
1219 print('+', path_msg(meta.path), file=sys.stderr)
1220 _set_up_path(meta, create_symlinks=create_symlinks)
1221 if os.path.isdir(meta.path):
1222 all_dirs.append(meta)
1225 print('=', path_msg(meta.path), file=sys.stderr)
1226 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1227 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1228 for dir in all_dirs:
1229 # Don't need to check xpath -- won't be in all_dirs if not OK.
1230 xpath = _clean_up_extract_path(dir.path)
1232 print('=', path_msg(xpath), file=sys.stderr)
1233 # Shouldn't have to check for risky paths here (omitted above).
1234 dir.apply_to_path(path=dir.path,
1235 restore_numeric_ids=restore_numeric_ids)