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.
7 import errno, os, sys, stat, time, platform, pwd, grp
8 from cStringIO import StringIO
9 from bup import vint, xstat
10 from bup.drecurse import recursive_dirlist
11 from bup.helpers import add_error, mkdirp, log, is_superuser
12 from bup.helpers import pwd_from_uid, pwd_from_name, grp_from_gid, grp_from_name
13 from bup.xstat import utime, lutime
15 if 'Linux' in platform.system():
19 log('Warning: Linux xattr support missing; install python-pyxattr.\n')
24 except AttributeError:
25 log('Warning: python-xattr module is too old; '
26 'install python-pyxattr instead.\n')
32 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
35 from bup._helpers import get_linux_file_attr, set_linux_file_attr
37 # No need for a warning here; the only reason they won't exist is that we're
38 # not on Linux, in which case files don't have any linux attrs anyway, so
39 # lacking the functions isn't a problem.
40 get_linux_file_attr = set_linux_file_attr = None
43 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
45 # Q: Consider hardlink support?
46 # Q: Is it OK to store raw linux attr (chattr) flags?
47 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
48 # Q: Is the application of posix1e has_extended() correct?
49 # Q: Is one global --numeric-ids argument sufficient?
50 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
51 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
53 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
54 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
55 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
56 # FIXME: Consider pack('vvvvsss', ...) optimization.
57 # FIXME: Consider caching users/groups.
61 # osx (varies between hfs and hfs+):
62 # type - regular dir char block fifo socket ...
63 # perms - rwxrwxrwxsgt
64 # times - ctime atime mtime
67 # hard-link-info (hfs+ only)
70 # attributes-osx see chflags
76 # type - regular dir ...
77 # times - creation, modification, posix change, access
80 # attributes - see attrib
82 # forks (alternate data streams)
86 # type - regular dir ...
87 # perms - rwxrwxrwx (maybe - see wikipedia)
88 # times - creation, modification, access
89 # attributes - see attrib
93 _have_lchmod = hasattr(os, 'lchmod')
96 def _clean_up_path_for_archive(p):
97 # Not the most efficient approach.
100 # Take everything after any '/../'.
101 pos = result.rfind('/../')
103 result = result[result.rfind('/../') + 4:]
105 # Take everything after any remaining '../'.
106 if result.startswith("../"):
109 # Remove any '/./' sequences.
110 pos = result.find('/./')
112 result = result[0:pos] + '/' + result[pos + 3:]
113 pos = result.find('/./')
115 # Remove any leading '/'s.
116 result = result.lstrip('/')
118 # Replace '//' with '/' everywhere.
119 pos = result.find('//')
121 result = result[0:pos] + '/' + result[pos + 2:]
122 pos = result.find('//')
124 # Take everything after any remaining './'.
125 if result.startswith('./'):
128 # Take everything before any remaining '/.'.
129 if result.endswith('/.'):
132 if result == '' or result.endswith('/..'):
139 if p.startswith('/'):
141 if p.find('/../') != -1:
143 if p.startswith('../'):
145 if p.endswith('/..'):
150 def _clean_up_extract_path(p):
151 result = p.lstrip('/')
154 elif _risky_path(result):
160 # These tags are currently conceptually private to Metadata, and they
161 # must be unique, and must *never* be changed.
164 _rec_tag_common = 2 # times, user, group, type, perms, etc.
165 _rec_tag_symlink_target = 3
166 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
167 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e acls?
168 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
169 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
170 _rec_tag_hardlink_target = 8 # hard link target path
173 class ApplyError(Exception):
174 # Thrown when unable to apply any given bit of metadata to a path.
179 # Metadata is stored as a sequence of tagged binary records. Each
180 # record will have some subset of add, encode, load, create, and
181 # apply methods, i.e. _add_foo...
183 # We do allow an "empty" object as a special case, i.e. no
184 # records. One can be created by trying to write Metadata(), and
185 # for such an object, read() will return None. This is used by
186 # "bup save", for example, as a placeholder in cases where
189 # NOTE: if any relevant fields are added or removed, be sure to
190 # update same_file() below.
194 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
195 # must be non-negative and < 10**9.
197 def _add_common(self, path, st):
200 self.rdev = st.st_rdev
201 self.atime = st.st_atime
202 self.mtime = st.st_mtime
203 self.ctime = st.st_ctime
204 self.user = self.group = ''
205 entry = pwd_from_uid(st.st_uid)
207 self.user = entry.pw_name
208 entry = grp_from_gid(st.st_gid)
210 self.group = entry.gr_name
211 self.mode = st.st_mode
213 def _same_common(self, other):
214 """Return true or false to indicate similarity in the hardlink sense."""
215 return self.uid == other.uid \
216 and self.gid == other.gid \
217 and self.rdev == other.rdev \
218 and self.atime == other.atime \
219 and self.mtime == other.mtime \
220 and self.ctime == other.ctime \
221 and self.user == other.user \
222 and self.group == other.group
224 def _encode_common(self):
227 atime = xstat.nsecs_to_timespec(self.atime)
228 mtime = xstat.nsecs_to_timespec(self.mtime)
229 ctime = xstat.nsecs_to_timespec(self.ctime)
230 result = vint.pack('VVsVsVvVvVvV',
245 def _load_common_rec(self, port):
246 data = vint.read_bvec(port)
258 ctime_ns) = vint.unpack('VVsVsVvVvVvV', data)
259 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
260 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
261 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
263 def _recognized_file_type(self):
264 return stat.S_ISREG(self.mode) \
265 or stat.S_ISDIR(self.mode) \
266 or stat.S_ISCHR(self.mode) \
267 or stat.S_ISBLK(self.mode) \
268 or stat.S_ISFIFO(self.mode) \
269 or stat.S_ISSOCK(self.mode) \
270 or stat.S_ISLNK(self.mode)
272 def _create_via_common_rec(self, path, create_symlinks=True):
274 raise ApplyError('no metadata - cannot create path ' + path)
276 # If the path already exists and is a dir, try rmdir.
277 # If the path already exists and is anything else, try unlink.
280 st = xstat.lstat(path)
282 if e.errno != errno.ENOENT:
285 if stat.S_ISDIR(st.st_mode):
289 if e.errno == errno.ENOTEMPTY:
290 msg = 'refusing to overwrite non-empty dir ' + path
296 if stat.S_ISREG(self.mode):
297 assert(self._recognized_file_type())
298 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0600)
300 elif stat.S_ISDIR(self.mode):
301 assert(self._recognized_file_type())
303 elif stat.S_ISCHR(self.mode):
304 assert(self._recognized_file_type())
305 os.mknod(path, 0600 | stat.S_IFCHR, self.rdev)
306 elif stat.S_ISBLK(self.mode):
307 assert(self._recognized_file_type())
308 os.mknod(path, 0600 | stat.S_IFBLK, self.rdev)
309 elif stat.S_ISFIFO(self.mode):
310 assert(self._recognized_file_type())
311 os.mknod(path, 0600 | stat.S_IFIFO)
312 elif stat.S_ISSOCK(self.mode):
313 os.mknod(path, 0600 | stat.S_IFSOCK)
314 elif stat.S_ISLNK(self.mode):
315 assert(self._recognized_file_type())
316 if self.symlink_target and create_symlinks:
317 # on MacOS, symlink() permissions depend on umask, and there's
318 # no way to chown a symlink after creating it, so we have to
320 oldumask = os.umask((self.mode & 0777) ^ 0777)
322 os.symlink(self.symlink_target, path)
325 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
327 assert(not self._recognized_file_type())
328 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
331 def _apply_common_rec(self, path, restore_numeric_ids=False):
333 raise ApplyError('no metadata - cannot apply to ' + path)
335 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
336 # EACCES errors at this stage are fatal for the current path.
337 if lutime and stat.S_ISLNK(self.mode):
339 lutime(path, (self.atime, self.mtime))
341 if e.errno == errno.EACCES:
342 raise ApplyError('lutime: %s' % e)
347 utime(path, (self.atime, self.mtime))
349 if e.errno == errno.EACCES:
350 raise ApplyError('utime: %s' % e)
354 # Implement tar/rsync-like semantics; see bup-restore(1).
355 # FIXME: should we consider caching user/group name <-> id
356 # mappings, getgroups(), etc.?
357 uid = gid = -1 # By default, do nothing.
361 if not restore_numeric_ids:
362 if self.uid != 0 and self.user:
363 entry = pwd_from_name(self.user)
366 if self.gid != 0 and self.group:
367 entry = grp_from_name(self.group)
370 else: # not superuser - only consider changing the group/gid
371 user_gids = os.getgroups()
372 if self.gid in user_gids:
374 if not restore_numeric_ids and self.gid != 0:
375 # The grp might not exist on the local system.
376 grps = filter(None, [grp_from_gid(x) for x in user_gids])
377 if self.group in [x.gr_name for x in grps]:
378 g = grp_from_name(self.group)
382 if uid != -1 or gid != -1:
384 os.lchown(path, uid, gid)
386 if e.errno == errno.EPERM:
387 add_error('lchown: %s' % e)
392 os.lchmod(path, stat.S_IMODE(self.mode))
393 elif not stat.S_ISLNK(self.mode):
394 os.chmod(path, stat.S_IMODE(self.mode))
399 def _encode_path(self):
401 return vint.pack('s', self.path)
405 def _load_path_rec(self, port):
406 self.path = vint.unpack('s', vint.read_bvec(port))[0]
411 def _add_symlink_target(self, path, st):
413 if stat.S_ISLNK(st.st_mode):
414 self.symlink_target = os.readlink(path)
416 add_error('readlink: %s', e)
418 def _encode_symlink_target(self):
419 return self.symlink_target
421 def _load_symlink_target_rec(self, port):
422 self.symlink_target = vint.read_bvec(port)
427 def _add_hardlink_target(self, target):
428 self.hardlink_target = target
430 def _same_hardlink_target(self, other):
431 """Return true or false to indicate similarity in the hardlink sense."""
432 return self.hardlink_target == other.hardlink_target
434 def _encode_hardlink_target(self):
435 return self.hardlink_target
437 def _load_hardlink_target_rec(self, port):
438 self.hardlink_target = vint.read_bvec(port)
441 ## POSIX1e ACL records
443 # Recorded as a list:
444 # [txt_id_acl, num_id_acl]
445 # or, if a directory:
446 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
447 # The numeric/text distinction only matters when reading/restoring
449 def _add_posix1e_acl(self, path, st):
450 if not posix1e: return
451 if not stat.S_ISLNK(st.st_mode):
453 if posix1e.has_extended(path):
454 acl = posix1e.ACL(file=path)
455 self.posix1e_acl = [acl, acl] # txt and num are the same
456 if stat.S_ISDIR(st.st_mode):
457 acl = posix1e.ACL(filedef=path)
458 self.posix1e_acl.extend([acl, acl])
459 except EnvironmentError, e:
460 if e.errno != errno.EOPNOTSUPP:
463 def _same_posix1e_acl(self, other):
464 """Return true or false to indicate similarity in the hardlink sense."""
465 return self.posix1e_acl == other.posix1e_acl
467 def _encode_posix1e_acl(self):
468 # Encode as two strings (w/default ACL string possibly empty).
470 acls = self.posix1e_acl
471 txt_flags = posix1e.TEXT_ABBREVIATE
472 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
473 acl_reps = [acls[0].to_any_text('', '\n', txt_flags),
474 acls[1].to_any_text('', '\n', num_flags)]
478 acl_reps.append(acls[2].to_any_text('', '\n', txt_flags))
479 acl_reps.append(acls[3].to_any_text('', '\n', num_flags))
480 return vint.pack('ssss',
481 acl_reps[0], acl_reps[1], acl_reps[2], acl_reps[3])
485 def _load_posix1e_acl_rec(self, port):
486 data = vint.read_bvec(port)
487 acl_reps = vint.unpack('ssss', data)
488 if acl_reps[2] == '':
489 acl_reps = acl_reps[:2]
490 self.posix1e_acl = [posix1e.ACL(text=x) for x in acl_reps]
492 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
493 def apply_acl(acl, kind):
495 acl.applyto(path, kind)
497 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
498 raise ApplyError('POSIX1e ACL applyto: %s' % e)
504 add_error("%s: can't restore ACLs; posix1e support missing.\n"
508 acls = self.posix1e_acl
510 if restore_numeric_ids:
511 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
513 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
514 if restore_numeric_ids:
515 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
517 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
520 ## Linux attributes (lsattr(1), chattr(1))
522 def _add_linux_attr(self, path, st):
523 if not get_linux_file_attr: return
524 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
526 attr = get_linux_file_attr(path)
528 self.linux_attr = attr
530 if e.errno == errno.EACCES:
531 add_error('read Linux attr: %s' % e)
532 elif e.errno == errno.ENOTTY or e.errno == errno.ENOSYS:
533 # ENOTTY: Function not implemented.
534 # ENOSYS: Inappropriate ioctl for device.
535 # Assume filesystem doesn't support attrs.
540 def _same_linux_attr(self, other):
541 """Return true or false to indicate similarity in the hardlink sense."""
542 return self.linux_attr == other.linux_attr
544 def _encode_linux_attr(self):
546 return vint.pack('V', self.linux_attr)
550 def _load_linux_attr_rec(self, port):
551 data = vint.read_bvec(port)
552 self.linux_attr = vint.unpack('V', data)[0]
554 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
556 if not set_linux_file_attr:
557 add_error("%s: can't restore linuxattrs: "
558 "linuxattr support missing.\n" % path)
561 set_linux_file_attr(path, self.linux_attr)
563 if e.errno == errno.ENOTTY:
564 raise ApplyError('Linux chattr: %s' % e)
569 ## Linux extended attributes (getfattr(1), setfattr(1))
571 def _add_linux_xattr(self, path, st):
574 self.linux_xattr = xattr.get_all(path, nofollow=True)
575 except EnvironmentError, e:
576 if e.errno != errno.EOPNOTSUPP:
579 def _same_linux_xattr(self, other):
580 """Return true or false to indicate similarity in the hardlink sense."""
581 return self.linux_xattr == other.linux_xattr
583 def _encode_linux_xattr(self):
585 result = vint.pack('V', len(self.linux_xattr))
586 for name, value in self.linux_xattr:
587 result += vint.pack('ss', name, value)
592 def _load_linux_xattr_rec(self, file):
593 data = vint.read_bvec(file)
594 memfile = StringIO(data)
596 for i in range(vint.read_vuint(memfile)):
597 key = vint.read_bvec(memfile)
598 value = vint.read_bvec(memfile)
599 result.append((key, value))
600 self.linux_xattr = result
602 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
605 add_error("%s: can't restore xattr; xattr support missing.\n"
608 existing_xattrs = set(xattr.list(path, nofollow=True))
610 for k, v in self.linux_xattr:
611 if k not in existing_xattrs \
612 or v != xattr.get(path, k, nofollow=True):
614 xattr.set(path, k, v, nofollow=True)
616 if e.errno == errno.EPERM \
617 or e.errno == errno.EOPNOTSUPP:
618 raise ApplyError('xattr.set: %s' % e)
621 existing_xattrs -= frozenset([k])
622 for k in existing_xattrs:
624 xattr.remove(path, k, nofollow=True)
626 if e.errno == errno.EPERM:
627 raise ApplyError('xattr.remove: %s' % e)
636 self.symlink_target = None
637 self.hardlink_target = None
638 self.linux_attr = None
639 self.linux_xattr = None
640 self.posix1e_acl = None
641 self.posix1e_acl_default = None
643 def write(self, port, include_path=True):
644 records = include_path and [(_rec_tag_path, self._encode_path())] or []
645 records.extend([(_rec_tag_common, self._encode_common()),
646 (_rec_tag_symlink_target,
647 self._encode_symlink_target()),
648 (_rec_tag_hardlink_target,
649 self._encode_hardlink_target()),
650 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
651 (_rec_tag_linux_attr, self._encode_linux_attr()),
652 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
653 for tag, data in records:
655 vint.write_vuint(port, tag)
656 vint.write_bvec(port, data)
657 vint.write_vuint(port, _rec_tag_end)
659 def encode(self, include_path=True):
661 self.write(port, include_path)
662 return port.getvalue()
666 # This method should either return a valid Metadata object,
667 # return None if there was no information at all (just a
668 # _rec_tag_end), throw EOFError if there was nothing at all to
669 # read, or throw an Exception if a valid object could not be
671 tag = vint.read_vuint(port)
672 if tag == _rec_tag_end:
674 try: # From here on, EOF is an error.
676 while True: # only exit is error (exception) or _rec_tag_end
677 if tag == _rec_tag_path:
678 result._load_path_rec(port)
679 elif tag == _rec_tag_common:
680 result._load_common_rec(port)
681 elif tag == _rec_tag_symlink_target:
682 result._load_symlink_target_rec(port)
683 elif tag == _rec_tag_hardlink_target:
684 result._load_hardlink_target_rec(port)
685 elif tag == _rec_tag_posix1e_acl:
686 result._load_posix1e_acl_rec(port)
687 elif tag ==_rec_tag_nfsv4_acl:
688 result._load_nfsv4_acl_rec(port)
689 elif tag == _rec_tag_linux_attr:
690 result._load_linux_attr_rec(port)
691 elif tag == _rec_tag_linux_xattr:
692 result._load_linux_xattr_rec(port)
693 elif tag == _rec_tag_end:
695 else: # unknown record
697 tag = vint.read_vuint(port)
699 raise Exception("EOF while reading Metadata")
702 return stat.S_ISDIR(self.mode)
704 def create_path(self, path, create_symlinks=True):
705 self._create_via_common_rec(path, create_symlinks=create_symlinks)
707 def apply_to_path(self, path=None, restore_numeric_ids=False):
708 # apply metadata to path -- file must exist
712 raise Exception('Metadata.apply_to_path() called with no path')
713 if not self._recognized_file_type():
714 add_error('not applying metadata to "%s"' % path
715 + ' with unrecognized mode "0x%x"\n' % self.mode)
717 num_ids = restore_numeric_ids
719 self._apply_common_rec(path, restore_numeric_ids=num_ids)
720 self._apply_posix1e_acl_rec(path, restore_numeric_ids=num_ids)
721 self._apply_linux_attr_rec(path, restore_numeric_ids=num_ids)
722 self._apply_linux_xattr_rec(path, restore_numeric_ids=num_ids)
723 except ApplyError, e:
726 def same_file(self, other):
727 """Compare this to other for equivalency. Return true if
728 their information implies they could represent the same file
729 on disk, in the hardlink sense. Assume they're both regular
731 return self._same_common(other) \
732 and self._same_hardlink_target(other) \
733 and self._same_posix1e_acl(other) \
734 and self._same_linux_attr(other) \
735 and self._same_linux_xattr(other)
738 def from_path(path, statinfo=None, archive_path=None,
739 save_symlinks=True, hardlink_target=None):
741 result.path = archive_path
742 st = statinfo or xstat.lstat(path)
743 result.size = st.st_size
744 result._add_common(path, st)
746 result._add_symlink_target(path, st)
747 result._add_hardlink_target(hardlink_target)
748 result._add_posix1e_acl(path, st)
749 result._add_linux_attr(path, st)
750 result._add_linux_xattr(path, st)
754 def save_tree(output_file, paths,
760 # Issue top-level rewrite warnings.
762 safe_path = _clean_up_path_for_archive(path)
763 if safe_path != path:
764 log('archiving "%s" as "%s"\n' % (path, safe_path))
766 start_dir = os.getcwd()
768 for (p, st) in recursive_dirlist(paths, xdev=xdev):
769 dirlist_dir = os.getcwd()
771 safe_path = _clean_up_path_for_archive(p)
772 m = from_path(p, statinfo=st, archive_path=safe_path,
773 save_symlinks=save_symlinks)
775 print >> sys.stderr, m.path
776 m.write(output_file, include_path=write_paths)
777 os.chdir(dirlist_dir)
782 def _set_up_path(meta, create_symlinks=True):
783 # Allow directories to exist as a special case -- might have
784 # been created by an earlier longer path.
788 parent = os.path.dirname(meta.path)
791 meta.create_path(meta.path, create_symlinks=create_symlinks)
794 all_fields = frozenset(['path',
811 def summary_str(meta):
812 mode_val = xstat.mode_str(meta.mode)
815 user_val = str(meta.uid)
816 group_val = meta.group
818 group_val = str(meta.gid)
819 size_or_dev_val = '-'
820 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
821 size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
823 size_or_dev_val = meta.size
824 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
825 time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
826 path_val = meta.path or ''
827 if stat.S_ISLNK(meta.mode):
828 path_val += ' -> ' + meta.symlink_target
829 return '%-10s %-11s %11s %16s %s' % (mode_val,
830 user_val + "/" + group_val,
836 def detailed_str(meta, fields = None):
837 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
838 # 0", "link-target:", etc.
844 path = meta.path or ''
845 result.append('path: ' + path)
847 result.append('mode: %s (%s)' % (oct(meta.mode),
848 xstat.mode_str(meta.mode)))
849 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
850 result.append('link-target: ' + meta.symlink_target)
853 result.append('rdev: %d,%d' % (os.major(meta.rdev),
854 os.minor(meta.rdev)))
856 result.append('rdev: 0')
857 if 'size' in fields and meta.size:
858 result.append('size: ' + str(meta.size))
860 result.append('uid: ' + str(meta.uid))
862 result.append('gid: ' + str(meta.gid))
864 result.append('user: ' + meta.user)
865 if 'group' in fields:
866 result.append('group: ' + meta.group)
867 if 'atime' in fields:
868 # If we don't have xstat.lutime, that means we have to use
869 # utime(), and utime() has no way to set the mtime/atime of a
870 # symlink. Thus, the mtime/atime of a symlink is meaningless,
871 # so let's not report it. (That way scripts comparing
872 # before/after won't trigger.)
873 if xstat.lutime or not stat.S_ISLNK(meta.mode):
874 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
876 result.append('atime: 0')
877 if 'mtime' in fields:
878 if xstat.lutime or not stat.S_ISLNK(meta.mode):
879 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
881 result.append('mtime: 0')
882 if 'ctime' in fields:
883 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
884 if 'linux-attr' in fields and meta.linux_attr:
885 result.append('linux-attr: ' + hex(meta.linux_attr))
886 if 'linux-xattr' in fields and meta.linux_xattr:
887 for name, value in meta.linux_xattr:
888 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
889 if 'posix1e-acl' in fields and meta.posix1e_acl and posix1e:
890 flags = posix1e.TEXT_ABBREVIATE
891 if stat.S_ISDIR(meta.mode):
892 acl = meta.posix1e_acl[0]
893 default_acl = meta.posix1e_acl[2]
894 result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
895 result.append(acl.to_any_text('posix1e-acl-default: ', '\n', flags))
897 acl = meta.posix1e_acl[0]
898 result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
899 return '\n'.join(result)
902 class _ArchiveIterator:
905 return Metadata.read(self._file)
907 raise StopIteration()
912 def __init__(self, file):
916 def display_archive(file):
919 for meta in _ArchiveIterator(file):
922 print detailed_str(meta)
925 for meta in _ArchiveIterator(file):
926 print summary_str(meta)
928 for meta in _ArchiveIterator(file):
930 print >> sys.stderr, \
931 'bup: no metadata path, but asked to only display path', \
932 '(increase verbosity?)'
937 def start_extract(file, create_symlinks=True):
938 for meta in _ArchiveIterator(file):
939 if not meta: # Hit end record.
942 print >> sys.stderr, meta.path
943 xpath = _clean_up_extract_path(meta.path)
945 add_error(Exception('skipping risky path "%s"' % meta.path))
948 _set_up_path(meta, create_symlinks=create_symlinks)
951 def finish_extract(file, restore_numeric_ids=False):
953 for meta in _ArchiveIterator(file):
954 if not meta: # Hit end record.
956 xpath = _clean_up_extract_path(meta.path)
958 add_error(Exception('skipping risky path "%s"' % dir.path))
960 if os.path.isdir(meta.path):
961 all_dirs.append(meta)
964 print >> sys.stderr, meta.path
965 meta.apply_to_path(path=xpath,
966 restore_numeric_ids=restore_numeric_ids)
967 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
969 # Don't need to check xpath -- won't be in all_dirs if not OK.
970 xpath = _clean_up_extract_path(dir.path)
972 print >> sys.stderr, dir.path
973 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
976 def extract(file, restore_numeric_ids=False, create_symlinks=True):
977 # For now, just store all the directories and handle them last,
980 for meta in _ArchiveIterator(file):
981 if not meta: # Hit end record.
983 xpath = _clean_up_extract_path(meta.path)
985 add_error(Exception('skipping risky path "%s"' % meta.path))
989 print >> sys.stderr, '+', meta.path
990 _set_up_path(meta, create_symlinks=create_symlinks)
991 if os.path.isdir(meta.path):
992 all_dirs.append(meta)
995 print >> sys.stderr, '=', meta.path
996 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
997 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
999 # Don't need to check xpath -- won't be in all_dirs if not OK.
1000 xpath = _clean_up_extract_path(dir.path)
1002 print >> sys.stderr, '=', xpath
1003 # Shouldn't have to check for risky paths here (omitted above).
1004 dir.apply_to_path(path=dir.path,
1005 restore_numeric_ids=restore_numeric_ids)