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 copy import deepcopy
9 from errno import EACCES, EINVAL, ENOTTY, ENOSYS, EOPNOTSUPP
10 from io import BytesIO
11 from time import gmtime, strftime
12 import errno, os, sys, stat, time, pwd, grp, socket, struct
14 from bup import vint, xstat
15 from bup.drecurse import recursive_dirlist
16 from bup.helpers import add_error, mkdirp, log, is_superuser, format_filesize
17 from bup.helpers import pwd_from_uid, pwd_from_name, grp_from_gid, grp_from_name
18 from bup.xstat import utime, lutime
21 if sys.platform.startswith('linux'):
25 log('Warning: Linux xattr support missing; install python-pyxattr.\n')
29 except AttributeError:
30 log('Warning: python-xattr module is too old; '
31 'install python-pyxattr instead.\n')
35 if not (sys.platform.startswith('cygwin') \
36 or sys.platform.startswith('darwin') \
37 or sys.platform.startswith('netbsd')):
41 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
44 from bup._helpers import get_linux_file_attr, set_linux_file_attr
46 # No need for a warning here; the only reason they won't exist is that we're
47 # not on Linux, in which case files don't have any linux attrs anyway, so
48 # lacking the functions isn't a problem.
49 get_linux_file_attr = set_linux_file_attr = None
52 # See the bup_get_linux_file_attr() comments.
53 _suppress_linux_file_attr = \
54 sys.byteorder == 'big' and struct.calcsize('@l') > struct.calcsize('@i')
56 def check_linux_file_attr_api():
57 global get_linux_file_attr, set_linux_file_attr
58 if not (get_linux_file_attr or set_linux_file_attr):
60 if _suppress_linux_file_attr:
61 log('Warning: Linux attr support disabled (see "bup help index").\n')
62 get_linux_file_attr = set_linux_file_attr = None
65 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
67 # Q: Consider hardlink support?
68 # Q: Is it OK to store raw linux attr (chattr) flags?
69 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
70 # Q: Is the application of posix1e has_extended() correct?
71 # Q: Is one global --numeric-ids argument sufficient?
72 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
73 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
75 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
76 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
77 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
78 # FIXME: Consider pack('vvvvsss', ...) optimization.
82 # osx (varies between hfs and hfs+):
83 # type - regular dir char block fifo socket ...
84 # perms - rwxrwxrwxsgt
85 # times - ctime atime mtime
88 # hard-link-info (hfs+ only)
91 # attributes-osx see chflags
97 # type - regular dir ...
98 # times - creation, modification, posix change, access
101 # attributes - see attrib
103 # forks (alternate data streams)
107 # type - regular dir ...
108 # perms - rwxrwxrwx (maybe - see wikipedia)
109 # times - creation, modification, access
110 # attributes - see attrib
114 _have_lchmod = hasattr(os, 'lchmod')
117 def _clean_up_path_for_archive(p):
118 # Not the most efficient approach.
121 # Take everything after any '/../'.
122 pos = result.rfind('/../')
124 result = result[result.rfind('/../') + 4:]
126 # Take everything after any remaining '../'.
127 if result.startswith("../"):
130 # Remove any '/./' sequences.
131 pos = result.find('/./')
133 result = result[0:pos] + '/' + result[pos + 3:]
134 pos = result.find('/./')
136 # Remove any leading '/'s.
137 result = result.lstrip('/')
139 # Replace '//' with '/' everywhere.
140 pos = result.find('//')
142 result = result[0:pos] + '/' + result[pos + 2:]
143 pos = result.find('//')
145 # Take everything after any remaining './'.
146 if result.startswith('./'):
149 # Take everything before any remaining '/.'.
150 if result.endswith('/.'):
153 if result == '' or result.endswith('/..'):
160 if p.startswith('/'):
162 if p.find('/../') != -1:
164 if p.startswith('../'):
166 if p.endswith('/..'):
171 def _clean_up_extract_path(p):
172 result = p.lstrip('/')
175 elif _risky_path(result):
181 # These tags are currently conceptually private to Metadata, and they
182 # must be unique, and must *never* be changed.
185 _rec_tag_common = 2 # times, user, group, type, perms, etc. (legacy/broken)
186 _rec_tag_symlink_target = 3
187 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
188 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
189 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
190 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
191 _rec_tag_hardlink_target = 8 # hard link target path
192 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
194 _warned_about_attr_einval = None
197 class ApplyError(Exception):
198 # Thrown when unable to apply any given bit of metadata to a path.
203 # Metadata is stored as a sequence of tagged binary records. Each
204 # record will have some subset of add, encode, load, create, and
205 # apply methods, i.e. _add_foo...
207 # We do allow an "empty" object as a special case, i.e. no
208 # records. One can be created by trying to write Metadata(), and
209 # for such an object, read() will return None. This is used by
210 # "bup save", for example, as a placeholder in cases where
213 # NOTE: if any relevant fields are added or removed, be sure to
214 # update same_file() below.
218 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
219 # must be non-negative and < 10**9.
221 def _add_common(self, path, st):
222 assert(st.st_uid >= 0)
223 assert(st.st_gid >= 0)
226 self.atime = st.st_atime
227 self.mtime = st.st_mtime
228 self.ctime = st.st_ctime
229 self.user = self.group = ''
230 entry = pwd_from_uid(st.st_uid)
232 self.user = entry.pw_name
233 entry = grp_from_gid(st.st_gid)
235 self.group = entry.gr_name
236 self.mode = st.st_mode
237 # Only collect st_rdev if we might need it for a mknod()
238 # during restore. On some platforms (i.e. kFreeBSD), it isn't
239 # stable for other file types. For example "cp -a" will
240 # change it for a plain file.
241 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
242 self.rdev = st.st_rdev
246 def _same_common(self, other):
247 """Return true or false to indicate similarity in the hardlink sense."""
248 return self.uid == other.uid \
249 and self.gid == other.gid \
250 and self.rdev == other.rdev \
251 and self.mtime == other.mtime \
252 and self.ctime == other.ctime \
253 and self.user == other.user \
254 and self.group == other.group
256 def _encode_common(self):
259 atime = xstat.nsecs_to_timespec(self.atime)
260 mtime = xstat.nsecs_to_timespec(self.mtime)
261 ctime = xstat.nsecs_to_timespec(self.ctime)
262 result = vint.pack('vvsvsvvVvVvV',
277 def _load_common_rec(self, port, legacy_format=False):
278 unpack_fmt = 'vvsvsvvVvVvV'
280 unpack_fmt = 'VVsVsVvVvVvV'
281 data = vint.read_bvec(port)
293 ctime_ns) = vint.unpack(unpack_fmt, data)
294 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
295 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
296 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
298 def _recognized_file_type(self):
299 return stat.S_ISREG(self.mode) \
300 or stat.S_ISDIR(self.mode) \
301 or stat.S_ISCHR(self.mode) \
302 or stat.S_ISBLK(self.mode) \
303 or stat.S_ISFIFO(self.mode) \
304 or stat.S_ISSOCK(self.mode) \
305 or stat.S_ISLNK(self.mode)
307 def _create_via_common_rec(self, path, create_symlinks=True):
309 raise ApplyError('no metadata - cannot create path ' + path)
311 # If the path already exists and is a dir, try rmdir.
312 # If the path already exists and is anything else, try unlink.
315 st = xstat.lstat(path)
317 if e.errno != errno.ENOENT:
320 if stat.S_ISDIR(st.st_mode):
324 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
325 msg = 'refusing to overwrite non-empty dir ' + path
331 if stat.S_ISREG(self.mode):
332 assert(self._recognized_file_type())
333 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0o600)
335 elif stat.S_ISDIR(self.mode):
336 assert(self._recognized_file_type())
337 os.mkdir(path, 0o700)
338 elif stat.S_ISCHR(self.mode):
339 assert(self._recognized_file_type())
340 os.mknod(path, 0o600 | stat.S_IFCHR, self.rdev)
341 elif stat.S_ISBLK(self.mode):
342 assert(self._recognized_file_type())
343 os.mknod(path, 0o600 | stat.S_IFBLK, self.rdev)
344 elif stat.S_ISFIFO(self.mode):
345 assert(self._recognized_file_type())
346 os.mknod(path, 0o600 | stat.S_IFIFO)
347 elif stat.S_ISSOCK(self.mode):
349 os.mknod(path, 0o600 | stat.S_IFSOCK)
351 if e.errno in (errno.EINVAL, errno.EPERM):
352 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
356 elif stat.S_ISLNK(self.mode):
357 assert(self._recognized_file_type())
358 if self.symlink_target and create_symlinks:
359 # on MacOS, symlink() permissions depend on umask, and there's
360 # no way to chown a symlink after creating it, so we have to
362 oldumask = os.umask((self.mode & 0o777) ^ 0o777)
364 os.symlink(self.symlink_target, path)
367 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
369 assert(not self._recognized_file_type())
370 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
373 def _apply_common_rec(self, path, restore_numeric_ids=False):
375 raise ApplyError('no metadata - cannot apply to ' + path)
377 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
378 # EACCES errors at this stage are fatal for the current path.
379 if lutime and stat.S_ISLNK(self.mode):
381 lutime(path, (self.atime, self.mtime))
383 if e.errno == errno.EACCES:
384 raise ApplyError('lutime: %s' % e)
389 utime(path, (self.atime, self.mtime))
391 if e.errno == errno.EACCES:
392 raise ApplyError('utime: %s' % e)
396 uid = gid = -1 # By default, do nothing.
400 if not restore_numeric_ids:
401 if self.uid != 0 and self.user:
402 entry = pwd_from_name(self.user)
405 if self.gid != 0 and self.group:
406 entry = grp_from_name(self.group)
409 else: # not superuser - only consider changing the group/gid
410 user_gids = os.getgroups()
411 if self.gid in user_gids:
413 if not restore_numeric_ids and self.gid != 0:
414 # The grp might not exist on the local system.
415 grps = filter(None, [grp_from_gid(x) for x in user_gids])
416 if self.group in [x.gr_name for x in grps]:
417 g = grp_from_name(self.group)
421 if uid != -1 or gid != -1:
423 os.lchown(path, uid, gid)
425 if e.errno == errno.EPERM:
426 add_error('lchown: %s' % e)
427 elif sys.platform.startswith('cygwin') \
428 and e.errno == errno.EINVAL:
429 add_error('lchown: unknown uid/gid (%d/%d) for %s'
436 os.lchmod(path, stat.S_IMODE(self.mode))
437 except errno.ENOSYS: # Function not implemented
439 elif not stat.S_ISLNK(self.mode):
440 os.chmod(path, stat.S_IMODE(self.mode))
445 def _encode_path(self):
447 return vint.pack('s', self.path)
451 def _load_path_rec(self, port):
452 self.path = vint.unpack('s', vint.read_bvec(port))[0]
457 def _add_symlink_target(self, path, st):
459 if stat.S_ISLNK(st.st_mode):
460 self.symlink_target = os.readlink(path)
462 add_error('readlink: %s' % e)
464 def _encode_symlink_target(self):
465 return self.symlink_target
467 def _load_symlink_target_rec(self, port):
468 target = vint.read_bvec(port)
469 self.symlink_target = target
470 self.size = len(target)
475 def _add_hardlink_target(self, target):
476 self.hardlink_target = target
478 def _same_hardlink_target(self, other):
479 """Return true or false to indicate similarity in the hardlink sense."""
480 return self.hardlink_target == other.hardlink_target
482 def _encode_hardlink_target(self):
483 return self.hardlink_target
485 def _load_hardlink_target_rec(self, port):
486 self.hardlink_target = vint.read_bvec(port)
489 ## POSIX1e ACL records
491 # Recorded as a list:
492 # [txt_id_acl, num_id_acl]
493 # or, if a directory:
494 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
495 # The numeric/text distinction only matters when reading/restoring
497 def _add_posix1e_acl(self, path, st):
498 if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
500 if not stat.S_ISLNK(st.st_mode):
504 if posix1e.has_extended(path):
505 acl = posix1e.ACL(file=path)
506 acls = [acl, acl] # txt and num are the same
507 if stat.S_ISDIR(st.st_mode):
508 def_acl = posix1e.ACL(filedef=path)
509 def_acls = [def_acl, def_acl]
510 except EnvironmentError as e:
511 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
514 txt_flags = posix1e.TEXT_ABBREVIATE
515 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
516 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
517 acls[1].to_any_text('', '\n', num_flags)]
519 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
520 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
521 self.posix1e_acl = acl_rep
523 def _same_posix1e_acl(self, other):
524 """Return true or false to indicate similarity in the hardlink sense."""
525 return self.posix1e_acl == other.posix1e_acl
527 def _encode_posix1e_acl(self):
528 # Encode as two strings (w/default ACL string possibly empty).
530 acls = self.posix1e_acl
532 acls.extend(['', ''])
533 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
537 def _load_posix1e_acl_rec(self, port):
538 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
540 acl_rep = acl_rep[:2]
541 self.posix1e_acl = acl_rep
543 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
544 def apply_acl(acl_rep, kind):
546 acl = posix1e.ACL(text = acl_rep)
549 # pylibacl appears to return an IOError with errno
550 # set to 0 if a group referred to by the ACL rep
551 # doesn't exist on the current system.
552 raise ApplyError("POSIX1e ACL: can't create %r for %r"
557 acl.applyto(path, kind)
559 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
560 raise ApplyError('POSIX1e ACL applyto: %s' % e)
566 add_error("%s: can't restore ACLs; posix1e support missing.\n"
570 acls = self.posix1e_acl
572 if restore_numeric_ids:
573 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
575 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
576 if restore_numeric_ids:
577 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
579 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
582 ## Linux attributes (lsattr(1), chattr(1))
584 def _add_linux_attr(self, path, st):
585 check_linux_file_attr_api()
586 if not get_linux_file_attr: return
587 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
589 attr = get_linux_file_attr(path)
591 self.linux_attr = attr
593 if e.errno == errno.EACCES:
594 add_error('read Linux attr: %s' % e)
595 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
596 # Assume filesystem doesn't support attrs.
598 elif e.errno == EINVAL:
599 global _warned_about_attr_einval
600 if not _warned_about_attr_einval:
601 log("Ignoring attr EINVAL;"
602 + " if you're not using ntfs-3g, please report: "
604 _warned_about_attr_einval = True
609 def _same_linux_attr(self, other):
610 """Return true or false to indicate similarity in the hardlink sense."""
611 return self.linux_attr == other.linux_attr
613 def _encode_linux_attr(self):
615 return vint.pack('V', self.linux_attr)
619 def _load_linux_attr_rec(self, port):
620 data = vint.read_bvec(port)
621 self.linux_attr = vint.unpack('V', data)[0]
623 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
625 check_linux_file_attr_api()
626 if not set_linux_file_attr:
627 add_error("%s: can't restore linuxattrs: "
628 "linuxattr support missing.\n" % path)
631 set_linux_file_attr(path, self.linux_attr)
633 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
634 raise ApplyError('Linux chattr: %s (0x%s)'
635 % (e, hex(self.linux_attr)))
636 elif e.errno == EINVAL:
637 msg = "if you're not using ntfs-3g, please report"
638 raise ApplyError('Linux chattr: %s (0x%s) (%s)'
639 % (e, hex(self.linux_attr), msg))
644 ## Linux extended attributes (getfattr(1), setfattr(1))
646 def _add_linux_xattr(self, path, st):
649 self.linux_xattr = xattr.get_all(path, nofollow=True)
650 except EnvironmentError as e:
651 if e.errno != errno.EOPNOTSUPP:
654 def _same_linux_xattr(self, other):
655 """Return true or false to indicate similarity in the hardlink sense."""
656 return self.linux_xattr == other.linux_xattr
658 def _encode_linux_xattr(self):
660 result = vint.pack('V', len(self.linux_xattr))
661 for name, value in self.linux_xattr:
662 result += vint.pack('ss', name, value)
667 def _load_linux_xattr_rec(self, file):
668 data = vint.read_bvec(file)
669 memfile = BytesIO(data)
671 for i in range(vint.read_vuint(memfile)):
672 key = vint.read_bvec(memfile)
673 value = vint.read_bvec(memfile)
674 result.append((key, value))
675 self.linux_xattr = result
677 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
680 add_error("%s: can't restore xattr; xattr support missing.\n"
683 if not self.linux_xattr:
686 existing_xattrs = set(xattr.list(path, nofollow=True))
688 if e.errno == errno.EACCES:
689 raise ApplyError('xattr.set %r: %s' % (path, e))
692 for k, v in self.linux_xattr:
693 if k not in existing_xattrs \
694 or v != xattr.get(path, k, nofollow=True):
696 xattr.set(path, k, v, nofollow=True)
698 if e.errno == errno.EPERM \
699 or e.errno == errno.EOPNOTSUPP:
700 raise ApplyError('xattr.set %r: %s' % (path, e))
703 existing_xattrs -= frozenset([k])
704 for k in existing_xattrs:
706 xattr.remove(path, k, nofollow=True)
708 if e.errno in (errno.EPERM, errno.EACCES):
709 raise ApplyError('xattr.remove %r: %s' % (path, e))
714 self.mode = self.uid = self.gid = self.user = self.group = None
715 self.atime = self.mtime = self.ctime = None
719 self.symlink_target = None
720 self.hardlink_target = None
721 self.linux_attr = None
722 self.linux_xattr = None
723 self.posix1e_acl = None
725 def __eq__(self, other):
726 if not isinstance(other, Metadata): return False
727 if self.mode != other.mode: return False
728 if self.mtime != other.mtime: return False
729 if self.ctime != other.ctime: return False
730 if self.atime != other.atime: return False
731 if self.path != other.path: return False
732 if self.uid != other.uid: return False
733 if self.gid != other.gid: return False
734 if self.size != other.size: return False
735 if self.user != other.user: return False
736 if self.group != other.group: return False
737 if self.symlink_target != other.symlink_target: return False
738 if self.hardlink_target != other.hardlink_target: return False
739 if self.linux_attr != other.linux_attr: return False
740 if self.posix1e_acl != other.posix1e_acl: return False
743 def __ne__(self, other):
744 return not self.__eq__(other)
747 return hash((self.mode,
758 self.hardlink_target,
763 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
764 if self.path is not None:
765 result += ' path:' + repr(self.path)
766 if self.mode is not None:
767 result += ' mode:' + repr(xstat.mode_str(self.mode)
768 + '(%s)' % oct(self.mode))
769 if self.uid is not None:
770 result += ' uid:' + str(self.uid)
771 if self.gid is not None:
772 result += ' gid:' + str(self.gid)
773 if self.user is not None:
774 result += ' user:' + repr(self.user)
775 if self.group is not None:
776 result += ' group:' + repr(self.group)
777 if self.size is not None:
778 result += ' size:' + repr(self.size)
779 for name, val in (('atime', self.atime),
780 ('mtime', self.mtime),
781 ('ctime', self.ctime)):
783 result += ' %s:%r (%d)' \
785 strftime('%Y-%m-%d %H:%M %z',
786 gmtime(xstat.fstime_floor_secs(val))),
789 return ''.join(result)
791 def write(self, port, include_path=True):
792 records = include_path and [(_rec_tag_path, self._encode_path())] or []
793 records.extend([(_rec_tag_common_v2, self._encode_common()),
794 (_rec_tag_symlink_target,
795 self._encode_symlink_target()),
796 (_rec_tag_hardlink_target,
797 self._encode_hardlink_target()),
798 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
799 (_rec_tag_linux_attr, self._encode_linux_attr()),
800 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
801 for tag, data in records:
803 vint.write_vuint(port, tag)
804 vint.write_bvec(port, data)
805 vint.write_vuint(port, _rec_tag_end)
807 def encode(self, include_path=True):
809 self.write(port, include_path)
810 return port.getvalue()
813 return deepcopy(self)
817 # This method should either return a valid Metadata object,
818 # return None if there was no information at all (just a
819 # _rec_tag_end), throw EOFError if there was nothing at all to
820 # read, or throw an Exception if a valid object could not be
822 tag = vint.read_vuint(port)
823 if tag == _rec_tag_end:
825 try: # From here on, EOF is an error.
827 while True: # only exit is error (exception) or _rec_tag_end
828 if tag == _rec_tag_path:
829 result._load_path_rec(port)
830 elif tag == _rec_tag_common_v2:
831 result._load_common_rec(port)
832 elif tag == _rec_tag_symlink_target:
833 result._load_symlink_target_rec(port)
834 elif tag == _rec_tag_hardlink_target:
835 result._load_hardlink_target_rec(port)
836 elif tag == _rec_tag_posix1e_acl:
837 result._load_posix1e_acl_rec(port)
838 elif tag == _rec_tag_linux_attr:
839 result._load_linux_attr_rec(port)
840 elif tag == _rec_tag_linux_xattr:
841 result._load_linux_xattr_rec(port)
842 elif tag == _rec_tag_end:
844 elif tag == _rec_tag_common: # Should be very rare.
845 result._load_common_rec(port, legacy_format = True)
846 else: # unknown record
848 tag = vint.read_vuint(port)
850 raise Exception("EOF while reading Metadata")
853 return stat.S_ISDIR(self.mode)
855 def create_path(self, path, create_symlinks=True):
856 self._create_via_common_rec(path, create_symlinks=create_symlinks)
858 def apply_to_path(self, path=None, restore_numeric_ids=False):
859 # apply metadata to path -- file must exist
863 raise Exception('Metadata.apply_to_path() called with no path')
864 if not self._recognized_file_type():
865 add_error('not applying metadata to "%s"' % path
866 + ' with unrecognized mode "0x%x"\n' % self.mode)
868 num_ids = restore_numeric_ids
869 for apply_metadata in (self._apply_common_rec,
870 self._apply_posix1e_acl_rec,
871 self._apply_linux_attr_rec,
872 self._apply_linux_xattr_rec):
874 apply_metadata(path, restore_numeric_ids=num_ids)
875 except ApplyError as e:
878 def same_file(self, other):
879 """Compare this to other for equivalency. Return true if
880 their information implies they could represent the same file
881 on disk, in the hardlink sense. Assume they're both regular
883 return self._same_common(other) \
884 and self._same_hardlink_target(other) \
885 and self._same_posix1e_acl(other) \
886 and self._same_linux_attr(other) \
887 and self._same_linux_xattr(other)
890 def from_path(path, statinfo=None, archive_path=None,
891 save_symlinks=True, hardlink_target=None):
893 result.path = archive_path
894 st = statinfo or xstat.lstat(path)
895 result.size = st.st_size
896 result._add_common(path, st)
898 result._add_symlink_target(path, st)
899 result._add_hardlink_target(hardlink_target)
900 result._add_posix1e_acl(path, st)
901 result._add_linux_attr(path, st)
902 result._add_linux_xattr(path, st)
906 def save_tree(output_file, paths,
912 # Issue top-level rewrite warnings.
914 safe_path = _clean_up_path_for_archive(path)
915 if safe_path != path:
916 log('archiving "%s" as "%s"\n' % (path, safe_path))
920 safe_path = _clean_up_path_for_archive(p)
922 if stat.S_ISDIR(st.st_mode):
924 m = from_path(p, statinfo=st, archive_path=safe_path,
925 save_symlinks=save_symlinks)
927 print >> sys.stderr, m.path
928 m.write(output_file, include_path=write_paths)
930 start_dir = os.getcwd()
932 for (p, st) in recursive_dirlist(paths, xdev=xdev):
933 dirlist_dir = os.getcwd()
935 safe_path = _clean_up_path_for_archive(p)
936 m = from_path(p, statinfo=st, archive_path=safe_path,
937 save_symlinks=save_symlinks)
939 print >> sys.stderr, m.path
940 m.write(output_file, include_path=write_paths)
941 os.chdir(dirlist_dir)
946 def _set_up_path(meta, create_symlinks=True):
947 # Allow directories to exist as a special case -- might have
948 # been created by an earlier longer path.
952 parent = os.path.dirname(meta.path)
955 meta.create_path(meta.path, create_symlinks=create_symlinks)
958 all_fields = frozenset(['path',
975 def summary_str(meta, numeric_ids = False, classification = None,
976 human_readable = False):
978 """Return a string containing the "ls -l" style listing for meta.
979 Classification may be "all", "type", or None."""
980 user_str = group_str = size_or_dev_str = '?'
981 symlink_target = None
984 mode_str = xstat.mode_str(meta.mode)
985 symlink_target = meta.symlink_target
986 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
987 mtime_str = strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
988 if meta.user and not numeric_ids:
990 elif meta.uid != None:
991 user_str = str(meta.uid)
992 if meta.group and not numeric_ids:
993 group_str = meta.group
994 elif meta.gid != None:
995 group_str = str(meta.gid)
996 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
998 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
1000 elif meta.size != None:
1002 size_or_dev_str = format_filesize(meta.size)
1004 size_or_dev_str = str(meta.size)
1006 size_or_dev_str = '-'
1008 classification_str = \
1009 xstat.classification_str(meta.mode, classification == 'all')
1012 mtime_str = '????-??-?? ??:??'
1013 classification_str = '?'
1017 name += classification_str
1019 name += ' -> ' + meta.symlink_target
1021 return '%-10s %-11s %11s %16s %s' % (mode_str,
1022 user_str + "/" + group_str,
1028 def detailed_str(meta, fields = None):
1029 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
1030 # 0", "link-target:", etc.
1035 if 'path' in fields:
1036 path = meta.path or ''
1037 result.append('path: ' + path)
1038 if 'mode' in fields:
1039 result.append('mode: %s (%s)' % (oct(meta.mode),
1040 xstat.mode_str(meta.mode)))
1041 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
1042 result.append('link-target: ' + meta.symlink_target)
1043 if 'rdev' in fields:
1045 result.append('rdev: %d,%d' % (os.major(meta.rdev),
1046 os.minor(meta.rdev)))
1048 result.append('rdev: 0')
1049 if 'size' in fields and meta.size:
1050 result.append('size: ' + str(meta.size))
1052 result.append('uid: ' + str(meta.uid))
1054 result.append('gid: ' + str(meta.gid))
1055 if 'user' in fields:
1056 result.append('user: ' + meta.user)
1057 if 'group' in fields:
1058 result.append('group: ' + meta.group)
1059 if 'atime' in fields:
1060 # If we don't have xstat.lutime, that means we have to use
1061 # utime(), and utime() has no way to set the mtime/atime of a
1062 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1063 # so let's not report it. (That way scripts comparing
1064 # before/after won't trigger.)
1065 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1066 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1068 result.append('atime: 0')
1069 if 'mtime' in fields:
1070 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1071 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1073 result.append('mtime: 0')
1074 if 'ctime' in fields:
1075 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1076 if 'linux-attr' in fields and meta.linux_attr:
1077 result.append('linux-attr: ' + hex(meta.linux_attr))
1078 if 'linux-xattr' in fields and meta.linux_xattr:
1079 for name, value in meta.linux_xattr:
1080 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1081 if 'posix1e-acl' in fields and meta.posix1e_acl:
1082 acl = meta.posix1e_acl[0]
1083 result.append('posix1e-acl: ' + acl + '\n')
1084 if stat.S_ISDIR(meta.mode):
1085 def_acl = meta.posix1e_acl[2]
1086 result.append('posix1e-acl-default: ' + def_acl + '\n')
1087 return '\n'.join(result)
1090 class _ArchiveIterator:
1093 return Metadata.read(self._file)
1095 raise StopIteration()
1100 def __init__(self, file):
1104 def display_archive(file):
1107 for meta in _ArchiveIterator(file):
1110 print detailed_str(meta)
1113 for meta in _ArchiveIterator(file):
1114 print summary_str(meta)
1116 for meta in _ArchiveIterator(file):
1118 print >> sys.stderr, \
1119 'bup: no metadata path, but asked to only display path', \
1120 '(increase verbosity?)'
1125 def start_extract(file, create_symlinks=True):
1126 for meta in _ArchiveIterator(file):
1127 if not meta: # Hit end record.
1130 print >> sys.stderr, meta.path
1131 xpath = _clean_up_extract_path(meta.path)
1133 add_error(Exception('skipping risky path "%s"' % meta.path))
1136 _set_up_path(meta, create_symlinks=create_symlinks)
1139 def finish_extract(file, restore_numeric_ids=False):
1141 for meta in _ArchiveIterator(file):
1142 if not meta: # Hit end record.
1144 xpath = _clean_up_extract_path(meta.path)
1146 add_error(Exception('skipping risky path "%s"' % dir.path))
1148 if os.path.isdir(meta.path):
1149 all_dirs.append(meta)
1152 print >> sys.stderr, meta.path
1153 meta.apply_to_path(path=xpath,
1154 restore_numeric_ids=restore_numeric_ids)
1155 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1156 for dir in all_dirs:
1157 # Don't need to check xpath -- won't be in all_dirs if not OK.
1158 xpath = _clean_up_extract_path(dir.path)
1160 print >> sys.stderr, dir.path
1161 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1164 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1165 # For now, just store all the directories and handle them last,
1168 for meta in _ArchiveIterator(file):
1169 if not meta: # Hit end record.
1171 xpath = _clean_up_extract_path(meta.path)
1173 add_error(Exception('skipping risky path "%s"' % meta.path))
1177 print >> sys.stderr, '+', meta.path
1178 _set_up_path(meta, create_symlinks=create_symlinks)
1179 if os.path.isdir(meta.path):
1180 all_dirs.append(meta)
1183 print >> sys.stderr, '=', meta.path
1184 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1185 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1186 for dir in all_dirs:
1187 # Don't need to check xpath -- won't be in all_dirs if not OK.
1188 xpath = _clean_up_extract_path(dir.path)
1190 print >> sys.stderr, '=', xpath
1191 # Shouldn't have to check for risky paths here (omitted above).
1192 dir.apply_to_path(path=dir.path,
1193 restore_numeric_ids=restore_numeric_ids)